环形 DP
概念讲解
什么是环形 DP?
环形 DP 是将环形问题转化为线性问题的技巧。
核心思想
破环为链:将环复制一倍,转化为长度为 2n 的链。
经典问题
1. 环形石子合并
问题: n 堆石子围成一圈,每次合并相邻两堆,求最小代价。
解答:
1234567891011121314151617181920212223242526272829303132333435363738394041int circularStoneMerge(vector<int>& stones) {
int n = stones.size();
vector<int> a(2 * n);
// 破环为链
for (int i = 0; i < n; i++) {
a[i] = a[i + n] = stones[i];
}
// 计算前缀和
vector<int> prefix(2 * n + 1, 0);
for (int i = 1; i <= 2 * n; i++) {
prefix[i] = prefix[i - 1] + a[i - 1];
}
// 区间 DP
vector<vector<int>> dp(2 * n, vector<int>(2 * n, 1e9));
for (int i = 0; i < 2 * n; i++) {
dp[i][i] = 0;
}
for (int len = 2; len <= n; len++) {
for (int i = 0; i + len - 1 < 2 * n; i++) {
int j = i + len - 1;
int sum = prefix[j + 1] - prefix[i];
for (int k = i; k < j; k++) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum);
}
}
}
// 在所有长度为 n 的区间中找最小值
int result = 1e9;
for (int i = 0; i < n; i++) {
result = min(result, dp[i][i + n - 1]);
}
return result;
}考点分析
初赛考查方式
- 选择题: 环形 DP 的概念
- 程序阅读: 理解环形 DP
- 编程题: 环形 DP 应用
常见题型
- 环形石子合并
- 环形背包
- 环形涂色
例题精讲
例题 1:环形石子合并
题目: n 堆石子围成一圈,求合并成一堆的最小代价。
解答:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152#include <iostream>
#include <vector>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> stones(n);
for (int i = 0; i < n; i++) {
cin >> stones[i];
}
// 破环为链
vector<int> a(2 * n);
for (int i = 0; i < n; i++) {
a[i] = a[i + n] = stones[i];
}
// 计算前缀和
vector<int> prefix(2 * n + 1, 0);
for (int i = 1; i <= 2 * n; i++) {
prefix[i] = prefix[i - 1] + a[i - 1];
}
// 区间 DP
vector<vector<int>> dp(2 * n, vector<int>(2 * n, 1e9));
for (int i = 0; i < 2 * n; i++) {
dp[i][i] = 0;
}
for (int len = 2; len <= n; len++) {
for (int i = 0; i + len - 1 < 2 * n; i++) {
int j = i + len - 1;
int sum = prefix[j + 1] - prefix[i];
for (int k = i; k < j; k++) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum);
}
}
}
int result = 1e9;
for (int i = 0; i < n; i++) {
result = min(result, dp[i][i + n - 1]);
}
cout << result << endl;
return 0;
}练习题
- 洛谷 P1880 石子合并
查看答案
思路:环形石子合并,破环为链后做区间 DP。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849#include <iostream>
#include <vector>
using namespace std;
int main() {
int n;
cin >> n;
int a[2 * n + 1];
for (int i = 1; i <= n; i++) {
cin >> a[i];
a[i + n] = a[i];
}
int prefix[2 * n + 1] = {0};
for (int i = 1; i <= 2 * n; i++) {
prefix[i] = prefix[i - 1] + a[i];
}
int dpMin[2 * n + 1][2 * n + 1], dpMax[2 * n + 1][2 * n + 1];
for (int i = 1; i <= 2 * n; i++) {
for (int j = 1; j <= 2 * n; j++) {
dpMin[i][j] = 1e9;
dpMax[i][j] = 0;
}
dpMin[i][i] = 0;
dpMax[i][i] = 0;
}
for (int len = 2; len <= n; len++) {
for (int i = 1; i + len - 1 <= 2 * n; i++) {
int j = i + len - 1;
int sum = prefix[j] - prefix[i - 1];
for (int k = i; k < j; k++) {
dpMin[i][j] = min(dpMin[i][j], dpMin[i][k] + dpMin[k + 1][j] + sum);
dpMax[i][j] = max(dpMax[i][j], dpMax[i][k] + dpMax[k + 1][j] + sum);
}
}
}
int minAns = 1e9, maxAns = 0;
for (int i = 1; i <= n; i++) {
minAns = min(minAns, dpMin[i][i + n - 1]);
maxAns = max(maxAns, dpMax[i][i + n - 1]);
}
cout << minAns << endl << maxAns << endl;
return 0;
}- 洛谷 P1005 矩阵取数游戏
查看答案
思路:每行独立,对每行做区间 DP。
123456789101112131415161718192021222324252627282930313233#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
long long ans = 0;
for (int row = 0; row < n; row++) {
vector<int> a(m);
for (int i = 0; i < m; i++) cin >> a[i];
vector<vector<long long>> dp(m + 1, vector<long long>(m + 1, 0));
for (int len = 1; len <= m; len++) {
for (int i = 0; i + len - 1 < m; i++) {
int j = i + len - 1;
int power = m - len + 1;
dp[i][j] = max(
(i <= j - 1 ? dp[i][j - 1] : 0) + (long long)a[j] * power,
(i + 1 <= j ? dp[i + 1][j] : 0) + (long long)a[i] * power
);
}
}
ans += dp[0][m - 1];
}
cout << ans << endl;
return 0;
}- 洛谷 P3146 248
查看答案
思路:区间 DP,合并相邻相同值。
12345678910111213141516171819202122232425262728293031323334#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int a[n];
for (int i = 0; i < n; i++) cin >> a[i];
int dp[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
dp[i][j] = 0;
}
dp[i][i] = a[i];
}
int ans = 0;
for (int len = 2; len <= n; len++) {
for (int i = 0; i + len - 1 < n; i++) {
int j = i + len - 1;
for (int k = i; k < j; k++) {
if (dp[i][k] == dp[k + 1][j] && dp[i][k] != 0) {
dp[i][j] = max(dp[i][j], dp[i][k] + 1);
}
}
ans = max(ans, dp[i][j]);
}
}
cout << ans << endl;
return 0;
}易错点
⚠️ 注意:
- 环形 DP 要破环为链
- 复制一倍后要找所有长度为 n 的区间
- 环形 DP 是区间 DP 的变种
- 注意边界条件