1.动态规划基础
动态规划应用于子问题重叠的情况:
- 要去刻画最优解的结构特征;
- 尝试递归地定义最优解的值(就是我们常说的考虑从
转移到 ); - 计算最优解;
- 利用计算出的信息构造一个最优解。
# 钢条切割
给定一段钢条,和不同长度的价格,问如何切割使得总价格最大。
为了求解规模为
最优子结构:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。
动态规划的两种实现方法:
- 带备忘的自顶向下法(记忆化搜索);
- 自底向上法(将子问题按规模排序,类似于递推)。
算导用子问题图上按照逆拓扑序求解问题,引出记忆化搜索。
重构解(输出方案):转移的时候记录最优子结构的位置。
# 矩阵链乘法
给出
(认为
完全括号化方案是指要给出谁先和谁乘。
# 动态规划原理
两个要素:
# 最优子结构
具有最优子结构也可能是适合用贪心的方法求解。
注意要确保我们考察了最优解中用到的所有子问题。
- 证明问题最优解的第一个组成部分是做出一个选择;
- 对于一个给定问题,在其可能的第一步选择中,你界定已经知道哪种选择才会得到最优解。你现在并不关心这种选择具体是如何得到的,只是假定已经知道了这种选择;
- 给定可获得的最优解的选择后,确定这次选择会产生哪些子问题,以及如何最好地刻画子问题空间;
- 证明作为构成原问题最优解的组成部分,每个子问题的解就是它本身的最优解。方法是反证法,考虑加入某个子问题的解不是其自身的最优解,那么就可以从原问题的解中用该子问题的最优解替换掉当前的非最优解,从而得到原问题的一个更优的解,从而与原问题最优解的假设矛盾。
要保持子问题空间尽量简单,只在必要时扩展。
最优子结构的不同体现在两个方面:
- 原问题的最优解中涉及多少个子问题;
- 确定最优解使用哪些子问题时,需要考察多少种选择。
子问题图中每个定点对应一个子问题,而需要考察的选择对应关联至子问题顶点的边。
经典问题:
- 无权最短路径: 具有最优子结构性质。
- 无权最长(简单)路径: 此问题不具有,是 NPC 的。区别在于,要保证子问题无关,即同一个原问题的一个子问题的解不影响另一个子问题的解。相关:求解一个子问题时用到了某些资源,导致这些资源在求解其他子问题时不可用。
# 子问题重叠
子问题空间要足够小,即问题的递归算法会反复地求解相同的子问题,而不是一直生成新的子问题。
# 重构最优解
存表记录最优分割的位置,就不用重新按照代价来重构。
# 最长公共子序列
子序列允许不连续。
每个
记录最优方案的时候可以不需要额外建表(优化空间),因为重新选择一遍(转移过程)也是
# 最优二叉搜索树
给二叉搜索树的每个节点定义一个权值,问如何安排使得权值和深度的乘积最小。
考虑当一棵子树成为了一个节点的子树时,答案(期望搜索代价)有何变化?
由于每个节点的深度都增加了 1,这棵子树的期望搜索代价的增加值应为所有概率之和。
tD/eD 动态规划: 状态空间是
的,每一项依赖其他 项。
# 最长连续不下降子序列
我们的目标是求出给定序列的一个最长的连续子序列,满足这个序列中的后一个元素 不小于 前一个元素。
因为是连续的,所以只要与上一个元素进行比较即可。
int a[MAXN];
int dp() {
int now = 1, ans = 1;
for (int i = 2; i <= n; i++) {
if (a[i] >= a[i - 1])
now++;
else
now = 1;
ans = max(now, ans);
}
return ans;
}
2
3
4
5
6
7
8
9
10
11
12
a = [0] * MAXN
def dp():
now, ans = 1, 1
for i in range(2, n + 1):
if a[i] >= a[i + 1]:
now += 1
else:
now = 1
ans = max(now, ans)
return ans
2
3
4
5
6
7
8
9
10
// Make sure to add code blocks to your code group
# 最长不下降子序列
与最长连续不下降子序列不同的是,不需要这个子序列是连续的了。
求最长子序列的方法有两种。
# 最简单的第一种
int a[MAXN], d[MAXN];
int dp() {
d[1] = 1;
int ans = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j < i; j++)
if (a[j] <= a[i]) {
d[i] = max(d[i], d[j] + 1);
ans = max(ans, d[i]);
}
}
return ans;
}
2
3
4
5
6
7
8
9
10
11
12
13
a = [0] * MAXN
d = [0] * MAXN
def dp():
d[1] = 1
ans = 1
for i in range(2, n + 1):
for j in range(1, i):
if a[j] <= a[i]:
d[i] = max(d[i], d[j] + 1)
ans = max(ans, d[i])
return ans
2
3
4
5
6
7
8
9
10
11
// Make sure to add code blocks to your code group
# 稍复杂的第二种
首先,定义
初始化:
现在我们已知最长的不下降子序列长度为 1,那么我们让
考虑进来一个元素
- 元素大于等于
,直接 即可,这个比较好理解。 - 元素小于
,找到 第一个 大于它的元素,插入进去,其他小于它的元素不要。
那么代码如下:
// C++ Version
for (int i = 0; i < n; ++i) scanf("%d", a + i);
memset(dp, 0x1f, sizeof dp);
mx = dp[0];
for (int i = 0; i < n; ++i) {
*std::upper_bound(dp, dp + n, a[i]) = a[i];
}
ans = 0;
while (dp[ans] != mx) ++ans;
2
3
4
5
6
7
8
9
# Python Version
dp = [0x1f1f1f1f] * MAXN
mx = dp[0]
for i in range(0, n):
bisect.insort_left(dp, a[i], 0, len(dp))
ans = 0
while dp[ans] != mx:
ans += 1
2
3
4
5
6
7
8
// Make sure to add code blocks to your code group
# 经典问题(来自习题)
# DAG 中的最长简单路径
# 最长回文子序列
$$ dp[i][i + len] = \begin{cases} dp[i + 1][i + len - 1] + 2, & \text{if}\ s[i] = s[i + len] \[2ex] \max(dp[i + 1][i + len], dp[i][i + len - 1]), & \text{else} \end{cases} $$
边界:
注意:
也可以转化为 LCS 问题,只需要把
注意区分子串(要求连续)的问题。
# 最长回文子串
为了处理方便,我们把原串每两个字符之间加一个(不包含在原串中的)#
,开头加一个 $
。
这样得到的回文串长度就保证是奇数了
考虑如果按顺序得到了
如果之前有一个位置比如说是
如果找不到呢?就先让
之后再暴力延伸一下。
可以证明是
至于如何找是否有这么一个
代码在:https://github.com/Ir1d/Fantasy/blob/master/HDU/3068.cpp (opens new window)
# 双调欧几里得旅行商问题
好像出成了某一年程设期末。
upd:其实是 程设期末推荐练习 (opens new window) 里面的。
书上的提示是:从左到右扫描,对巡游路线的两个部分分别维护可能的最优解。
说的就是把回路给拆开吧。
# 思路一
我们可以人为要求
这样考虑第
如果是分给快的那条:
如果是慢的,原来是慢的那条就变成了快的,所以另一条是到
答案是
代码:https://github.com/Ir1d/Fantasy/blob/master/openjudge/cssx/2018rec/11.cpp (opens new window)
# 思路二
把
改成是
这样的转移更好写:
我们记
边界是:
答案是
# 整齐打印
希望最小化所有行的额外空格数的立方之和。
注意到实际问题要求单词不能打乱顺序,所以就好做了起来。不要把题目看复杂。
不知道这样可不可做:有
# 编辑距离
变换操作有
# 最优对齐问题
把空格符插入到字符串里,使得相似度最大。
定义了按字符比较的相似度。
然后发现最优对齐问题可以转换为编辑距离问题。
相当于仅有三个操作的带权编辑距离。
copy : 1
replace : -1
insert : -2
2
3
# 公司聚会计划
没有上司的舞会。
# 译码算法
Viterbi algorithm (opens new window) 之前写词性标注的时候有用到,好像用在输入法里面也是类似的。
本题中用来实现语音识别,其实就是找一条对应的概率最大的路径。
ref:https://segmentfault.com/a/1190000008720143 (opens new window)
# 基于接缝裁剪的图像压缩
玩过 opencv 的应该有印象,seam carving 就是在做 dp。
题中要求每一行删除一个像,每个像素都有代价,要求总代价最小。
限制:要求相邻两行中删除的像素必须位于同一列或相邻列。
边界:
# 字符串拆分
相当于问怎么按顺序拼起来使得总代价最小。
等价于之前那个最优二叉搜索树。
注意
边界:
就按照区间 dp 的姿势来写就好了。
# 投资策略规划
引理:存在最优投资策略,每年都将所有钱投入到单一投资中。
这是个很有趣的结论,dp 问题中很常见。
https://fogsail.github.io/2017/05/08/20170508/ (opens new window)
剩下的就是个二维 dp,想成从
# 库存规划
生产多了少了都有额外的成本,问怎么安排生产策略使得额外的成本尽可能地少。
https://walkccc.github.io/CLRS/Chap15/Problems/15-11/ (opens new window)
# 签约棒球自由球员
https://walkccc.github.io/CLRS/Chap15/Problems/15-12/ (opens new window)
类似于背包问题。
当选取的状态难以进行递推时(分解出的子问题和原问题形式不一样),考虑将问题状态分类细化,增加维度。