算法导论:动态规划
多阶段决策问题
求解的问题可以划分为一系列相互联系的阶段,在每个阶段都需要作出决策,且一个阶段决策的选择会影响下一个阶段的决策,从而影响整个过程的活动路线,求解的目标是选择各个阶段的决策使整个过程达到最优。
基本概念
- 阶段:把所给的问题的求解过程恰当地划分为若干个相互联系的阶段
- 状态:表示每个阶段开始时,问题或系统所处的客观状况。状态既是该阶段的某个起点,又是前一个阶段的某个终点。通常一个阶段有若干个状态。
- 状态的无后效性:如果某阶段状态给定后,则该阶段以后过程的发展不受该阶段以前各阶段状态的影响,也就是说状态具有马尔科夫性
- 策略:各个阶段决策的确定后,就组成了一个决策序列,该序列称之为一个策略。由某个阶段开始到终止阶段的过程称为子过程,其对应的某个策略称为子策略。
最优化原理
求解问题的一个最优策略序列的子策略序列总是最优的,则称该问题满足最优性原理。
对具有最优性原理性质的问题而言,如果有一决策序列包含有非最优的决策子序列,则该决策序列一定不是最优的。
最优性原理判别
设
是一个有向加权图,则 从顶点 到顶点 之间的最短路径问题满足最优性原理
反证法:
设
假设最优路径是
显然路径
所以该问题满足最优性原理
最长路径问题不满足最优性原理
反例:
但是
所以,该问题不满足最优性原理。
基本思想
动态规划的思想实质是分治思想和解决冗余。
- 与分治法类似的是:将原问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
- 与分治法不同的是:经分解的子问题往往不是互相独立的。若用分治法来解,有些共同部分(子问题或子子问题)会被重复计算了很多次。
动态规划用一个表来记录所有已解的子问题的答案。
求解步骤
- 找出最优解的性质,并刻画其结构特征
- 递归地定义最优值(写出动态规划方程)
- 以自底向上的方式计算出最优值
- 根据计算最优值时记录的信息,构造最优解
适用条件
动态规划法的有效性依赖于问题本身所具有的两个重要的适用性质:
- 最优子结构:问题的最优解是由其子问题的最优解来构造
- 重叠子问题:在用递归算法自顶向下解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次
背包问题
问题描述
- 给定
个物品:- 整数容量:
- 价值:
- 整数容量:
- 具有整数
容量的背包
目标:寻找最优价值的子集可以放入背包中。
最优性原理判别
以
背包问题 为例
设
如果不是子问题的最优解,则设
这说明
解题思路
假设我们已经求解了在前
则递推关系:
初始条件:
举例
背包容量
物品 | 体积 | 价值 |
---|---|---|
1 | 2 | 12 |
2 | 1 | 10 |
3 | 3 | 20 |
4 | 2 | 15 |
解如下,横坐标表示容量
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 12 | 12 | 12 | 12 |
0 | 10 | 12 | 22 | 22 | 22 |
0 | 10 | 12 | 22 | 30 | 32 |
0 | 10 | 15 | 25 | 30 | 37 |
伪代码
int[][] DPKnapsack(int[] w, int[] v, int W, int n) {
for(int j = 0; j <= W; j++) V[0][j] = 0;
for(int i = 0; i <= n; i++) V[i][0] = 0;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= W; j++) {
if(w[i] <= j && v[i] + V[i-1][j-w[i]] > V[i-1][j]) {
V[i][j] = v[i] + V[i-1][j-w[i]];
}
else V[i][j] = V[i-1][j];
}
}
return V;
}
切杆问题
问题描述
给定一段长度为
例如:
显而易见,对于 4 英寸的钢条来说,
自顶向下递归
问题分解为:将长度为
int cutRod(int[] p, int n) {
if(n == 0) return 0;
int q = -inf;
for(int i = 1; i <= n; i++) {
q = max(q, p[i]+cutRod(p, n-i));
}
return q;
}
但是该方法效率十分差,因为它会反复地用相同的参数值对自身进行递归调用。
带备忘的自顶向下
自顶向下的过程中,会保存每个子问题的解(数组或者散列表)。当需要一个子问题的解时,首先检查是否已经保存过此解。
int memorizedCutRod(int[] p, int n, int[] r) {
if(r[n] >= 0) return r[n];
if(n == 0) return 0;
else {
int q = -inf;
for(int i = 1; i <= n; i++) {
q = max(q, p[i]+memorizedCutRod(p, n-i, r));
}
}
r[n] = q;
return q;
}
自底向上
将子问题按规模排序,按由小到大的顺序进行求解。当求解某个子问题时,它所依赖的哪些更小的子问题都已求解完毕,结果已经保存。每个子问题只需求解一次,当我们求解它时,它的所有前提子问题都已求解完成。
int bottomUpCutRod(int[] p, int n) {
int[] r;
r[0] = 0;
for(int i = 1; i <= n; i++) {
int q = -inf;
for(int j = 1; j <= i; j++) {
q = max(q, p[i] + r[j-i]);
}
r[i] = q;
}
return r[n];
}
最长公共子序列(LCS)
问题描述
输入:
输出:
LCS 最优解结构特征
定义
设序列
- 若
,且 是 和 的一个 LCS - 若
是 和 的一个 LCS - 若
是 和 的一个 LCS
子问题的递归解
计算最优解值
当构造解时,从
void LCS(String X, String Y) {
int m = length(X);
int n = length(Y);
for(int i = 0; i <= m; i++) c[i][0] = 0;
for(int j = 0; j <= n; j++) c[0][j] = 0;
for(int i = 1; i <= m; i++) {
for(int j = 1; j <= n; j++) {
if(X[i] == Y[j]) {
c[i][j] = c[i-1][j-1]+1;
b[i][j] = "1";
}
else if(c[i-1][j] >= c[i][j-1]) {
c[i][j] = c[i-1][j];
b[i][j] = "2";
}
else {
c[i][j] = c[i][j-1];
b[i][j] = "3";
}
}
}
}
时间
构造一个 LCS
void printLCS(char[][] b, String X, int i, int j) {
if(i == 0 || j == 0) return;
if(b[i][j] == '1') {
printLCS(b, X, i-1, j-1);
print(X[i]);
}
else {
if(b[i][j] == '2') printLCS(b, X, i-1, j);
else printLCS(b, X, i, j-1);
}
}
时间
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步