动态规划:
- 将一个复杂的问题分解成若干个子问题,通过综合子问题的最优解来得到原问题的最优解
- 动态规划会将每个求解过的子问题的解记录下来,这样下一次碰到同样的子问题时,就可以直接使用之前记录的结果,而不是重复计算
- 可以用递归或者递推的写法实现,递归的写法又叫记忆化搜索
- 重叠子问题:如果一个问题可以被分解成若干个子问题,且这些子问题会重复出现,就称这个问题拥有重叠子问题。 一个问题必须拥有重叠子问题,才能用动态规划去解决。
- 最优子结构:如果一个问题的最优解可以由其子问题的最优解有效地构造出来,那么称为这个问题拥有的最有子结构。最优子结构保证了动态规划中的原问题的最优解可以由子问题的最优解推导而来
- 动态规划与分治的区别:都是分解为子问题然后合并子问题得到解,但是分治分解出的子问题是不重叠的
- 动态规划与贪心的区别:都有最优子结构,但是贪心直接选择一个子问题去求解,会抛弃一些子问题,这种选择的正确性需要用归纳法证明,而动态规划会考虑所有的子问题
- DP:考虑且仅仅考虑由前一阶段状态转移到当前状态后,递推并选取出当前状态的最优解,具有无后效性和最优子结构的基本特征,其中所谓的无后效性是指:“下一时刻的状态只与当前状态有关,而和当前状态之前的状态无关,当前的状态是对以往决策的总结”。因此DP是由前一阶段的一系列阶段转移并选取最优而来,即抵达当前状态路径不唯一,仅是最终最优结果唯一。
- 贪心:对于尚未加入解集的元素按固定策略(最大或最小)选取状态转移,妄图从局部最优达到整体最优,也具有最优子结构性质,但转移路径单一,一旦确定贪心策略后就是一条路走到底,没有多余的状态供给选择。显然,最终结果也必定唯一,具有后效性,即当前最优状态与之前的选取路径中全部节点都有关。
- 相对DP来讲,贪心算法效率要高,但是有些问题用贪心往往无法求解,因为一些限制(如0-1背包中的背包不一定要装满)贪心无法在满足的条件下且同时达到最优,这样一来就只能使用DP枚举全部可转移的状态,从而递推出最优解。个人觉得,若限制条件较多,应该偏向于DP,若限制条件较少(一般为一个)则可以考虑贪心算法。
动态规划的递归和递推写法
- 递归写法
//不使用动态规划
int F(int n) {
if(n == 0 || n == 1) return 1;
else return F(n - 1) + F(n - 2);
}
// 此时F(5) = F(4) + F(3), F(4) = F(3) + F(2),3会被计算两次
// 采用动态规划的方法(记忆化搜索)
int dp[10000];
memeset(dp, -1, sizeof(dp));
int F(int n) {
if(n == 0 || n == 1) return 1;
if(dp[n] != -1) return dp[n];
else {
dp[n] = F(n-1) + F(n - 2);
return dp[n];
}
}
- 递推写法
// 数塔为例
// 递推方程:f[i][j] += max(f[i+1][j], f[i+1][j+1])
// 如果非要建立dp数组,先要初始化dp[n][j] = f[n][j] [(j从1~n)]
// 然后dp[i][j] = max(dp[i+1][j], dp[i+1][j+1]) + f[i][j]
// f[i][j]为第i行j列数字的大小
// 采用自底向上递推的方法
// 数组从1开始作为下标存储
for(int i = n - 1; i >= 1; i--) {
for(int j = 1; j <= i; j++)
f[i][j] += max(f[i+1][j], f[i+1][j+1]);
}
return f[1][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])
// a数组从下标0开始
dp[0] = a[0];
for(int i = 1; i < n; i++)
dp[i] = max(a[i], dp[i-1]+a[i]);
int maxn = dp[0];
for(int i = 1; i < n; i++)
maxn = max(dp[i], maxn);
printf("%d", maxn);
最长不下降子序列(LIS)
- 求一个序列的最长的子序列(可以不连续),使得这个子序列是不下降的
- dp[i]表示必须以a[i]结尾的最长不下降子序列的长度
- dp[i] = max{1, dp[j] + 1}; // j从1 ~ i-1 且必须满足a[j] <= 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);
最长公共子序列(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)
- 当a[i] == b[j] :
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]);
}
}
printf("%d", dp[lena][lenb]);
最长回文子串
- 给出一个字符串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
- 当s[i] == s[j] :
- 因为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);
DAG最长路
背包问题
- 多阶段动态规划问题:有一类动态规划可解的问题,它可以描述成若干个有序的阶段,且每个阶段的状态有关,一般把这类问题称为多阶段动态规划问题
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]
- 不放第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[v] = max(dp[v], dp[v[w[i]]] + c[i]);
}
完全背包问题
- 有n种物品,每种物品的单件重量为w[i],价值为c[i]。现有一个容量为v的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品有无穷件
- 递推方程:
dp[i][j] = max(dp[i-1][v], dp[i][j-w[i]] + c[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[i], dp[j-w[j]]+ c[i]);
}