动态规划基础
动态规划
方案数问题
例题: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 杂务
为了让每个任务尽早完成,应该让其在所有准备工作完成后立马开始,也就是跟在所有准备工作中最晚结束的那个后面。如果设
参考代码
#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 提高组] 挖地雷
设
注意,本题还要输出挖地雷的顺序,前面的计算过程并没有考虑最优方案对应的路径。
实际上,这只需要在更新
参考代码
#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] 上升点列
解题思路
这个题的状态其实不难想,设
为了能够写出状态转移方程,强制
那么就需要让整数点以
时间复杂度为
#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] 线段
解题思路
显然每一行走完会留在左端点或右端点。
所以设
转移时枚举是从上一行的哪个端点走过来的,合理的走法应该是先往下走一步,然后往最终要留的端点的另一侧端点方向走,然后再走整个线段的长度走到最终要留在的端点。以上一行的线段左端点走到当前行的线段左端点为例,应该是先走一步向下,然后奔向当前行线段的右端点,最后走过整个当前行线段留在左端点处。
注意初始化
#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 提高组] 乌龟棋
解题思路
设
那么此时只需要考虑最近一次用的卡片是哪一张即可,有四种决策策略,取最优的方案。
#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] 滑雪
设
这样就发现一个问题,计算顺序如何确定?如何保证计算某个位置时其四个相邻位置中可以转移过来的位置的结果已经计算完成。
实际上如果写成记忆化搜索的形式,就可以减少对计算顺序的思考。在记忆化搜索的模式下,必然可以保证计算顺序。
参考代码
#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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!