动态规划基础
动态规划
方案数问题
例题:P1002 [NOIP2002 普及组] 过河卒
参考代码
#include <cstdio>
typedef long long LL;
const int N = 25;
int dx[8] = {-2, -2, -1, -1, 1, 1, 2, 2};
int dy[8] = {-1, 1, -2, 2, -2, 2, -1, 1};
bool control[N][N];
LL dp[N][N];
int main()
{
int n, m, x, y;
scanf("%d%d%d%d", &n, &m, &x, &y);
for (int i = 0; i < 8; i++) {
int xx = x + dx[i], yy = y + dy[i];
if (xx >= 0 && xx <= n && yy >= 0 && yy <= m) control[xx][yy] = true;
}
control[x][y] = true;
for (int i = 0; i <= m; i++) {
if (control[0][i]) break;
dp[0][i] = 1;
}
for (int i = 0; i <= n; i++) {
if (control[i][0]) break;
dp[i][0] = 1;
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if (control[i][j]) dp[i][j] = 0;
else dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
printf("%lld\n", dp[n][m]);
return 0;
}
例题:P1057 [NOIP2008 普及组] 传球游戏
当想清楚状态转移方程并使用递推方式去实现时,通常有两种写法:
- 填表法:枚举未知量,由之前的已知量计算出当前要求的未知量,就好像在填表格的一个一个空格一样。
- 刷表法:枚举已知量,并且根据这个已知量去更新依赖于它的未知量状态,这个未知量可以是前面的,也可以是后面的。
参考代码(填表法)
#include <cstdio>
const int N = 35;
int dp[N][N];
int main()
{
int n, m;
scanf("%d%d", &n, &m);
dp[0][1] = 1;
for (int i = 1; i <= m; i++) {
dp[i][1] = dp[i - 1][n] + dp[i - 1][2];
dp[i][n] = dp[i - 1][n - 1] + dp[i - 1][1];
for (int j = 2; j < n; j++)
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j + 1];
}
printf("%d\n", dp[m][1]);
return 0;
}
参考代码(刷表法)
#include <cstdio>
const int N = 35;
int dp[N][N];
int main()
{
int n, m;
scanf("%d%d", &n, &m);
dp[0][1] = 1;
for (int i = 0; i < m; i++) {
for (int j = 1; j <= n; j++) {
int l = (j == 1 ? n : j - 1);
dp[i + 1][l] += dp[i][j];
int r = (j == n ? 1 : j + 1);
dp[i + 1][r] += dp[i][j];
}
}
printf("%d\n", dp[m][1]);
return 0;
}
习题:P1077 [NOIP2012 普及组] 摆花
解题思路
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 105;
const int MOD = 1000007;
int a[N], dp[N][N], sum[N];
int getsum(int l, int r) {
return l > 0 ? (sum[r] + MOD - sum[l - 1]) % MOD : sum[r];
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 0; i <= m; i++) sum[i] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++)
dp[i][j] = getsum(max(0, j - a[i]), j);
sum[0] = dp[i][0];
for (int j = 1; j <= m; j++) sum[j] = (sum[j - 1] + dp[i][j]) % MOD;
}
printf("%d\n", dp[n][m]);
return 0;
}
最优解问题
例题:P1216 [USACO1.5] [IOI1994]数字三角形 Number Triangles
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1005;
int a[N][N], dp[N][N];
int main()
{
int r;
scanf("%d", &r);
for (int i = 1; i <= r; i++)
for (int j = 1; j <= i; j++)
scanf("%d", &a[i][j]);
dp[1][1] = a[1][1];
for (int i = 2; i <= r; i++) {
dp[i][1] = dp[i - 1][1] + a[i][1];
dp[i][i] = dp[i - 1][i - 1] + a[i][i];
for (int j = 2; j < i; j++)
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1]) + a[i][j];
}
int ans = 0;
for (int i = 1; i <= r; i++) ans = max(ans, dp[r][i]);
printf("%d\n", ans);
return 0;
}
例题:P1113 杂务
为了让每个任务尽早完成,应该让其在所有准备工作完成后立马开始,也就是跟在所有准备工作中最晚结束的那个后面。如果设 \(dp_i\) 表示任务 \(i\) 的最早完成时间,则 \(dp_i = \max\{dp_{pre}\} + len_i\),这里的 \(pre\) 是每一个准备工作,注意题目描述“杂务 \(k\) 的准备工作只可能在杂务 \(1\) 至 \(k-1\) 中”,因此每一个 \(dp_j\) 一定是在 \(dp_i\) 计算时已经确定的结果。
参考代码
#include <cstdio>
#include <algorithm>
using std::max;
const int N = 1e4 + 5;
int dp[N];
int main()
{
int n; scanf("%d", &n);
int ans = 0;
for (int i = 1; i <= n; i++) {
int id, len; scanf("%d%d", &id, &len);
while (true) {
int pre; scanf("%d", &pre); // 读入准备工作
if (pre == 0) break;
dp[i] = max(dp[i], dp[pre]);
}
dp[i] += len;
ans = max(ans, dp[i]); // 完成所有杂务的最短时间是每个任务完成时间中的最大值
}
printf("%d\n", ans);
return 0;
}
例题:P2196 [NOIP1996 提高组] 挖地雷
设 \(dp_i\) 表示从某处开始挖地雷一直挖到第 \(i\) 个地窖能挖到的最多的地雷,则类似上一题,有 \(dp_i = \max \{ dp_{pre} \} + a_i\),这里的 \(a_i\) 指的是第 \(i\) 个地窖处的地雷数,\(pre\) 指的是所有能够连接到地窖 \(i\) 的地窖。由于输入的格式保证了一定是编号小的地窖连接到编号大的地窖,因此计算顺序可以是 \(1 \rightarrow n\)。并且输入的方式是给定每个地窖可以连向后面的某些地窖,因此可以考虑写成刷表法的形式。
注意,本题还要输出挖地雷的顺序,前面的计算过程并没有考虑最优方案对应的路径。
实际上,这只需要在更新 \(dp_i\) 时顺便记录一下转移来源即可(\(dp_i\) 的最大值是在哪个 \(pre\) 那里取到的)。
参考代码
#include <cstdio>
const int N = 25;
int a[N], dp[N], from[N], path[N];
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
int ans = 0, pos = 0;
for (int i = 1; i <= n - 1; i++) {
dp[i] += a[i];
if (dp[i] > ans) {
ans = dp[i]; pos = i;
}
for (int j = i + 1; j <= n; j++) {
int x; scanf("%d", &x);
if (x == 1) {
// i -> j
if (dp[i] > dp[j]) {
dp[j] = dp[i]; from[j] = i;
}
}
}
}
dp[n] += a[n];
if (dp[n] > ans) {
ans = dp[n]; pos = n;
}
int cnt = 0;
while (pos != 0) {
path[++cnt] = pos; pos = from[pos];
}
for (int i = cnt; i >= 1; i--) printf("%d ", path[i]);
printf("\n%d\n", ans);
return 0;
}
例题:P1006 [NOIP2008 提高组] 传纸条
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 55;
int dp[N][N][N][N], a[N][N];
int main()
{
int m, n;
scanf("%d%d", &m, &n);
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
scanf("%d", &a[i][j]);
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
for (int k = 1; k <= m; k++)
for (int l = 1; l <= n; l++)
dp[i][j][k][l] = -1;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
for (int k = 1; k <= m; k++) {
int l = i + j - k;
if (i * j * k * l == 1) dp[i][j][k][l] = a[1][1];
else {
// (i-1,j) (i,j-1)
// (k-1,l) (k,l-1)
int tmp = -1;
if (i > 1 && k > 1)
tmp = max(tmp, dp[i - 1][j][k - 1][l]);
if (i > 1 && l > 1)
tmp = max(tmp, dp[i - 1][j][k][l - 1]);
if (j > 1 && k > 1)
tmp = max(tmp, dp[i][j - 1][k - 1][l]);
if (j > 1 && l > 1)
tmp = max(tmp, dp[i][j - 1][k][l - 1]);
if (tmp == -1) dp[i][j][k][l] = -1;
else dp[i][j][k][l] = tmp + a[i][j] + a[k][l];
if (i == k && j == l && (i != m || j != n))
dp[i][j][k][l] = -1;
}
}
printf("%d\n", dp[m][n][m][n]);
return 0;
}
例题:P7074 [CSP-J2020] 方格取数
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1005;
const LL INF = 1e11;
int a[N][N];
LL dp[N][N][3]; // 0: from up, 1 from down, 2 from left
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
scanf("%d", &a[i][j]);
dp[i][j][0] = dp[i][j][1] = dp[i][j][2] = -INF;
}
dp[1][1][0] = dp[1][1][1] = dp[1][1][2] = a[1][1];
for (int i = 2; i <= n; i++) dp[i][1][0] = dp[i - 1][1][0] + a[i][1];
for (int j = 2; j <= m; j++) {
for (int i = 1; i <= n; i++) {
LL from = max(dp[i][j - 1][0], max(dp[i][j - 1][1], dp[i][j - 1][2]));
if (from != -INF) dp[i][j][2] = from + a[i][j];
}
for (int i = 2; i <= n; i++) {
LL from = max(dp[i - 1][j][0], dp[i - 1][j][2]);
if (from != -INF) dp[i][j][0] = from + a[i][j];
}
for (int i = n - 1; i >= 1; i--) {
LL from = max(dp[i + 1][j][1], dp[i + 1][j][2]);
if (from != -INF) dp[i][j][1] = from + a[i][j];
}
}
printf("%lld\n", max(dp[n][m][0], max(dp[n][m][1], dp[n][m][2])));
return 0;
}
习题:P8816 [CSP-J 2022] 上升点列
解题思路
这个题的状态其实不难想,设 \(dp_{i,j}\) 表示考虑到了第 \(i\) 个点,添加了 \(j\) 个整点时的最优解。
为了能够写出状态转移方程,强制 \(dp_{i,j}\) 表示必须选上第 \(i\) 个点的情况,这样可以从前边枚举一个 \(pre\),然后从 \(dp_{pre,j-add}\) 转移到 \(dp_{i,j}\),其中 \(add\) 表示需要增加的整点数。
那么就需要让整数点以 \(x\) 为第一关键字从小到大排序,\(x\) 相同时以 \(y\) 为第二关键字从小到大排序,在枚举 \(pre\) 时,计算如果从 \(pre\) 到 \(i\) 需要补充的整点数 \(add\),当 \(pre\) 和 \(i\) 的坐标满足相应的大小关系且 \(j \ge add\) 的时候才能转移。
时间复杂度为 \(O(n^2 k)\)。
#include <cstdio>
#include <algorithm>
using std::max;
using std::sort;
const int N = 505;
const int K = 105;
struct Point {
int x, y;
bool operator<(const Point& p) const {
return x != p.x ? x < p.x : y < p.y;
}
};
Point a[N];
int dp[N][K];
int main()
{
int n, k;
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++) scanf("%d%d", &a[i].x, &a[i].y);
for (int i = 1; i <= n; i++)
for (int j = 0; j <= k; j++)
dp[i][j] = j + 1;
sort(a + 1, a + n + 1);
int ans = k + 1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= k; j++) {
for (int pre = 1; pre < i; pre++) {
if (a[pre].x <= a[i].x && a[pre].y <= a[i].y) {
int add = a[i].x - a[pre].x + a[i].y - a[pre].y - 1;
if (j >= add) {
dp[i][j] = max(dp[i][j], dp[pre][j - add] + add + 1);
ans = max(ans, dp[i][j]);
}
}
}
}
}
printf("%d\n", ans);
return 0;
}
习题:P3842 [TJOI2007] 线段
解题思路
显然每一行走完会留在左端点或右端点。
所以设 \(dp_{i,0/1}\) 分别表示走完第 \(i\) 行的线段,留在其左端点或右端点时,路径的最小长度。
转移时枚举是从上一行的哪个端点走过来的,合理的走法应该是先往下走一步,然后往最终要留的端点的另一侧端点方向走,然后再走整个线段的长度走到最终要留在的端点。以上一行的线段左端点走到当前行的线段左端点为例,应该是先走一步向下,然后奔向当前行线段的右端点,最后走过整个当前行线段留在左端点处。
注意初始化 \(dp_{1, 0/1}\),最终计算答案时因为最后一行的线段的左右端点不一定是 \((n,n)\),还要考虑加上到 \((n,n)\) 的距离。
#include <cstdio>
#include <algorithm>
using std::min;
const int N = 2e4 + 5;
const int INF = 1e9;
int l[N], r[N], dp[N][2];
// dp[][0] 留在右端点, dp[][1] 留在左端点
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &l[i], &r[i]);
dp[i][0] = dp[i][1] = INF;
}
dp[1][0] = r[1] - 1; dp[1][1] = r[1] - 1 + (r[1] - l[1]);
for (int i = 2; i <= n; i++) {
dp[i][0] = min(dp[i][0], dp[i - 1][0] + abs(l[i] - r[i - 1]) + r[i] - l[i] + 1);
dp[i][0] = min(dp[i][0], dp[i - 1][1] + abs(l[i] - l[i - 1]) + r[i] - l[i] + 1);
dp[i][1] = min(dp[i][1], dp[i - 1][0] + abs(r[i] - r[i - 1]) + r[i] - l[i] + 1);
dp[i][1] = min(dp[i][1], dp[i - 1][1] + abs(r[i] - l[i - 1]) + r[i] - l[i] + 1);
}
printf("%d\n", min(dp[n][0] + n - r[n], dp[n][1] + n - l[n]));
return 0;
}
习题:P1541 [NOIP2010 提高组] 乌龟棋
解题思路
设 \(dp_{i,j,k,l}\) 表示四种卡片分别使用了 \(i,j,k,l\) 张时能够获得的最大分值。为什么位置不用作为状态的一部分呢?实际上,当四张卡片的用量已知时,所处的格子位置是个定值。
那么此时只需要考虑最近一次用的卡片是哪一张即可,有四种决策策略,取最优的方案。
#include <cstdio>
#include <algorithm>
using std::max;
const int N = 355;
const int A = 45;
int a[N], dp[A][A][A][A], cnt[5];
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= m; i++) {
int b; scanf("%d", &b);
cnt[b]++;
}
dp[0][0][0][0] = a[1];
for (int i = 0; i <= cnt[1]; i++)
for (int j = 0; j <= cnt[2]; j++)
for (int k = 0; k <= cnt[3]; k++)
for (int l = 0; l <= cnt[4]; l++) {
int cur = i + 2 * j + 3 * k + 4 * l + 1;
int tmp = 0;
if (i > 0) tmp = max(tmp, dp[i - 1][j][k][l]);
if (j > 0) tmp = max(tmp, dp[i][j - 1][k][l]);
if (k > 0) tmp = max(tmp, dp[i][j][k - 1][l]);
if (l > 0) tmp = max(tmp, dp[i][j][k][l - 1]);
dp[i][j][k][l] = tmp + a[cur];
}
printf("%d\n", dp[cnt[1]][cnt[2]][cnt[3]][cnt[4]]);
return 0;
}
例题:P1434 [SHOI2002] 滑雪
设 \(dp_{i,j}\) 表示从某个起点处一直滑雪到 \((i,j)\) 处的最长滑雪长度。则有 \(dp_{i,j} = \max\{dp_{neighbor}\}\) + 1,这里的 \(neighbor\) 指的是四个相邻位置,并且需要满足相邻位置的高度高于当前位置 \((i,j)\)。
这样就发现一个问题,计算顺序如何确定?如何保证计算某个位置时其四个相邻位置中可以转移过来的位置的结果已经计算完成。
实际上如果写成记忆化搜索的形式,就可以减少对计算顺序的思考。在记忆化搜索的模式下,必然可以保证计算顺序。
参考代码
#include <cstdio>
#include <algorithm>
using std::max;
const int N = 105;
int r, c, a[N][N], dp[N][N];
int dx[4] = {-1, 0, 0, 1};
int dy[4] = {0, -1, 1, 0};
void dfs(int x, int y) {
if (dp[x][y] != 0) return;
for (int i = 0; i < 4; ++i) {
int xx = x + dx[i];
int yy = y + dy[i];
if (xx >= 1 && xx <= r && yy >= 1 && yy <= c && a[x][y] < a[xx][yy]) {
dfs(xx, yy);
dp[x][y] = max(dp[x][y], dp[xx][yy]);
}
}
dp[x][y]++; // 延伸一次滑雪
// printf("dp[%d][%d] = %d\n", x, y, dp[x][y]);
}
int main()
{
scanf("%d%d", &r, &c);
for (int i = 1; i <= r; i++)
for (int j = 1; j <= c; j++)
scanf("%d", &a[i][j]);
for (int i = 1; i <= r; i++)
for (int j = 1; j <= c; j++)
if (dp[i][j] == 0) dfs(i, j);
int ans = 1;
for (int i = 1; i <= r; i++)
for (int j = 1; j <= c; j++)
ans = max(ans, dp[i][j]);
printf("%d\n", ans);
return 0;
}