动态规划理解
参考知乎什么是动态规划?动态规划的意义是什么?高票回答 https://www.zhihu.com/question/23995189
参考DP各类型题目 http://blog.csdn.net/cc_again/article/details/25866971
0. 概念
动态规划本质上是一种思想,不是一种算法。它通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的思路去解决。
当你企图使用计算机解决一个问题是,其实就是在思考如何将这个问题表达成状态(用哪些变量存储哪些数据)以及如何在状态中转移(怎样根据一些变量计算出另一些变量)。所以所谓的空间复杂度就是为了支持你的计算所必需存储的状态最多有多少,所谓时间复杂度就是从初始状态到达最终状态中间需要多少步!
判断一个问题是否动规
- 每个阶段只有一个状态->递推;
- 每个阶段的最优状态都是由上一个阶段的最优状态得到的->贪心;
- 每个阶段的最优状态是由之前所有阶段的状态的组合得到的->搜索;
- 每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得- 到而不管之前这个状态是如何得到的->动态规划。
1. 常见的状态方程
- 最大连续子序列和
给定序列,求连续的子序列要求和最大,求最大的和为多少。
dp[i]表示以a[i]作为末尾的连续序列的最大和(a[i]必须是末尾被选的数啊啊),dp数组中所有的数据的最大值就是所求。
因为a[i]一定是所选序列的末尾,所以分为两种情况:
- a[i]开始,a[i]结束
- 某数开始,到a[i]结束(最大和是dp[i-1] + a[i])
- 所以递推方程为dp[i] = max(a[i], dp[i-1]+a[i])
int ans = 0;
for(int i = 0; i < n; i++) {
for(int j = 1; j < i; j++) {
if(a[i] >= a[j])
dp[i] = max(1, dp[j] + 1);
}
ans = max(dp[i], ans);
}
printf("%d", ans);
- 最长不下降子序列(LIS)
求一个序列的最长的子序列(可以不连续),使得这个子序列是不下降的
dp[i]表示必须以a[i]结尾的最长不下降子序列的长度。
dp[i] = max{1, dp[j] + 1}; // j从1 ~ i-1 且必须满足a[j] <= a[i]
int ans = -1; //记录最大的 dp[i]
for(int i = 1; i <= n; i++) //按顺序计算出dp[i]的值
{
dp[i] = 1; //边界初始条件(即先假设每个元素自成一个子序列)
for(int j=1;j<i;j++)
{
if(A[i] >= A[j] && (dp[j] + 1 > dp[1]))
dp[i] = dp[j] + 1; //状态转移方程,用以更新dp[i]
}
ans = max(ans, dp[i]);
}
- 最长公共子序列(LCS)
给定两个字符串或者数字序列A和B,求一个字符串,使得这个字符串是A和B的最长公共部分(子序列可以不连续)。
dp[i][j]表示A的第i位之前和B的第i位之前的这两个序列的LCS最长公共子序列的长度(下标从1开始)
那么dp[lena][lenb]即为所求。
递推方程:
- 当a[i] == b[j] : dp[i][j] = dp[i-1][j-1] + 1
- 当a[i] != b[j] : dp[i][j] = max(dp[i-1][j], dp[i][j-1])
- 边界:dp[i][0] = dp[0][j] = 0(0 <= i <= n, 1 <= j <= m)
char a[100], b[100];
scanf("%s", a+1);
scanf("%s", b+1);
int lena = strlen(a + 1);
int lenb = strlen(b + 1);
for(int i = 0; i <= lena; i++)
dp[i][0] = 0;
for(int j = 0; j <= lenb; j++)
dp[0][j] = 0;
for(int i =1; i <= lena; i++) {
for(int j - 1; j <= lenb; j++) {
if(a[i] == b[j])
dp[i][j] = dp[i-1][j-1] + 1;
else
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
- 最长回文子串
给出一个字符串s,求s的最长回文字串的长度
dp[i][j]表示s[i]到s[j]所表示的字串是否是回文字串。只有0和1
递推方程:
- 当s[i] == s[j] : dp[i][j] = dp[i+1][j-1]
- 当s[i] != s[j] : dp[i][j] =0
- 边界:dp[i][j] = 1, dp[i][i+1] = (s[i] == s[i+1]) ? 1 : 0
因为i、j如果从小到大的顺序来枚举的话,无法保证更新dp[i][j]的时候dp[i+1][j-1]已经被计算过。因此不妨考虑按照字串的长度和子串的初试位置进行枚举,即第一遍将长度为3的子串的dp的值全部求出,第二遍通过第一遍结果计算出长度为4的子串的dp的值…这样就可以避免状态无法转移的问题。
int len = s.length();
//先把1和2长度的都初始化了
int ans = 1;
for(int i = 0; i < len; i++) {
dp[i][i] = 1;
if(i < len - 1 && s[i] == s[i+1]) {
dp[i][i+1] = 1;
ans = 2;
}
}
//状态转移方程
for(int L = 3; L <= len; L++) {
for(int i = 0; i + L - 1 < len; i++) {
int j = i + L - 1;
if(s[i] == s[j] && dp[i+1][j-1] == 1) {
dp[i][j] = 1;
ans = L;
}
}
}
printf("%d", ans);
2. 背包九讲
- 01背包问题
有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个重量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品只有1件。
- dp[i][j]表示前i件物品恰好装入容量为j的背包所能获得的最大价值
不放第i件物品,则dp[i][j] = dp[i-1][j]
放第i件物品,那么问题转化为前i – 1件物品恰好装入容量j – w[i]的背包中所能获得的最大价值dp[i-1][j-w[i]] + c[i] - 递推方程dp[i][j] = max{ dp[i-1][j], dp[i-1][j-w[i]]+c[i] }
for(int i = 1; i <= n; i++) {
for(int j = 1, j <= v; j++)
if(j < w[i])
dp[i][j] = dp[i-1][j];
else
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + c[i]);
}
// 空间优化后,投机取巧:
for(int i = 1; i <= n; i++) {
for(int j = v; j >= w[i]; j--)
dp[j] = max(dp[j], dp[j - w[i]] + c[i]);
}
-
若要求"恰好装满背包"时的最优解,dp[0] = 0;其他为-无穷;
-
完全背包问题
有n种物品,每种物品的单件重量为w[i],价值为c[i]。现有一个容量为v的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品有无穷件
基本思路
类似01背包,区别是一个物品可以取0 件、取1 件、取2 件…
- 递推方程
f[i][j]=max(f[i−1][j−k∗w[i]]+k∗v[i])∣0<=k∗w[i]<=j
O(VN)的算法
- 递推方程
f[i][j]=max(f[i−1][j],f[i][j−w[i]]+v[i])
f[i][j]=max(f[i−1][j],f[i][j−w[i]]+v[i])
和01背包不同,这里的j的枚举顺序为正向枚举, 而且是dp[i][j-w[i]]
for(int i =1; i <= n; i++) {
for(int j = w[i]; j <= v; j++)
dp[j] = max(dp[j], dp[j-w[i]]+ c[i]);
}