「学习笔记」DP 经典模型

Posted liuzimingc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了「学习笔记」DP 经典模型相关的知识,希望对你有一定的参考价值。

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)\\)

综上,得到状态转移方程

\\[ f_i, j =\\left\\ \\beginaligned & f_i - 1, j - 1 + 1 \\ (A_i = B_j)\\\\ & \\max(f_i - 1, j, f_i,j - 1) \\ \\textelse \\endaligned \\right. \\]

代码如下:

#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 经典模型的主要内容,如果未能解决你的问题,请参考以下文章

读书笔记 - 其他经典动态规划问题

12篇顶会论文,深度学习时间序列预测经典方案汇总

Pytorch学习记录-TextMatching几个经典模型

第三章 动态规划-基于模型的RL

算法动态规划DP自学笔记 入门:基本知识+经典例题

OSI七层模型学习笔记