区间 DP、环形 DP
区间 DP
区间 DP 是可以由小区间的结果往两边扩展一位得到大区间的结果,或者由两个小区间的结果可以拼出大区间的结果的一类 DP 问题
往往设
因此一般是先写一层循环从小到大枚举长度
区间 DP 也可以由大区间推到小区间,此时可以从大到小枚举
区间 DP 的提示信息:
- 从两端取出或在两端插入,这就是大区间变到小区间或者小区间变到大区间
- 合并相邻的,这样的一步相当于把已经处理好的两个小区间得到的结果合并为当前大区间的结果
- 消去连续一段使两边接起来,可以枚举最后一次消哪个区间,这样就可以把大区间拆成小区间
- 两个东西可以配对消掉,这时往往可以按左端点和哪个东西配对,把当前区间拆成两个子区间的问题
- 时间复杂度通常为
或
例:P2858 [USACO06FEB] Treats for the Cows G/S
解题思路
考虑操作过程,以第一步为例,你会把
我们可以考虑最后一次拿零食,此时一定是只剩一件零食了,这就是长度为
由此我们设计
那么状态转移方程就是
初始化
时间复杂度
参考代码
#include <cstdio> #include <algorithm> using namespace std; const int N = 2005; int v[N], dp[N][N]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d", &v[i]); dp[i][i] = v[i] * n; } for (int len = 2; len <= n; len++) { for (int i = 1; i <= n - len + 1; i++) { int j = i + len - 1, a = n - len + 1; dp[i][j] = max(dp[i + 1][j] + v[i] * a, dp[i][j - 1] + v[j] * a); } } printf("%d\n", dp[1][n]); return 0; }
例:P3205 [HNOI2010] 合唱队
解题思路
每次插入到队伍最左边或最右边,也就是说如果
但是能不能插进来还要看这次加入的数和上一次插入的数是否符合对应的大小关系,因此我们还需要知道插入的最后一个数是最左边的还是最右边的
可以设
考虑转移,对于
对于
时间复杂度
参考代码
#include <cstdio> const int N = 1005; const int MOD = 19650827; int dp[N][N][2], h[N]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d", &h[i]); dp[i][i][0] = 1; } for (int len = 2; len <= n; len++) { for (int i = 1; i <= n - len + 1; i++) { int j = i + len - 1; // [i,j] from [i+1,j] [i,j-1] if (h[i] < h[i+1]) dp[i][j][0] = (dp[i][j][0] + dp[i+1][j][0]) % MOD; if (h[i] < h[j]) dp[i][j][0] = (dp[i][j][0] + dp[i+1][j][1]) % MOD; if (h[j] > h[i]) dp[i][j][1] = (dp[i][j][1] + dp[i][j-1][0]) % MOD; if (h[j] > h[j-1]) dp[i][j][1] = (dp[i][j][1] + dp[i][j-1][1]) % MOD; } } printf("%d\n", (dp[1][n][0] + dp[1][n][1]) % MOD); return 0; }
例:P3146 [USACO16OPEN] 248 G
这个问题和之前扩展一位的问题略有不同,这是由两个区间的结果合并推到更大区间的结果
解题思路
可以设
初始化
考虑转移,对于区间
这样就可以写出状态转移方程:如果
时间复杂度
参考代码
#include <cstdio> #include <algorithm> using namespace std; const int N = 250; int dp[N][N]; int main() { int n, ans = 0; scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d", &dp[i][i]); ans = max(ans, dp[i][i]); } for (int len = 2; len <= n; len++) { for (int i = 1; i <= n - len + 1; i++) { int j = i + len - 1; for (int k = i; k < j; k++) if (dp[i][k] == dp[k + 1][j] && dp[i][k]) dp[i][j] = max(dp[i][j], dp[i][k] + 1); ans = max(ans, dp[i][j]); } } printf("%d\n", ans); return 0; }
例:P4170 [CQOI2007] 涂色
解题思路
考虑染色过程,因为是一段一段染的,长段可以看成是两个短段拼起来,并且如果某一次染了一段之后,可以在这段内部继续染色,这都提示我们可以考虑区间 DP
设
特殊情况:如果
分析:拆段意味着存在两步分别染
对于其他题目,有可能出现在可以不拆段时依然是拆段取到最优解的情况,注意分析,如果想简化分析过程可以统一枚举拆段转移最优解的过程,因为在可能需要拆段的题目中这样做不会影响时间复杂度
时间复杂度
参考代码
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N = 55; char s[N]; int dp[N][N]; int main() { scanf("%s", s + 1); int n = strlen(s + 1); for (int i = 1; i <= n; i++) dp[i][i] = 1; for (int len = 2; len <= n; len++) { for (int i = 1; i <= n - len + 1; i++) { int j = i + len - 1; dp[i][j] = min(dp[i + 1][j], dp[i][j - 1]) + (s[i] != s[j]); for (int k = i; k < j; k++) dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]); } } printf("%d\n", dp[1][n]); return 0; }
例:CF607B Zuma
解题思路
设
状态转移方程
注意:就算是
因此无论
参考代码
#include <cstdio> #include <algorithm> using namespace std; const int N = 505; int c[N], dp[N][N]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d", &c[i]); dp[i][i] = 1; } for (int len = 2; len <= n; len++) { for (int i = 1; i <= n - len + 1; i++) { int j = i + len - 1; dp[i][j] = min(dp[i + 1][j], dp[i][j - 1]) + 1; if (c[i] == c[j]) dp[i][j] = min(dp[i][j], len == 2 ? 1 : dp[i + 1][j - 1]); for (int k = i; k < j; k++) dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]); } } printf("%d\n", dp[1][n]); return 0; }
例:CF149D Coloring Brackets
解题思路
设
当
当
以上两种转移方式要想组合到一起不容易用循环来实现,实际上这里更容易写的方式是利用递归回溯的过程完成计算
参考代码
#include <cstdio> #include <cstring> #include <stack> using namespace std; const int N = 705; const int MOD = 1000000007; char s[N]; // 0: none, 1: red, 2: blue int dp[N][N][3][3], match[N]; void dfs(int l, int r) { if (l >= r) return; if (l + 1 == r) { // 会执行到这个位置只能是 () dp[l][r][0][1] = dp[l][r][0][2] = 1; dp[l][r][1][0] = dp[l][r][2][0] = 1; return; } if (match[l] != r) { dfs(l, match[l]); dfs(match[l] + 1, r); for (int c1 = 0; c1 < 3; c1++) { for (int c2 = 0; c2 < 3; c2++) { for (int c3 = 0; c3 < 3; c3++) { if (c2 == c3 && c2 != 0) continue; for (int c4 = 0; c4 < 3; c4++) { int left = dp[l][match[l]][c1][c2]; int right = dp[match[l] + 1][r][c3][c4]; dp[l][r][c1][c4] += 1ll * left * right % MOD; dp[l][r][c1][c4] %= MOD; } } } } } else { dfs(l + 1, r - 1); for (int c1 = 0; c1 < 3; c1++) { for (int c2 = 0; c2 < 3; c2++) { if (c1 == c2 && c1 != 0) continue; for (int c3 = 0; c3 < 3; c3++) { for (int c4 = 0; c4 < 3; c4++) { if (c3 == c4 && c3 != 0) continue; if (c1 == 0 && c4 == 0) continue; if (c1 != 0 && c4 != 0) continue; dp[l][r][c1][c4] += dp[l + 1][r - 1][c2][c3]; dp[l][r][c1][c4] %= MOD; } } } } } } int main() { scanf("%s", s + 1); int n = strlen(s + 1); stack<int> stk; for (int i = 1; i <= n; i++) { dp[i][i][0][0] = dp[i][i][1][1] = dp[i][i][2][2] = 1; if (s[i] == '(') { stk.push(i); } else if (s[i] == ')') { int t = stk.top(); stk.pop(); match[i] = t; match[t] = i; } } dfs(1, n); int ans = 0; for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) ans = (ans + dp[1][n][i][j]) % MOD; printf("%d\n", ans); return 0; }
例:P7914 [CSP-S 2021] 括号序列
解题思路
我们设符合要求的序列成为
根据题意,符合要求的括号序列可以分成
对于
对于
然而,对于
解决方法:计算
参考代码
#include <cstdio> const int N = 505; const int MOD = 1000000007; char s[N]; int dp_a[N][N], dp_s[N][N], dp_ba[N][N], dp_sa[N][N]; bool check(int idx, char ch) { return s[idx] == '?' || s[idx] == ch; } int main() { int n, k; scanf("%d%d%s", &n, &k, s + 1); for (int i = 1; i <= n; i++) if (check(i, '*')) dp_s[i][i] = 1; if (k >= 2) { for (int i = 1; i < n; i++) if (check(i, '*') && check(i + 1, '*')) dp_s[i][i + 1] = 1; for (int len = 3; len <= k; len++) { for (int i = 1; i <= n - len + 1; i++) { int j = i + len - 1; if (check(i, '*') && check(j, '*')) dp_s[i][j] = dp_s[i + 1][j - 1]; } } } for (int i = 1; i < n; i++) { if (check(i, '(') && check(i + 1, ')')) dp_ba[i][i + 1] = dp_a[i][i + 1] = 1; } for (int len = 3; len <= n; len++) { for (int i = 1; i <= n - len + 1; i++) { int j = i + len - 1; if (check(i, '(') && check(j, ')')) { // (A) dp_ba[i][j] += dp_a[i + 1][j - 1]; dp_ba[i][j] %= MOD; // (S) dp_ba[i][j] += dp_s[i + 1][j - 1]; dp_ba[i][j] %= MOD; // (AS) (SA) for (int k = i + 1; k < j - 1; k++) { dp_ba[i][j] += 1ll * dp_a[i + 1][k] * dp_s[k + 1][j - 1] % MOD; dp_ba[i][j] %= MOD; dp_ba[i][j] += 1ll * dp_s[i + 1][k] * dp_a[k + 1][j - 1] % MOD; dp_ba[i][j] %= MOD; } dp_a[i][j] = dp_ba[i][j]; } // AA ASA for (int k = i; k < j; k++) { dp_a[i][j] += 1ll * dp_ba[i][k] * dp_a[k + 1][j] % MOD; dp_a[i][j] %= MOD; dp_a[i][j] += 1ll * dp_ba[i][k] * dp_sa[k + 1][j] % MOD; dp_a[i][j] %= MOD; dp_sa[i][j] += 1ll * dp_s[i][k] * dp_a[k + 1][j] % MOD; dp_sa[i][j] %= MOD; } } } printf("%d\n", dp_a[1][n]); return 0; }
环形 DP
有时我们会面临输入是环形数组的情况,即认为
-
如果是线性 DP,比如选数问题,可以对
是否选进行分类,假设 不选,把 初始化好,最后推到 时,只留下 不选的情况计入答案;再假设 选,把 初始化好,最后推到 时,只留下 选的情况计入答案 -
如果是区间 DP,一种常见的方法是破环成链,将数组复制一倍接在原数组之后,然后对这个长度为
的数组进行长度不超过 的区间 DP 中 且 的情况就是把 和 也看成了相邻的,比如 ,就代表原数组 这样一个环上的结果
例:P1880 [NOI1995] 石子合并
解题思路
破环成链后,对产生的长度为
设
设
其中
参考代码
#include <cstdio> #include <algorithm> using namespace std; const int N = 205; const int INF = 1e9; int a[N], sum[N]; int dp1[N][N]; int dp2[N][N]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); sum[i] = sum[i - 1] + a[i]; a[i + n] = a[i]; } for (int i = n + 1; i <= 2 * n; i++) sum[i] = sum[i - 1] + a[i]; for (int i = 1; i < 2 * n; i++) for (int j = 1; j < 2 * n; j++) { dp1[i][j] = INF; dp2[i][j] = 0; } for (int i = 1; i < 2 * n; i++) dp1[i][i] = 0; for (int len = 2; len <= n; len++) { for (int i = 1; i <= 2 * n - len; i++) { int j = i + len - 1; for (int k = i; k < j; k++) { int total = sum[j] - sum[i - 1]; dp1[i][j] = min(dp1[i][j], dp1[i][k] + dp1[k + 1][j] + total); dp2[i][j] = max(dp2[i][j], dp2[i][k] + dp2[k + 1][j] + total); } } } int ans1 = INF, ans2 = 0; for (int i = 1; i <= n; i++) { ans1 = min(ans1, dp1[i][i + n - 1]); ans2 = max(ans2, dp2[i][i + n - 1]); } printf("%d\n%d\n", ans1, ans2); return 0; }
例:P1063 [NOIP2006 提高组] 能量项链
解题思路
与石子合并基本相同,先破环成链,然后设
则有
需要注意要乘的三个数是哪三个,尤其是最后一个数,因为需要
最后答案为
参考代码
#include <cstdio> #include <algorithm> using namespace std; const int N = 205; int a[N], dp[N][N]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); a[i + n] = a[i]; } a[2 * n + 1] = a[1]; for (int len = 2; len <= n; len++) { for (int i = 1; i <= 2 * n - len; i++) { int j = i + len - 1; for (int k = i; k < j; k++) { int energy = a[i] * a[k + 1] * a[j + 1]; dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j] + energy); } } } int ans = 0; for (int i = 1; i <= n; i++) ans = max(ans, dp[i][i + n - 1]); printf("%d\n", ans); return 0; }
例:P9119 [春季测试 2023] 圣诞树
前 6 个测试点(前 30 分),直接枚举全排列即可
前 12 个测试点(前 60 分),哈密顿路问题(后续在状态压缩 DP 中会讲)
特殊性质 B(额外 10 分),答案为
解题思路
首先要分析出一个重要结论
考虑将该凸多边形按最高点分为两边,最优路径一定不会出现交叉的情况,例如针对题图中的
也不会出现
因此,最优路径一定会是先在一边从头按顺序走几步,再去另一边从头按顺序走几步,再回初始那一边从没到过的点开始再按着顺序走几步,再去另一边走几步,这样不断交替(注意第
可以将输入的数据复制一份,破环成链以后最优路径走过的点一定是包含起点的一段长度为
可以设
这道题最后不是要输出最优解的那个值,而是输出路径,因此我们需要在 DP 过程中记录方案
常用方法是开一个和
最后先扫一遍所有长度为
参考代码
#include <cstdio> #include <cmath> #include <algorithm> using namespace std; const int N = 2005; const double INF = 1e12; double x[N], y[N], dp[N][N][2]; // from 记录从上一个状态的“左端点”还是“右端点”转移过来 int from[N][N][2], ans[N]; double distance(int i, int j) { double dx = x[i] - x[j], dy = y[i] - y[j]; return sqrt(dx * dx + dy * dy); } int main() { int n, k = 1; scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%lf%lf", &x[i], &y[i]); x[i + n] = x[i]; y[i + n] = y[i]; if (y[i] > y[k]) k = i; } for (int i = 1; i <= 2 * n; i++) for (int j = 1; j <= 2 * n; j++) dp[i][j][0] = dp[i][j][1] = INF; dp[k][k][0] = dp[k][k][1] = 0; dp[k + n][k + n][0] = dp[k + n][k + n][1] = 0; for (int len = 2; len <= n; len++) { for (int i = 1; i <= 2 * n - len + 1; i++) { int j = i + len - 1; // dp[i][j][0]: to i double tmp = dp[i + 1][j][0] + distance(i, i + 1); // i+1 -> i if (tmp < dp[i][j][0]) { dp[i][j][0] = tmp; from[i][j][0] = 0; } tmp = dp[i + 1][j][1] + distance(i, j); // j -> i if (tmp < dp[i][j][0]) { dp[i][j][0] = tmp; from[i][j][0] = 1; } // dp[i][j][1]: to j tmp = dp[i][j - 1][0] + distance(i, j); // i -> j if (tmp < dp[i][j][1]) { dp[i][j][1] = tmp; from[i][j][1] = 0; } tmp = dp[i][j - 1][1] + distance(j - 1, j); // j-1 -> j if (tmp < dp[i][j][1]) { dp[i][j][1] = tmp; from[i][j][1] = 1; } } } int mini = 0, f = 0; double mindis = INF; for (int i = 1; i <= n; i++) { if (dp[i][i + n - 1][0] < mindis) { mindis = dp[i][i + n - 1][0]; mini = i; f = 0; } if (dp[i][i + n - 1][1] < mindis) { mindis = dp[i][i + n - 1][1]; mini = i; f = 1; } } int l = mini, r = mini + n - 1; for (int i = 1; i <= n; i++) { if (f == 0) { ans[i] = l; f = from[l][r][0]; l++; } else { ans[i] = r; f = from[l][r][1]; r--; } } for (int i = n; i >= 1; i--) printf("%d%c", ans[i] > n ? ans[i] - n : ans[i], i == 1 ? '\n' : ' '); 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框架的用法!