目录
一、基本概念
动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。
动态规划算法通常用于求解最优解的问题,在这类问题中有很多可行解,每个可行解都对应一个值(cost or reward),希望求出最佳解(最小 cost or 最大 reward)。动态规划的基本思想与分治法类似,但差异在于动态规划法可以解决划分的子问题不独立的情况。若在划分子问题不独立的情况下使用分治法会导致子问题数目太多,许多子问题经过重复计算。DP 中将已解决的子问题答案保存到表中,从而减少算法的时间冗余。
1.多阶段决策问题
动态规划适用于多阶段决策问题。
若一类活动过程可以分为若干个互相联系的阶段,每个阶段都需要作出决策,每个阶段的决策确定常常影响到下一阶段的决策,从而完全确定一个过程的活动路线,则称其为多阶段决策问题。
各个阶段的决策构成决策序列,称为一个策略。每个阶段都有若干决策可选择,使得有大量可选择策略。每个策略的效果不同,多阶段决策问题就是要在可选的策略中选取一个最优策略,使得在预定的标准下达到最好的效果。
2.适用条件
(1)最优化原理(最优子结构性质)
最优化策略具有如下的性质:不论过去状态和决策如何,对之前的决策形成的状态而言,剩下的所有决策必须构成最优策略。简言之,一个最优化策略的子策略总是最优的,一个问题满足最优化原理时又称其具有最优子结构性质。
即最终的大问题的最优解可以通过逐个求解小问题最优解推出。
比如规划确定起点和目的地的需要中转的航班路线时,假设要从海南 → \to →黑龙江,最优航线是海南 → \to →安徽 → \to →北京 → \to →辽宁 → \to →黑龙江,那么从安徽 → \to →黑龙江的最优航线一定是这个解的一部分,即安徽 → \to →北京 → \to →辽宁 → \to →黑龙江。那么就称最优航线确定的问题满足最优化原理。
(2)无后效性
将各阶段按照一定的次序排列好后,对于某个给定的阶段状态,给定阶段前的各阶段状态不会直接影响其未来的决策,而只能通过当前状态。即每个状态都是对过去历史的完整总结,这就是无后效性。
即如果给定某一状态,则在这一阶段以后过程的发展不受这阶段以前各段状态的影响。
同时满足以上最优化原理和无后效性时,可以使用动态规划求解问题。
二、算法步骤
- 通过优化问题的目标,确定动态规划数组的定义,即 dp[i] 在当前问题中具体表示什么。
- 找出数组元素之间关系,比如 dp[n] = dp[n-1] + dp[n-2]。
- 找出当前问题的初始值。
三、算法例题
1.爬楼梯问题(一维动态规划数组)
假设这样一个情境:你准备上楼梯,一次可以走1级台阶,也可以走2级。求上一个n级的台阶总共有多少种走法。
(1)确定动态规划数组定义
要求上n级台阶总共多少种走法,不妨直接将动态规划数组 dp[i] 定义为上 i 级台阶总共的走法数量。即最终要求的结果就是 dp[n] 。
(2)数组元素间的关系
要找出数组元素之间的关系,需要将规模较大的问题划分成多个规模较小的问题。一般来说,这一步是找到用 dp[i-1]、dp[i-2] 等规模较小的动态规划数组值表示 dp[i] 这个规模较大的动态规划数组值。
结合题目和上一步确定的动态规划数组的定义,dp[i] 是上 i 级台阶总共的走法数量,因为只能一次上1级或者上2级,上 i 级台阶的情况就可以分为两类:
- 从 i - 1 级台阶走 1 级后到达第 i 级
- 从 i - 2 级台阶走 2 级后到达第 i 级
上到第 i 级台阶的总共走法数量应当是上述两种情况走法数量的和, 即得到: d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] dp[i] = dp[i-1] + dp[i-2] dp[i]=dp[i−1]+dp[i−2]
(3)确定初始状态
i = 0 或 1 时, d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] dp[i] = dp[i-1] + dp[i-2] dp[i]=dp[i−1]+dp[i−2]出现数组下标为负数的情况,故需要单独对 i = 0 或 1 的情况进行数组赋值。
不难得知: d p [ 0 ] = 0 , d p [ 1 ] = 1 dp[0] = 0, dp[1] = 1 dp[0]=0,dp[1]=1
而验证时发现 i = 2 时,根据递推式有: d p [ 2 ] = d p [ 1 ] + d p [ 0 ] = 1 dp[2] = dp[1] + dp[0] = 1 dp[2]=dp[1]+dp[0]=1,而实际情况应当是 d p [ 2 ] = 2 dp[2] = 2 dp[2]=2,故也设定 i = 2 的初始状态: d p [ 2 ] = 2 dp[2] = 2 dp[2]=2
注意:在确定初始状态时不要遗漏,应当多检查几项的合理性。
至此,可以得到问题求解的函数代码:
int goUpstairs( int n )
{
if(n <= 1)
return n;
// 创建dp数组,用于存储较小规模问题的解
int[] dp = new int[n+1];
// 给出初始值
dp[0] = 0;
dp[1] = 1;
dp[2] = 2;
// 通过数组元素间关系来计算 dp[i]
for(int i = 2; i <= n; i++){
dp[i] = dp[i-1] + dp[i-2];
}
// 返回最终结果
return dp[n];
}
2.数字三角形(二维动态规划数组)
有如下一个数字三角形:
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
从第一行(7)出发,每个节点都可以选择向下走或向右下走,一直走到底层。试设计一种算法,计算从三角形顶端到底部的一条路径,使该路径经过的数字总和最大。
(1)确定动态规划数组定义
为方便数组索引,将数字三角形的第 1 行表示为第 0 行,要求从第 0 行出发到第 i 行、第 j 列的最大数字总和,不妨直接定义二维动态规划数组 dp[ i ][ j ] 定义为从第 0 行出发到第 i 行、第 j 列的最大数字总和。即最终要求的结果就是 dp[4][j] 。
(2)数组元素间的关系
结合题目和上一步确定的动态规划数组的定义,dp[ i ][ j ] 是从第 0 行出发到第 i 行、第 j 列的最大数字总和,因为每次只能向正下方和右下方移动,到达[ i ][ j ]位置的情况就可以分为两类:
- 从 [ i - 1 ][ j ] 向下方移动后到达 [ i ][ j ]
- 从 [ i - 1 ][ j - 1 ] 向右下方移动后到达 [ i ][ j ]
到达 [ i ][ j ] 位置的最大和应当是上述两种情况最大和的较大值加上当前 [ i ][ j ] 位置的值(D[i][j]), 即得到: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − 1 ] ) + D [ i ] [ j ] dp[i][j] = max(dp[i-1][j], dp[i-1][j-1]) + D[i][j] dp[i][j]=max(dp[i−1][j],dp[i−1][j−1])+D[i][j]
(3)确定初始状态
易知 i = 0 或 j = 0 时,会出现递推式中数组下标小于 0 的情况。
i = 0 时,即当前为三角形第一行,定义此时初始条件: d p [ 0 ] [ 0 ] = D [ 0 ] [ 0 ] dp[0][0] = D[0][0] dp[0][0]=D[0][0]
j = 0 时,即当前为三角形第一列,定义此时初始条件: d p [ i ] [ 0 ] = D [ 0 ] [ 0 ] + D [ 1 ] [ 0 ] + . . . + D [ i ] [ 0 ] dp[i][0] = D[0][0] + D[1][0] + ... + D[i][0] dp[i][0]=D[0][0]+D[1][0]+...+D[i][0]
至此,可以得到问题求解的函数代码:
int minNumberInRotateArray(int n[][])
{
int max = 0;
int dp[][] = new int[n.length][n.length];
dp[0][0] = n[0][0];
for(int i=1;i<n.length;i++){
for(int j=0;j<=i;j++){
if(j==0)
{
// 若是第一列,直接与正上方数字相加
dp[i][j] = dp[i-1][j] + n[i][j];
}
else
{
// 若不是第一列,比较其上方和左上dp,与较大者相加,放到这个位置
dp[i][j] = Math.max(dp[i-1][j-1], dp[i-1][j]) + n[i][j];
}
max = Math.max(dp[i][j], max);
}
}
return max;
}
3.DAG(有向无环图)最短路径规划
给定一个通过边连接的结点网络,所有的边都是单向边,且不会构成环。每个边都有长度,求解从 S 点到 T 点的最短路径长度。
(1)确定动态规划数组定义
要求从 S 到 T 的最短路径长度,不妨设 dp[ i ] 为从 S 点到 i 点的最短路径长度。
(2)数组元素间的关系
结合题目和上一步确定的动态规划数组的定义,dp[ i ] 为从 S 点到 i 点的最短路径长度,因为只能从与当前结点有边连接的结点到达当前结点,故到达当前结点的最短路径应当是到达当前结点前一个结点的最短路径加上前一个结点到当前结点的路径,选取其中的最短路径作为以当前结点为终点的最短路径。
即有: d p [ i ] = m i n { d p [ a ] + w [ a ] [ i ] ∣ a 可 以 到 达 i } dp[i] = min\{dp[a] + w[a][i]\ |\ a 可以到达i\} dp[i]=min{dp[a]+w[a][i] ∣ a可以到达i}
(3)确定初始状态
起点位置的 dp 数组值是需要单独初始化的初始状态,即设定初始状态: d q [ s ] = 0 dq[s] = 0 dq[s]=0
至此,可以得到问题求解的函数代码:
DagShortestPath(G, w, s, t)
{
// 对结点拓扑排序
topologically sort the nodes in G
// 初始化
for each vertex v in G
{
dp[v] = INF;
pre[v] = NULL;
}
dp[s] = 0;
// 根据拓扑顺序,遍历顶点v
for each v in G, taken in topologically sorted order
{
for each edge w[u][v]
{
if (dp[v] > dp[u] + w[u][v])
{
dp[v] = dp[u] + w[u][v];
pre[v] = u;
}
}
}
return dp[t];
}