一、基本概念
动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。
二、基本思想与策略
动态规划算法通常基于一个递推公式及一个或多个初始状态。
基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息, 当前子问题的解将由上一次子问题的解推出。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。这点和数学中的归纳是一样的思想。
由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。
三、动态规划算法的设计
两种方法:
自顶向下(又称记忆化搜索、备忘录):基本上对应着递归函数实现,从大范围开始计算,要注意不断保存中间结果,避免重复计算
自底向上(递推):从小范围递推计算到大范围
动态规划的重点:
递归方程+边界条件
四、适用的情况
能采用动态规划求解的问题的一般要具有3个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
五、求解的基本步骤
动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。
初始状态→│决策1│→│决策2│→…→│决策n│→结束状态
图1 动态规划决策过程示意图
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。
实际应用中可以按以下几个简化的步骤进行设计:
(1)分析最优解的性质,并刻画其结构特征。
(2)递归的定义最优解。
(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值
(4)根据计算最优值时得到的信息,构造问题的最优解
六、算法实现的说明
动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。
使用动态规划求解问题,最重要的就是确定动态规划三要素:
(1)问题的阶段 (2)每个阶段的状态
(3)从前一个阶段转化到后一个阶段之间的递推关系。
递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。
确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。
七、应用实例介绍:
1. 爬楼梯问题
一个人每次只能走一层楼梯或者两层楼梯,问走到第80层楼梯一共有多少种方法。
设DP[i]为走到第i层一共有多少种方法,那么DP[80]即为所求。很显然DP[1]=1, DP[2]=2(走到第一层只有一种方法:就是走一层楼梯;走到第二层有两种方法:走两次一层楼梯或者走一次两层楼梯)。那么走到第三层就可以从第一层走两层、或者从第二层走一层到达,这样走到第三层的方法就等于走到第一层的方法和走到第二层的方法之和。同理,走到第i层楼梯,可以从i-1层走一层,或者从i-2走两层。很容易得到:
递推公式: DP[i]=DP[i-1]+DP[i-2]
边界(初始)条件:DP[1]=1 DP[2]=2
自底向上的解法:
- long long DP(int N) {
- long[] dp = newlong[N+1];
- if(N>0) dp[1] = 1;
- if(N>1) dp[2] = 2;
- if(N>3){
- for(int i=3; i<=N; i++) {
- dp[n] = dp(n-1) + dp(n-2);
- }
- }
- return dp[N];
- }
自顶向下的解法:(递归调用)
- long long dp[81] = {0};/*用于保存中间结果 */
- long long DP(int n)
- {
- if(dp[n] > 0)
- return dp[n];
- if(n == 1)
- return 1;
- if(n == 2)
- return 2;
- dp[n] = DP(n-1) + DP(n-2);
- return dp[n];
- }
2、背包问题
有N件物品和一个容量为M的背包。第i件物品的体积是c[i],价值是v[i]。求解将哪些物品装入背包可使价值总和最大。
我们把题目具体下, 有5个商品,背包的体积为10,他们的体积为 c[5] = {3,5,2,7,4}; 价值为 v[5] = {2,4,1,6,5};
问题分析:
令V(i,j)表示在前i(0<=i<=N)个物品中能够装入容量为就j(0<=j<=M)的背包中的物品的最大价值,则可以得到如下的动态规划函数:
(1) V(i,0)=V(0,j)=0 //边界条件
(2) V(i,j)=V(i-1,j) j<ci
V(i,j)=max{V(i-1,j) ,V(i-1,j-ci)+vi) } j>ci
(2)式中第一个表达式表明:如果第i个物品的体积大于背包的容量,则装人前i个物品得到的最大价值和装入前i-1个物品得到的最大价是相同的,即物品i不能装入背包;
(2)式中第二个表达式表明: 如果第i个物品的体积小于背包的容量,则会有一下两种情况:(a)如果把第i个物品装入背包,则背包物品的价值等于第i-1个物品装入容量位j-wi 的背包中的最大价值加上第i个物品的价值vi; (b)如果第i个物品没有装入背包,则背包中物品价值就等于把前i-1个物品装入容量为j的背包中所取得的最大价值。显然,取二者中价值最大的作为把前i个物品装入容量为j的背包中的最大价值的最优解。
最后V(N, M)的值为最大值。
实例分析:
1. 初始状态: V(i,0)=0, V(0, j)=0. 当物品数量为0时,0~M体积的背包装入物品的价值为0;同样当背包体积为0时,前N个物品能装入背包的数目为0,价值也为0;
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 |
|
|
|
|
|
|
|
|
|
|
2 | 0 |
|
|
|
|
|
|
|
|
|
|
3 | 0 |
|
|
|
|
|
|
|
|
|
|
4 | 0 |
|
|
|
|
|
|
|
|
|
|
5 | 0 |
|
|
|
|
|
|
|
|
|
|
2. 当物品为第一个物品时,如果该物品的体积大于背包的体积,则价值为0,反之背包可装入的最大价值则为该物品的价值。
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
2 | 0 | ||||||||||
3 | 0 | ||||||||||
4 | 0 | ||||||||||
5 | 0 |
3、当装入物品为第二个时,i=2, 对于体积从0到M,即0<i<=10进行计算。首先判断ci是否大于当前背包体积j, 如果 ci > j, 则该物品不能放入背包,此时背包的最大价值则为第一个物品时的最大价值,即 V(i, j) = V(i-1, j);
如果 ci<j, 则该物品可以放进背包,此时计算将该物品放入背包中后,背包的最大价值就等于此时背包的体积 j 减去该物品的体积 ci 后的体积能容纳前 i-1 物品的最大价值加上该物品i的价值,即 V(i-1, j-ci)+vi; 然后和该物品不放入背包中的最大价值 V(i-1, j) 进行比较, 两者取最大值,即是前 i 物品中可装入容积为j的背包的最大值V(i, j)。
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
2 | 0 | 0 | 0 | 2 | 2 | 4 | 4 | 4 | 6 | 6 | 6 |
3 | 0 | ||||||||||
4 | 0 | ||||||||||
5 | 0 |
4. 当装入物品为第三个时,同上可计算:
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
2 | 0 | 0 | 0 | 2 | 2 | 4 | 4 | 4 | 6 | 6 | 6 |
3 | 0 | 0 | 1 | 2 | 2 | 4 | 4 | 5 | 6 | 6 | 7 |
4 | 0 |
|
|
|
|
|
|
|
|
|
|
5 | 0 |
5. 同理计算,第四个第五个:
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
2 | 0 | 0 | 0 | 2 | 2 | 4 | 4 | 4 | 6 | 6 | 6 |
3 | 0 | 0 | 1 | 2 | 2 | 4 | 4 | 5 | 6 | 6 | 7 |
4 | 0 | 0 | 1 | 2 | 2 | 4 | 4 | 6 | 6 | 7 | 8 |
5 | 0 | 0 | 1 | 2 | 5 | 5 | 6 | 7 | 7 | 9 | 9 |
代码示例:
public static int dealPackage(int[] C, int[] V, int N, int M) {
int[][] MaxV = new int[N+1][M+1];
int i, j;
for(i=0; i<=N; i++) {
MaxV[i][0] = 0;
}
for(j=0; j<=M; j++) {
MaxV[0][j] = 0;
}
int temp;
for(i=1; i<=N; i++) {
for(j=1; j<=M; j++) {
if(C[i] > j) {
MaxV[i][j] = MaxV[i-1][j];
} else {
temp = MaxV[i-1][j-C[i]] + V[i];
MaxV[i][j] = temp>MaxV[i-1][j] ? temp:MaxV[i-1][j];
}
}
}
return MaxV[N][M];
}