学习笔记:基础动态规划
线性 DP
定义
具有线性“阶段”划分的动态规划算法被统称为线性动态规划
入门线性动态 DP
LIS 问题
最长上升子序列问题。
问题:给定一个长度为 \(N\) 的数列 \(A\), 求数值单调递增的子序列的长度最长是多少(子序列不需要连续)。
经典的线性动态规划问题。
分析:容易发现,对于某一个位置 \(i\),其所处的最长上升子序列一定是 \(i\) 前面的最后一位小于 \(A_i\) 的最长的上升子序列。
状态:定义 \(f_i\) 表示以 \(A_i\) 为结尾的"最长上升子序列"的长度。
阶段划分:子序列的结尾的位置(从前往后,这明显是线性的)。
状态转移方程:$$f_i = max_{0 \leq j \lt i, A_j \lt A_i}(f_j + 1)$$
初始化:\(f_0 = 0\)。
答案:\(max_{1 \leq i \leq N}(f_i)\)。
时间复杂度:\(O(N^2)\)。
LIS 问题有 \(O(N \log N)\) 的做法,不过与 dp 关系不大。
点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 1e5 + 5;
int N, A[MAXN], f[MAXN], ans;
int main() {
scanf("%d", &N);
for (int i = 1; i <= N; i++) scanf("%d", &A[i]);
for (int i = 1; i <= N; i++)
for (int j = 0; j < i; j++)
if (A[i] > A[j]) f[i] = max(f[i], f[j] + 1);
for (int i = 1; i <= N; i++) ans = max(ans, f[i]);
printf("%d", ans);
return 0;
}
LCS 问题
最长公共子序列问题。
同样为简单的线性 dp 问题。
问题:给定一个长度为 \(N\) 的序列 \(A\) 和一个长度为 \(M\) 的序列 \(B\),问它们的最长公共子序列长为多少。(子序列同样不需要连续)
分析:容易发现,\(i, j\) 位置的最长公共子序列一定由 \(i, j\) 的前缀的最长公共子序列转移而来。
于是想到定义状态:\(f_{i, j}\) 表示 \(A\) 序列的前 \(i\) 项与 \(B\) 序列的前 \(j\) 项的最长公共子序列。
考虑转移:首先,令 \(f_{i, j} \leftarrow\max(f_{i -1, j}, f_{i, j-1})\)。
而若 \(A_i = B_j\) ,则让 \(f_{i, j} \leftarrow\max(f_{i, j}, f_{i - 1, j - 1} + 1)\) 即可。
综上,状态转移方程式为:
阶段划分:已经处理的前缀的位置。
初始化:\(f_{i, 0} = 0, f_{0, j} = 0\ (0\leq i \leq N, 0\leq j \leq M)\)。
答案:\(f_{N, M}\)。
时间复杂度:\(O(NM)\)。
点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 1e4 + 5, M = 1e4 + 5;
char A[N], B[M];
int f[N][M];
int n, m;
int main() {
scanf("%d%d%s%s", &n, &m, A + 1, B + 1);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
if (A[i] == B[j]) f[i][j] = max(f[i - 1][j - 1] + 1, f[i][j]);
}
printf("%d", f[n][m]);
return 0;
}
数字金字塔
问题:给定一个共有 \(N\) 行的三角矩阵 \(A\),其中第 \(i\) 行有 \(i\) 列。从左上角出发,每次可以向下方或右下方走一步,最终到达底部,求把经过的所有位置上的数加起来,和最大是多少?
状态:设 \(f_{i, j}\) 表示走到第 \(i\) 行第 \(j\) 列的时候,和最大为多少。
分析:一种用途广泛的 dp 技巧,求某个状态能拓展到那些状态不如求一个状态能被那些状态拓展而来。
因为每次可以向下方或右下方走一步,所以一个点可以由其左上角或上方的点拓展而来。
那么容易得状态转移方程为:
阶段:路径的结尾位置。
初始化:\(f_{0, 0} = f_{0, 1} = 0\)。
答案:\(\max_{i = 1}^nf_{n, i}\)
点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 1e3 + 5;
int n, A[N][N], f[N][N], ans;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++) scanf("%d", &A[i][j]);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
f[i][j] = A[i][j] + max(f[i - 1][j], f[i - 1][j - 1]);
for (int i = 1; i <= n; i++) ans = max(f[n][i], ans);
printf("%d", ans);
return 0;
}
线性动态规划例题
P2679 [NOIP2015 提高组] 子串
分析
容易想到设计状态:\(f_{i, j, k}\) 前表示从 \(A\) 前 \(i\) 项选取 \(k\) 个字符串按原顺序拼接,与 \(B\) 前 \(j\) 项相同的方案数。
考虑转移,如果当前考虑 \(A\) 中前 \(i\) 个字符,\(B\) 中前 \(j\) 个字符,用了 \(k\) 个字符串的状态:
-
若 \(A_i \ne B_j\),则 \(f_{i, j, k} = f_{i - 1, j, k}\)。
-
若 \(A_i = B_i\),再考虑两种状况:
-
不用 \(A_i\),则 \(f_{i, j, k} = f_{i - 1, j, k}\)。
-
用 \(A_i\),此时会发现,\(A_i\) 可能会归于前面的选出字符串,也可以不归于。
具体来说,考虑 \(x, \forall d \in [1, x], A_{i - d} = B_{j - d}\),且 \(A_{i - x - 1} \ne B_{j - x - 1}\),\(f_{i, j, k}\) 可以由 \(f_{i - d, j - d, k - 1}\) 转移而来。
-
综上可得:
暴力转移的话可能得到一个 \(O(NM^2K)\) 的算法,会爆。
但显然能用前缀和优化。
定义 \(sum_{i, j, k}\) 表示 \(\sum_{d = 1}^{x}f_{i - d, j - d, k - 1}\)。
考虑转移 \(sum\)。
-
如果 \(A_i \ne B_j\),那么 \(sum_{i, j, k} = 0\)。
-
否则,\(sum_{i, j, k} = sum_{i - 1, j - 1, k} + f_{i - 1, j - 1, k - 1}\)
那么就容易转化了
点击查看代码
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
for (int l = 1; l <= m; l++) {
f[i][j][l] = (f[i - 1][j][k] + (sum[i][j][k] = a[i] == b[i] ? sum[i - 1][j - 1][k] + f[i - 1][j - 1][k - 1] : 0) % mod) % mod
}
}
}
但是因为空间过大,所以需要去掉一维。
发现仅与 \(i - 1\) 有关,那么可以去掉 \(i\) 这一维(也可去掉 \(k\))。
点击查看代码
/*
--------------------------------
| code by FRZ_29 |
| code time |
| 2024/09/06 |
| 20:38:50 |
| 星期五 |
--------------------------------
*/
#include <iostream>
#include <climits>
#include <cstdio>
#include <ctime>
using namespace std;
void RD() {}
template<typename T, typename... U> void RD(T &x, U&... arg) {
x = 0; int f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
x *= f; RD(arg...);
}
const int N = 1005;
const int M = 205;
const int mod = 1e9 + 7;
#define PRINT(x) cout << #x << " = " << x << "\n"
#define LF(i, __l, __r) for (int i = __l; i <= __r; i++)
#define RF(i, __r, __l) for (int i = __r; i >= __l; i--)
char a[N], b[M];
int n, m, k;
int f[M][M] = {1}, sum[M][M];
int main() {
// freopen("read.in", "r", stdin);
// freopen("out.out", "w", stdout);
// time_t st = clock();
RD(n, m, k);
scanf("%s%s", a + 1, b + 1);
LF(i, 1, n) RF(j, m, 1) RF(l, k, 1)
f[j][l] = (f[j][l] + (sum[j][l] = (a[i] == b[j] ? sum[j - 1][l] + f[j - 1][l - 1] : 0) % mod)) % mod;
printf("%d", f[m][k]);
// printf("\n%dms", clock() - st)
return 0;
}
/* ps:FRZ弱爆了 */
有约不来过夜半,闲敲棋子落灯花
P1544 三倍经验
分析
简单题。
容易想到设计状态 \(f_{i, j, k}\) 表示到 \((i, j)\) 用了 \(k\) 次机会的最大值。
考虑转移(自下而上转移)。
- 如果不用一次机会,当前状态 \(f_{i, j, k}\) 可由 \(f_{i + 1, j, k}, f_{i + 1, j + 1, k}\) 转移而来,即:
- 如果使用一次机会,当前状态 \(f_{i, j, k}\) 可由 \(f_{i + 1, j, k - 1}, f_{i + 1, j + 1, k - 1}\) 转移, 即:
综上:
注意 \(a_{i, j}\) 可能为负,所以应把 \(f\) 初始化为负无穷。
然后暴力转移即可。
点击查看代码
/*
--------------------------------
| code by FRZ_29 |
| code time |
| 2024/09/08 |
| 10:05:50 |
| 星期天 |
--------------------------------
*/
#include <iostream>
#include <climits>
#include <cstdio>
#include <ctime>
typedef long long LL;
using namespace std;
void RD() {}
template<typename T, typename... U> void RD(T &x, U&... arg) {
x = 0; int f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
x *= f; RD(arg...);
}
const int N = 105;
const LL INF = -1e18;
#define PRINT(x) cout << #x << "=" << x << "\n"
#define LF(i, __l, __r) for (int i = __l; i <= __r; i++)
#define RF(i, __r, __l) for (int i = __r; i >= __l; i--)
LL a[N][N], f[N][N][N * (N + 1) / 2], ans = INF;
int n, k;
int main() {
// freopen("read.in", "r", stdin);
// freopen("out.out", "w", stdout);
// time_t st = clock();
RD(n, k);
LF(i, 1, n) LF(j, 1, i) RD(a[i][j]);
LF(i, 1, n) LF(j, 1, i) LF(l, 0, k) f[i][j][l] = INF;
LF(i, 1, n) f[n][i][0] = a[n][i], f[n][i][1] = a[n][i] * 3;
RF(i, n - 1, 1) LF(j, 1, i) LF(l, 0, k) {
f[i][j][l] = max(f[i + 1][j][l], f[i + 1][j + 1][l]) + a[i][j];
if (l >= 1) f[i][j][l] = max(f[i][j][l], max(f[i + 1][j][l - 1], f[i + 1][j + 1][l - 1]) + a[i][j] * 3);
}
LF(i, 0, k) ans = max(ans, f[1][1][i]);
printf("%lld", ans);
// printf("\n%dms", clock() - st);
return 0;
}
/* ps:FRZ弱爆了 */
青青子衿,悠悠我心。
纵我不往,子宁不嗣音?
青青子佩,悠悠我思。
纵我不往,子宁不来?
挑兮达兮,在城阙兮。
一日不见,如三月兮。
P1004 [NOIP2000 提高组] 方格取数
分析
经典题,利用网格的特殊性质。
因为题目要求走两次,不妨假设两次同时出发,每次同时移动。
设第一条路径的点当前位于 \((i, j)\),第二条路径的点当前位于 \((k, m)\),易得 \(i + j = k + m\)。
设状态为 \(f_{i, j, k, m}\) 表示第一条路径上点最后位于 \((i, j)\),第二条路径上点当前位于 \((k, m)\),这时,若 \(f_{i, j, k, m}\) 可由 \(f_{i - 1, j, k - 1, m}, f_{i - 1, j, k, m - 1}, f_{i, j - 1, k - 1, m}, f_{i, j - 1, k, m - 1}\) 转移。
此时如果 \(i = k\),说明两条路径经过同一个点,只加一遍。
即可解决问题。
也可以设 \(s = i + j = k + m\),则状态可以简化为 \(f_{s, i, k}\)。
容易得转移方程式:
点击查看代码
/*
--------------------------------
| code by FRZ_29 |
| code time |
| 2024/09/08 |
| 12:52:51 |
| 星期天 |
--------------------------------
*/
#include <iostream>
#include <climits>
#include <cstdio>
#include <ctime>
using namespace std;
void RD() {}
template<typename T, typename... U> void RD(T &x, U&... arg) {
x = 0; int f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
x *= f; RD(arg...);
}
#define PRINT(x) cout << #x << " = " << x << "\n"
#define LF(i, __l, __r) for (int i = __l; i <= __r; i++)
#define RF(i, __r, __l) for (int i = __r; i >= __l; i--)
int a[10][10], n, f[20][10][10];
int main() {
// freopen("read.in", "r", stdin);
// freopen("out.out", "w", stdout);
// time_t st = clock();
RD(n);
int x, y, val;
while (RD(x, y, val), x, y, val) a[x][y] = val;
LF(s, 2, n * 2) LF(i, max(1, s - n), min(s - 1, n)) LF(k, max(1, s - n), min(s - 1, n)) {
f[s][i][k] = f[s - 1][i][k];
f[s][i][k] = max(f[s - 1][i - 1][k], f[s][i][k]);
f[s][i][k] = max(f[s - 1][i][k - 1], f[s][i][k]);
f[s][i][k] = max(f[s - 1][i - 1][k - 1], f[s][i][k]);
if (i == k) f[s][i][k] += a[i][s - i];
else f[s][i][k] += a[i][s - i] + a[k][s - k];
}
printf("%d", f[2 * n][n][n]);
// printf("\n%dms", clock() - st)
return 0;
}
/* ps:FRZ弱爆了 */
人生如逆旅 我亦是行人
参考资料
《算法竞赛进阶指南》 李煜东