vlambda博客
学习文章列表

什么影响了你理解动态规划(一)

什么影响了你理解动态规划(一)

1. 简介动态规划

几乎很难给动态规划下一个易于理解的定义,映入眼帘的只有动态二字,将动态与其定义作一个贴切的解释就是多阶段决策。最后的结果与每个阶段的决策有关,其动态的概念在于最后的结果与每一个阶段的决策有关。这样一来将动态规划与贪心算法作比较就可以发现,动态规划属于全局最优算法,贪心算法属于局部最优算法。在了解动态规划的第一步只要知道其两个特点即可:多阶段与决策。

2. 动态规划问题的完整建模过程

(1)将问题过程划分成恰当的阶段;

(2)正确选择状态变量  ,使它即能描述过程的演变,又要满足无后效性;

(3)确定决策变量  及每阶段的允许决策的集合  ;

(4)正确写出状态转移方程;

在实际解题过程中,人们往往将重心放在第(2)步和第(4)步上。因为大多数情况下,很难确定将问题分为多少个阶段,要么阶段在题目表示中已经暗示,要么这个阶段数是动态的。而第(3)步也同样重要,因为每一个阶段都要在决策集中选择一个决策才能到达下一个阶段。

3. 什么影响了你理解动态规划

网上部分文章喜欢举斐波那契数列的例子来向初学者说明动态规划的内涵。但不幸的是,这个例子的建模过程缺少了其中的某些步骤,会导致读者在理解上出现偏差。在此说明斐波那契数列的例子在哪些步骤会缺省。

斐波那契数列的递推定义为:。之所以很多人喜欢用这个例子是因为,它天然地给出了递推式与状态表示,让人轻而易举的写出程序。还可以举出递归做法与动态规划做法的一些比较。但这并不足以帮助读者理解什么是动态规划,只是徒增了无畏的成就感罢了。

假设要求斐波那契数列的第100个数,则题目天然的将建模过程分成了100个阶段,每个阶段依赖它前面的两个数( )。同样很自然地,用数组变量f[i]代表第i个数完成了状态表示。但是它唯独缺少了第(3)步,每一个阶段并没有决策集或者说它的每一个阶段有且仅有唯一的一个决策(就是依赖它前面的两个数)。这个例子的第(3)步骤不够明显,导致许多读者并无法通过这个例子去理解动态规划的决策集,他只记得了这个例子,并不会记得动态规划。

4. 举一个可以让你理解动态规划建模四个步骤的例子

4.1 找零

题目描述

Z国的货币系统包含面值1元、4元、16元、64元共计4种硬币,以及面值1024元的纸币。现在小Y使用1024元的纸币购买了一件价值为 的商品,请问最少他会收到多少硬币?

输入描述:

一行,包含一个数N。

输出描述:

一行,包含一个数,表示最少收到的硬币数。

示例1

输入

200

输出

17

说明

花200,需要找零824块,找12个64元硬币,3个16元硬币,2个4元硬币即可。

决策集

可能一看题目,给人最直观的感觉就是决策集,可以将当前找零的n元的纸币先换成 1元 或 4元 或 16元 或 64元的一枚硬币,然后剩下 n-1元 或 n-4元 或 n-16元 或 n-64元的纸币,则进入了下一个阶段,下一个阶段也同样是在这个决策集中选择。其中的分法就是组成了决策集,需要读者慢慢体会一下。

阶段与状态表示

这道题目的状态表示也非常简单,可以使用数组 f[n]来表示 n元纸币的最小组成硬币数。其实这个天然已经帮助我们确定了计算的阶段数。

状态转移方程

在给出状态转移方程前,先考虑这样一个例子。求100元纸币的最小组成硬币数。

图一

所以状态转移方程就是:

每一个 均代表一个阶段,由状态转移方程可以得到,当前阶段需要依赖之前的阶段,由此可得出计算顺序为 n从小到大开始计算。

 public static int func(int N){ int[] coins = {1,4,16,64}; int n=1024-N; int[] f= new int[n+1]; f[0]=0; for(int j=1;j<=n;j++){ // 分为 n 个阶段,按照状态转移方程 n 从小到大进行计算 f[j]=Integer.MAX_VALUE; for(int i=0;i<coins.length;i++){ // 每一个阶段都要在决策集中进行决策,coins 即为决策集 if(j>=coins[i]){ f[j]=Math.min(f[j],1+f[j-coins[i]]); } } } return f[n]; }

4.2  01背包问题

题目描述

在n个物品中挑选若干物品装入背包,最多能装多满?假设背包的大小为m,每个物品的大小为A[i]。

例子

样例 1: 输入: [3,4,8,5], backpack size=10 输出: 9
样例 2: 输入: [2,3,5,7], backpack size=12 输出: 12

确定阶段

题目其实已经暗示了阶段数,即在遍历到第 i 个物品时,即处于第 i 个阶段,共有 n 个物品,即会有 n 个阶段。

决策集

决策集其实就只有两种,即放入背包,不放入背包。

状态表示

对于背包问题而言,我们的状态即要表示当前处于第几个阶段,又要表示对当前物品做出决策后记录背包的重量。

使用 f[i][w]来表示在第 i阶段时,对第 i个物品作出决策(放入与不放入)后,背包能否达到 w重量(true / false)。

状态转移方程

f[i][w]表示能否用前 i个物品拼出重量 w重量(true/false)。

图二

代码:

 public int backPack(int m, int[] A) { int n=A.length; if(n==0) return 0;  boolean[][] f=new boolean[n+1][m+1]; f[0][0]=true; // 分成 n 个阶段,每个阶段选择一个物品 for(int i=1;i<=n;i++){  for(int j=0;j<=m;j++){ // 对第i个物品做出不放入的决策 f[i][j]=f[i-1][j];  // 对第i个物品做出放入的决策 if(j-A[i-1]>=0&&f[i-1][j-A[i-1]]){  f[i][j]=true; }  } } int res=0; for(int i=m;i>=0;i--){ if(f[n][i]){ res=i; break; } } return res; }

总结