「学习笔记」DP 经典模型
Posted liuzimingc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了「学习笔记」DP 经典模型相关的知识,希望对你有一定的参考价值。
数字三角形
原题目链接:Link。
首先我们可以发现,贪心是行不通的。见 Hack:
1
10 1
10 1 1
0 0 0 100
假如每次都选可以走的点中最大的,那么走出来的是 \\(1 + 10 + 10 + 0 = 20\\),但实际上,我们可以走 \\(1 + 10 + 1 + 100 = 112\\),这也是最优路径。
所以,我们考虑动态规划(DP)。
逆推
设 \\(f_i, j\\) 表示从第 \\(i\\) 行第 \\(j\\) 列走到最后一行的最大的和。边界根据定义(术语叫做状态),就是 \\(f_n, i = w_n, i\\)(\\(w\\) 是点的权值)。这样,我们只需要从最后一层逆着往上走,推到 \\(f_1, 1\\) 即是答案。
考虑 \\((i, j)\\) 是如何被走到的,发现可以被 \\((i + 1, j)\\) 走到,也可以被 \\((i + 1, j + 1)\\) 走到(这里是逆着走的)。那么我们得到一个柿子(术语叫做状态转移方程,也就是 \\(f_i, j\\) 这个状态是怎么从别的状态转移过来的),\\(f_i, j = \\max(f_i + 1, j, f_i + 1, j + 1) + w_i, j\\)(别忘了加上本身的权值)。
顺推
上面我们介绍了逆推,那么,可不可以顺推呢?
既然要顺推,我们定义 \\(f_i, j\\) 表示从 \\((1, 1)\\) 走到 \\((i, j)\\) 的最大和。边界即 \\(f_1, 1 = w_1, 1\\)。
顺着走,发现 \\((i, j)\\) 可以被 \\((i - 1, j)\\) 和 \\((i - 1, j - 1)\\) 走到。从而有 \\(f_i, j = \\max(f_i - 1, j, f_i - 1, j - 1) + w_i, j\\)。
由于不能保证最后在哪里结束,所以答案为 \\(\\max_1 \\leq i \\leq n\\f_n, i\\\\)(走到第 \\(n\\) 行的所有路径的最大值)。
最长上升子序列
原题目链接:Link。
最长上升子序列即 Longest Increasing Subsequence(LIS)。这里我们介绍简单的 \\(O(n ^ 2)\\) 做法。
设 \\(f_i\\) 表示前 \\(i\\) 个数中以 \\(a_i\\) 结尾(经常这么设计状态)的 LIS 长度。初始 \\(f_i = 1\\)(\\(a_i\\) 自己就是一个上升子序列)。
考虑如何转移。注意到前 \\(i\\) 个数中以 \\(a_i\\) 结尾的 LIS 一定是一个不以 \\(a_i\\) 结尾的 LIS「拼上」\\(a_i\\)。那么我们可以枚举这个不以 \\(a_i\\) 结尾的 LIS 的结尾 \\(j\\)。从而有状态转移方程 \\(f_i = \\max_1 \\leq j < i, a_j < a_i\\f_j + 1\\\\)(这个状态转移方程很重要,请细细体会)。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int n, a[N], f[N], ans;
int main()
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
f[i] = 1;
for (int i = 2; i <= n; i++)
for (int j = 1; j < i; j++)
if (a[j] < a[i] && f[j] + 1 > f[i])
f[i] = f[j] + 1;
for (int i = 1; i <= n; i++)
if (f[i] > f[ans]) ans = i;
cout << f[ans] << endl;
return 0;
那如果我们要输出 LIS 呢?其实也非常简单。我们用一个 \\(pre\\) 数组来记录,表示 \\(i\\) 是从 \\(pre_i\\) 「转移」过来的(\\(pre_i\\) 下一个就是 \\(i\\))。这样,我们只需要在转移的时候,加上 pre[i] = j
即可。
输出时,我们可以使用递归。代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 5005;
int n, a[N], pre[N], f[N], ans;
void print(int x) // print(x) 表示输出以 x 结尾的 LIS
if (pre[x] != x) print(pre[x]); // 递归,先输出 x 前面的所有序列
cout << a[x] << " "; // 再输出它本身
int main()
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
pre[i] = i; // 初始每一个数都是自己转移的,定义为 i
f[i] = 1;
for (int i = 2; i <= n; i++)
for (int j = 1; j < i; j++)
if (a[j] < a[i] && f[j] + 1 > f[i])
f[i] = f[j] + 1;
pre[i] = j;
// f[i] 被 f[j] + 1 更新,则 i 是从 j 转移过来的
for (int i = 1; i <= n; i++)
if (f[i] > f[ans]) ans = i;
cout << f[ans] << endl;
print(ans);
return 0;
这种记录前驱 \\(pre\\) 或者后缀 \\(nxt\\) 的输出方法在 DP、搜索中很常见,请读者一定掌握。
PS:还有 \\(O(n \\log n)\\) 的 LIS 方法,使用二分,可以自行上网搜索。
最长公共子序列
最长公共子序列,即 Longest Common Subsequence(LCS)。
原题目链接:Link。
给出两个长度为 \\(n\\) 和 \\(m\\) 的字符串 \\(A\\) 和 \\(B\\),求即是 \\(A\\) 的子序列又是 \\(B\\) 的子序列的最大长度。
设 \\(f_i, j\\) 表示 \\(A_1 \\sim i\\) 与 \\(B_1 \\sim j\\) 的 LCS 长度。当 \\(A_i = B_j\\),即它们是公共时,我们可以把它们作为一个公共子序列,从而有 \\(f_i, j = f_i - 1, j - 1 + 1\\);否则它们不公共,那么「丢掉」\\(A_i\\) 或者 \\(B_j\\) 对 LCS 是没有影响的,有 \\(f_i, j = \\max(f_i - 1, j, f_i,j - 1)\\)。
综上,得到状态转移方程
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int n, m;
char a[N], b[N];
int dp[N][N];
int main()
scanf("%s", a + 1);
scanf("%s", b + 1);
n = strlen(a + 1);
m = strlen(b + 1); // 从 1 开始避免 -1 越界
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; 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\\n", dp[n][m]);
return 0;
以上是关于「学习笔记」DP 经典模型的主要内容,如果未能解决你的问题,请参考以下文章