动态规划
DP 问题的一般思路
DP 定义 ——有时 DP 的更新很难严格遵循定义,需要额外变量保存全局最优结果
初始化 ——初始值可以通过一个简单的特例来确定
递推公式 + 边界条件
DP 优化 (可选)
Reference
常见的动态规划问题分析与求解 - 五岳 - 博客园
什么是动态规划?动态规划的意义是什么? - 知乎
Index
背包问题
【注】关于“恰好装满”
如果要求恰好装满背包,可以在初始化时将
dp[0] / dp[i][0]初始化0,其他初始化为-INF。这样即可保证最终得到的dp[N] / dp[N][M]是一种恰好装满背包的解;如果不要求恰好装满,则全部初始化为
0即可。可以这样理解:初始化的 dp 数组实际上就是在没有任何物品可以放入背包时的合法状态。
如果要求背包恰好装满,那么此时只有容量为 0 的背包可能被价值为 0 的物品“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是
-INF。如果背包并非必须被装满,那么任何容量的背包都有一个合法解,即“什么都不装”,这个解的价值为0,所以初始时状态的值也全部为 0 。
01 背包
问题描述
二维 DP(无优化)
定义:
dp[i][j] := 从前 i 个物品中选取总重量不超过 j 的物品时总价值的最大值i从 1 开始计,包括第i个物品初始化
状态转移
```C++ // HDOJ 地址:http://acm.hdu.edu.cn/showproblem.php?pid=2602 int solve(int N, int V, vector& v, vector& w) {
vector > dp(N + 1, vector(V + 1, 0)); // 不要求装满,初始化为 0 即可
// 核心代码 for (int i = 1; i <= N; i++) { for (int j = 0; j <= V; j++) { // 可能存在重量为 0,但有价值的物品 if (w[i] > j) // 如果当前物品的重量大于剩余容量 dp[i][j] = dp[i - 1][j]; else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]); } } return dp[N][V]; }
int main() { int T; // 用例数 scanf("%d", &T); while (T--) { int N, V; // N: 物品数量;V: 背包容量 scanf("%d%d", &N, &V); vector v(N + 1, 0); // 保存每个物品的价值 vector w(N + 1, 0); // 保存每个物品的重量 for (int i = 1; i <= N; i++) scanf("%d", &v[i]); for (int i = 1; i <= N; i++) scanf("%d", &w[i]);
}
一维 DP
定义:
dp[j] := 重量不超过 j 公斤的最大价值递推公式
完全背包
问题描述
二维 DP(无优化)
直观思路:在 01 背包的基础上在加一层循环
递推关系:
关于
k的循环最坏可能从 0 到V,因此时间复杂度为O(N*V^2)
注意到:
完整代码
注意,这里要求的是恰好装满时的情况,所以需要将
dp[i][0]全部初始化为 0,其他初始化为-INF以下代码因超内存无法通过 NYOJ 311;
可以 AC 的代码,请参考 完全背包(一维 DP) 和 完全背包(滚动数组)
```C++ // NYOJ 311 会报超内存,所以无法测试
includeincludeincludeusing namespace std;
const int inf = 0x80000000;
void solve() { int T; scanf("%d", &T); while (T--) { int N, V; // N 表示物品种类的数目,V 表示背包的总容量 scanf("%d%d", &N, &V); vector w(N + 1), v(N + 1); // w 表示重量,v 表示价值 for (int i = 1; i <= N; i++) scanf("%d%d", &w[i], &v[i]);
}
int main() { solve(); return 0; }
```
二维 DP(滚动数组)
一维 DP
核心代码与 01 背包一致,只有第二层循环的递推方向不同
完整代码
多重背包 TODO
硬币问题
硬币找零
LeetCode - 322. 零钱兑换
问题描述
思路
定义:
dp[i] := 组成总金额 i 时的最少硬币数初始化:
状态转移
C++
硬币组合
LeetCode - 518. 零钱兑换 II
C++
最长公共子序列(LCS)
最长公共子序列_牛客网
求两个序列的最长公共字序列
示例:s1: "BDCABA" 与 s2:"ABCBDAB" 的一个最长公共字序列为 "BCBA"
最长公共子序列不唯一,但是它们的长度是一致的
子序列不要求连续
思路
DP 定义
记
s[0:i] := s 长度为 i 的**前缀**定义
dp[i][j] := s1[0:i] 和 s2[0:j] 最长公共子序列的长度
DP 初始化
DP 更新
当
s1[i] == s2[j]时当
s1[i] != s2[j]时
完整递推公式
Code - C++
最长公共子串
最长公共子串_牛客网
题目描述
思路 - 暴力求解
Longest common substring problem - Wikipedia
暴力求解思路:每当找到一对元素相同时就斜向比较
注意:如果两个串完全相同的话,时间复杂度将退化为
O(N^3)
思路 - DP
DP 定义
记
s[0:i] := s 长度为 i 的**前缀**定义dp[i][j] := s1[0:i] 和 s2[0:j] 最长公共子串的长度dp[i][j]只有当s1[i] == s2[j]的情况下才是s1[0:i] 和 s2[0:j] 最长公共子串的长度
DP 初始化
DP 更新
Code
DP 优化:空间复杂度
O(N)好不容易找到的优化为
O(N)的代码;多数优化直接优化到了O(1)因为内层循环是逆序的,所以有点不好理解,可以画一个矩阵手推 DP 的更新过程,很巧妙
DP 优化:空间复杂度
O(1)两个字符串的比较总是按一行一行或一列一列来比较,因此至少要保存一行的数据
而如果是按照斜向遍历,其实只要保存一个数据即可
斜向遍历的策略很多,下面的代码是从右上角(
row=0, col=m-1)开始遍历上述代码其实就是把下面的两段循环合并了
最长递增子序列(LIS)
最长递增子序列_牛客网
最长上升子序列 - LeetCode
牛客假设给定的数组中不存在重复元素,LeetCode 可能存在重复元素
问题描述
思路0 - O(N^2)
LIS 可以转化成 LCS (最长公共子序列) 问题
用另一个序列保存给定序列的排序结果 -
O(NlogN)则问题转化为求这两个序列的 LCS 问题 -
O(N^2)
思路1 - O(N^2)解法
DP 定义
记
nums[0:i] := 序列 nums 的前 i 个元素构成的子序列定义
dp[i] := nums[0:i] 中 LIS 的长度实际并没有严格按照这个定义,中间使用一个变量记录当前全局最长的 LIS
DP 初始化
DP 更新 -
O(N^2)的解法如果只看这个递推公式,很可能会写出如下的错误代码
错误代码(点击展开) ```C++ // 牛客网 class AscentSequence { public: int findLongest(vector nums, int n) { vector dp(n, 1); for (int i = 1; i nums[j]) dp[i] = max(dp[i], dp[j] + 1); else dp[i] = max(dp[i], dp[j]); } return dp[n-1]; } }; ``` - 这段代码的问题在于 `dp[i]` 应该等于 `max{dp[j]}` 对应的那个 `dp[j]+1`,且**只增加一次** - 这么写可能会导致 `dp[i]` 被增加多次 > [动态规划求解最长递增子序列的长度 - hapjin](https://www.cnblogs.com/hapjin/p/5597658.html) - 博客园
下面是网上比较流行的一种递推公式
注意:此时并没有严格按照定义处理 dp,它只记录了当
nums[i] > nums[j] && dp[i] < dp[j] + 1时的 LIS;不满足该条件的情况跳过了;所以需要额外一个变量记录当前已知全局的 LIS
Code
思路2 - O(NlogN)
该解法的思想是:长度为
i的 LIS 的尾元素应该大于长度为i-1的尾元素DP 定义
定义
dp[i] := 长度为 i 的 LIS 的最小尾元素
DP 更新
二分查找 nums[j] 在 dp 中的
upper_bound 位置lower_bound 位置upper_bound 位置指的是序列中第一个大于 nums[j] 的元素所在的位置
lower_bound 位置指的是序列中第一个大于等于 nums[j] 的元素所在的位置
C++ 中分别实现了 upper_bound 和 lower_bound,定义在
<algorithm>中如果在末尾,则插入;反之则替换
upper_bound 只能用于不存在重复元素的情况;而 lower_bound 可以兼容两种情况
Code
最长回文子序列
最长回文子序列 - LeetCode
问题描述
思路
相比最长回文子串,最长回文子序列更像最长公共子序列,只是改变了循环方向
DP 定义
记
s[i:j] := 字符串 s 在区间 [i:j] 上的子串定义
dp[i][j] := s[i:j] 上回文序列的长度
DP 初始化
DP 更新
Code
最长回文子串
最长回文子串_牛客网
最长回文子串 - LeetCode
牛客网只需要输出长度;LeetCode 还需要输出一个具体的回文串
问题描述
思路 - O(N^2)
DP 定义
记
s[i:j] := 字符串 s 在区间 [i:j] 上的子串定义
dp[i][j] := s[i:j] 是否是一个回文串
DP 初始化
DP 更新
Code
Manacher 算法 - O(N)
算法-最长回文子串(Manacher算法) - 琼珶和予 - 博客园
最大连续子序列和
最大连续子序列_牛客网
牛客网要求同时输出最大子序列的首尾元素
思路 - 基本问题:只输出最大连续子序列和
DP 定义
记
a[0:i] := 序列 a 在区间 [0:i] 上的子序列定义
dp[i] := a[0:i] 上的最大子序列和实际并没有严格按照上面的定义,中间使用一个变量记录当前全局的最大连续子序列和
DP 初始化
DP 更新
直观实现-无优化-空间复杂度`O(N)`(点击展开) ```C++ void foo() { int n; while (cin >> n) { vector a(n); for (int i = 0; i> a[i]; vector dp(n); dp[0] = a[0]; int ret = a[0]; for (int i = 1; i 0) dp[i] = dp[i - 1] + a[i]; else dp[i] = a[i]; ret = max(ret, dp[i]); } cout<< ret << endl; } }/* 输入 5 1 5 -3 2 4 6 1 -2 3 4 -10 6 4 -3 -1 -2 -5 输出 9 7 -1 */```
DP 优化
注意到每次递归实际只用到了
dp[i-1],实际只要用到一个变量,空间复杂度O(1)
思路 - 输出区间/首尾
增加两个变量即可
注意:题目要求,如果序列中全是负数,则输出 0,以及整个序列的首尾元素
编辑距离
LeetCode-编辑距离
问题描述
注意:编辑距离指的是将 word1 转换成 word2
思路
用一个 dp 数组维护两个字符串的前缀编辑距离
DP 定义
记
word[0:i] := word 长度为 i 的**前缀子串**定义
dp[i][j] := 将 word1[0:i] 转换为 word2[0:j] 的操作数
初始化
递推公式
word1[i] == word1[j]时word1[i] != word1[j]时,有三种更新方式,取最小
C++
DP 优化
注意到每次更新
dp[i][j]只需要用到dp[i - 1][j - 1], dp[i][j - 1], dp[i - 1][j]。因此实际上不需要用到二维 DP具体见下方代码
Code - 优化为一维 DP(点击展开) ```C++ class Solution { public: int minDistance(string word1, string word2) { int m = word1.length(), n = word2.length(); vector cur(m + 1, 0); for (int i = 1; i
矩阵中的最大正方形
LeetCode-221. 最大正方形
问题描述
思路
DP 定义:
dp[i][j] := 以 M[i][j] 为正方形**右下角**所能找到的最大正方形的边长注意保存的是边长
因为
dp保存的不是全局最大值,所以需要用一个额外变量更新结果
初始化
递推公式
注意到,本题的递推公式与 编辑距离 完全一致
C++
鹰蛋问题
Power Eggs http://acm.zcmu.edu.cn/JudgeOnline/problem.php?id=1894
问题描述
分析
如果只有
M=1个蛋,那么只能从第一层开始一层一层往上尝试,最坏情况下的最少次数为N如果蛋的数量足够多,那么问题转变为二分查找,最坏情况下的最少次数为
logN上取整
思路
DP 定义:
dp[i][j] := i 个蛋比较 j 次所能确定的最高楼层DP 初始化
DP 更新
说明:TODO(不理解是如何得到这个递推式的)
C++
Reference
从《鹰蛋》一题浅析对动态规划算法的优化_百度文库
矩阵链乘法 TODO
有代价的最短路径 TODO
瓷砖覆盖(状态压缩DP) TODO
工作量划分 TODO
三路取苹果 TODO
最后更新于
这有帮助吗?