20260604 - 区间dp 总结
一. 区间 dp
概念:区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系。
性质:
- 合并:即将两个或多个部分进行整合,当然也可以反过来;
- 特征:能将问题分解为能两两合并的形式;
- 求解:对整个问题设最优值,枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值
说了这么多,不如做几道题来让自己死的更惨
送分题看这个 P1435 [IOI 2000] 回文字串
(在此之前先默认读完了题目)
方法一(线性 dp): 首先,我们要摸清回文串的特性,回文就是正着读反着读一样,一种非常对称不会逼死强迫症的字符串;这就是我们的突破口。突破口其实是因为回文正着读反着读都相同的特性。这样我们就可以再建一个字符数组存储倒序的字符串。
其次用奇奇妙妙的 LCS(最长公共子序列),这个会吧,不会移步 奇奇妙妙的 lcs(这儿可以不用二分),那么最后的结果呢?就是 \(n - dp_{n,n},(n 表示字符串的长度)\), 完结!(就是 WA 了)
方法二(区间 DP):
首先搞清楚 DP 的状态,在按照状态进行转移。那么这个的动态转移方程是什么呢?
设 \(dp_{i,j}\) 为要把从 \(i\) 到 \(j\)(包括 \(i\) 和 \(j\))的字符串变为回文串的最少插入字符数。
怎么转移? 有两种可能:
\(S_i = S_j\) 说明串的两头相等,不需要多插入什么,只需要 \(dp_{i,j}=dp_{i+1,j−1}\) 就好了,\(i+1\) 到 \(j−1\) 一段就是除两头外的最少插入字符数
\(S_{i} \neq S_{j}\) 说明串的两头不同,需要多插入 1 字符以保持串的回文性质,我们这次新插入的字符可能在最右边,也可能在最左边。所以需要 \(dp_{i,j}= \min (dp_{i+1,j},dp_{i,j−1})+1\)
一个“小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小小”的坑。当 \(l = 2\) 时,其实要特判一下,不让当最后,会有一些奇奇妙妙的事情出现。
再给大家整一道送命题
二. 环形 dp
题目大意:在一个环上有 \(n\) 个数 \(a_1,a_2,\dots,a_n\),进行 \(n-1\) 次合并操作,每次操作将相邻的两堆合并成一堆,能获得新的一堆中的石子数量的和的得分。你需要最大化你的得分。
题目中石子围成一个环,而不是一条链,怎么办呢?
方法一:由于石子围成一个环,我们可以枚举分开的位置,将这个环转化成一个链,由于要枚举 \(n\) 次,最终的时间复杂度为 \(O(n^4)\)。
方法二:我们将这条链延长两倍,变成 \(2\times n\) 堆,其中第 \(i\) 堆与第 \(n+i\) 堆相同,用动态规划求解后,取 \(f(1,n),f(2,n+1),\dots,f(n,2n-1)\) 中的最优值,即为最后的答案。时间复杂度 \(O(n^3)\)。
A - 石子合并(弱化版)
题意简述:每次可以选择相邻的两堆合并,代价为两堆的和,求合并的最小代价是多少。
难度:1400
tag:DP
思路:设 \(dp_{l},_{r}\) 为 \([l,r]\) 区间的最小代价。如果合并,可以枚举一个分界点,然后合并,代价就是区间和,显然,可以前缀和优化。
时间复杂度:\(O(n^3)\)。
空间复杂度:\(O(n^2)\)。
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
constexpr int N = 300 + 7;
constexpr int P = 998244353;
int n, a[N];
int dp[N][N];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]), a[i] += a[i - 1];
memset(dp, 0x3f, sizeof(dp));
for (int i = 1; i <= n; i++) dp[i][i] = 0;
for (int d = 2; d <= n; d++) {
for (int i = 1; i + d - 1 <= n; i++) {
int j = i + d - 1;
for (int k = i; k < j; k++) {
int w = a[j] - a[i - 1];
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w);
}
}
}
printf("%d\n", dp[1][n]);
return 0;
}
诶,既然是 DP,一定有记忆化吧,然后,送上记忆化代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
constexpr int N = 300 + 7;
constexpr int P = 998244353;
int n, a[N];
int dp[N][N];
int solve(int l, int r) {
if (l == r) return 0;
if (dp[l][r] != -1) return dp[l][r];
int res = inf;
for (int k = l; k < r; k++) {
res = min(res, solve(l, k) + solve(k + 1, r) + a[r] - a[l - 1]);
}
return dp[l][r] = res;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]), a[i] += a[i - 1];
memset(dp, -1, sizeof(dp));
printf("%d\n", solve(1, n));
return 0;
}
然而,我有时候想写递推,有时候想写记忆化。
B - Deque
题意简述:A 和 B 想玩游戏,若 deque 不为空,就可以从开头或结尾删除一个数,得分增加删除的数,设 A 得分为 \(A\),B 得分为 \(B\),A 想 \(A - B\) 最大,B 想 \(A - B\) 最小,求最后的 \(A - B\)。
难度:1500
tag:DP
思路:我们发现,当 A 操作时,增加量为 \(+A\),B 操作时,增加量为 \(-B\),然后就可以区间 DP 了。
时间复杂度:\(O(n^2)\)。
空间复杂度:\(O(n^2)\)。
显然,写记忆化可以不用算第几步。
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
constexpr int N = 3000 + 7;
constexpr int P = 998244353;
int n, a[N];
ll dp[N][N];
ll solve(int l, int r, int cur) {
if (l > r) return 0;
if (dp[l][r] != 0x3f3f3f3f3f3f3f3f) return dp[l][r];
if (cur & 1) return dp[l][r] = max(solve(l, r - 1, cur + 1) + a[r], solve(l + 1, r, cur + 1) + a[l]);
else return dp[l][r] = min(solve(l, r - 1, cur + 1) - a[r], solve(l + 1, r, cur + 1) - a[l]);
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
memset(dp, 0x3f, sizeof(dp));
printf("%lld\n", solve(1, n, 1));
return 0;
}
C - Treats for the Cows G/S
题意简述:一个数列,可以取最前面的数或者最后面的数,取的代价为 \(v_i\) 乘第几个取的。
难度:1600
tag:DP
思路:每一次判断是取当前区间的左边还是右边,DP 的边界条件是取到最后只剩下一个元素。
时间复杂度:\(O(n^2)\)。
空间复杂度:\(O(n^2)\)。
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
constexpr int N = 2000 + 7;
constexpr int P = 998244353;
int n, a[N];
ll dp[N][N];
ll solve(int l, int r, int cur) {
if (l > r) return 0;
if (dp[l][r] != -1) return dp[l][r];
return dp[l][r] = max(solve(l, r - 1, cur + 1) + a[r] * cur, solve(l + 1, r, cur + 1) + a[l] * cur);
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
memset(dp, -1, sizeof(dp));
printf("%lld\n", solve(1, n, 1));
return 0;
}
D - 石子合并
见上。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
constexpr int N = 300 + 7;
constexpr int P = 998244353;
int n, a[N];
int dp[N][N], f[N][N];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]), a[i + n] = a[i];
n *= 2;
for (int i = 1; i <= n; i++) a[i] += a[i - 1];
memset(dp, 0x3f, sizeof(dp));
memset(f, 0, sizeof(f));
for (int i = 1; i <= n; i++) dp[i][i] = 0, f[i][i] = 0;
for (int d = 2; d <= n; d++) {
for (int i = 1; i + d - 1 <= n; i++) {
int j = i + d - 1;
for (int k = i; k < j; k++) {
int w = a[j] - a[i - 1];
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w);
f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j] + w);
}
}
}
n /= 2;
int ans1 = inf, ans2 = 0;
for (int i = 1; i <= n; i++) {
ans1 = min(ans1, dp[i][i + n - 1]);
ans2 = max(ans2, f[i][i + n - 1]);
}
printf("%d\n%d\n", ans1, ans2);
return 0;
}
E - The Sports Festival
题意简述:设 \(d_i\) 为 \(\max_{i=1}^{i} s_i\),可以调换顺序,使得 \(\sum_{i=1}^nd_i\) 经可能小。
难度:1600
tag:DP
思路:我们发现,排完序后肯定是选相邻的最好,因为如果选了其他的,肯定不如选了当前的,所以可以区间 DP 了。
PS:代码抄的第二题的。
时间复杂度:\(O(n^2)\)。
空间复杂度:\(O(n^2)\)。
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
constexpr int N = 2000 + 7;
constexpr int P = 998244353;
int n, a[N];
ll dp[N][N];
ll solve(int l, int r) {
if (l > r) return 0;
if (dp[l][r] != -1) return dp[l][r];
return dp[l][r] = min(solve(l, r - 1) + a[r] - a[l], solve(l + 1, r) + a[r] - a[l]);
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
sort(a + 1, a + n + 1);
memset(dp, -1, sizeof(dp));
printf("%lld\n", solve(1, n));
return 0;
}
F - Queries for Number of Palindromes
题意简述:求区间回文串个数。
难度:1700
tag:前缀和,DP,容斥
思路:首先考虑
manacher区间 DP,可以用一个bool数组标记区间是否为回文串,转移显然是dp[l][r] = dp[l + 1][r - 1] && s[l] == s[r],但我们发现,当 \(len = 2\) 时,会错过区间,我们特判一下就好了。然后题目就变成区间查询左端点 \(\ge l\) 且右端点 \(\le r\)。显然,这里可以用二维前缀和来搞一搞,不需要上 Segment_Tree。
时间复杂度:\(O(n^2)\)。
空间复杂度:\(O(n^2)\)。
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
constexpr int N = 5000 + 7;
constexpr int P = 998244353;
int n;
char s[N];
bool dp[N][N];
int pre[N][N];
int main() {
scanf("%s", s + 1);
n = strlen(s + 1);
for (int i = 1; i <= n; i++) dp[i][i] = true;
for (int d = 2; d <= n; d++) {
for (int i = 1; i + d - 1 <= n; i++) {
int j = i + d - 1;
if (s[i] != s[j]) {
dp[i][j] = false;
} else {
if (j - i + 1 <= 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
}
}
for (int i = 1; i <= n; i++) {
for (int j = i; j <= n; j++) {
if (dp[i][j]) {
++pre[i][j];
}
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
pre[i][j] += pre[i][j - 1];
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
pre[i][j] += pre[i - 1][j];
}
}
int q;
scanf("%d", &q);
while (q--) {
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", pre[r][r] - pre[r][l - 1] - pre[l - 1][r] + pre[l - 1][l - 1]);
}
return 0;
}
G - Zuma
题意简述:给定一个序列,如果其子序列是一个回文字串,那么它可以一次消除。问最少需要多少次才可以将字符串全部消除。
难度:1900
tag:DP
思路:就是判断一下是从左边删的还是右边删的就好了,坑点同上。
时间复杂度:\(O(n^2)\)。
空间复杂度:\(O(n^2)\)。
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
constexpr int N = 500 + 7;
constexpr int P = 998244353;
int n;
int a[N];
int dp[N][N];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
memset(dp, 0x3f, sizeof(dp));
for (int i = 1; i <= n; i++) dp[i][i] = 1;
for (int i = 1; i < n; i++) dp[i][i + 1] = 1 + (a[i] != a[i + 1]);
for (int d = 3; d <= n; d++) {
for (int i = 1; i + d - 1 <= n; i++) {
int j = i + d - 1;
// printf("i = %d j = %d\n", i, j);
if (a[i] == a[j]) {
dp[i][j] = 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;
}
H - Flood Fill
题意简述:有 \(n\) 个位置,初始颜色已知。每一次可以将与一个格子相邻的所有颜色相同的格子的颜色更改为任意数。要在尽可能少的步数内将所有格子染成同一个颜色,求这个步数。
难度:1900
tag:DP
思路:和上一题类似,就是注意左边还是右边。
时间复杂度:\(O(n^2)\)。
空间复杂度:\(O(n^2)\)。
#include <bits/stdc++.h>
using namespace std;
#define debug(x) cout<<#x<<' '<<x<<'\n'
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
constexpr int N = 5000 + 7;
constexpr int P = 998244353;
int n;
int a[N];
int dp[N][N];
int deluni() {
vector<int> c;
for (int i = 1; i <= n; i++) {
if (a[i] != a[i - 1]) c.push_back(a[i]);
}
for (int i = 0; i < (int)c.size(); i++)
a[i + 1] = c[i];
return c.size();
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
memset(dp, 0x3f, sizeof(dp));
n = deluni();
// for (int i = 1; i <= n; i++) printf("%d ", a[i]);
// puts("");
for (int i = 1; i <= n; i++) dp[i][i] = 0;
for (int i = 1; i < n; i++) dp[i][i + 1] = (a[i] != a[i + 1]);
for (int d = 3; d <= n; d++) {
for (int i = 1; i + d - 1 <= n; i++) {
int j = i + d - 1;
// if (i == 2 && j == n) {
// debug(dp[i + 1][j]);
// debug(dp[i][j - 1]);
// debug(dp[i + 1][j - 1]);
// }
dp[i][j] = min(dp[i][j - 1], dp[i + 1][j]) + 1;
if (a[i] == a[j]) {
dp[i][j] = min(dp[i][j], dp[i + 1][j - 1] + 1);
}
}
}
printf("%d\n", dp[1][n]);
return 0;
}
I - Coloring Brackets
还没写出来!
J - Clear the String
题意简述:给定一个字符串 \(S\),每次可以删除一个所有字符都相等的连续子串,求最小删除次数使得这个串变为空串。
难度:1800
tag:DP
思路:就是上上一题要枚举一个 \(k\)。
时间复杂度:\(O(n^3)\)。
空间复杂度:\(O(n^2)\)。
#include <bits/stdc++.h>
using namespace std;
#define debug(x) cout<<#x<<' '<<x<<'\n'
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
constexpr int N = 500 + 7;
constexpr int P = 998244353;
int n;
char a[N];
int dp[N][N];
bool same[N][N];
int main() {
scanf("%d", &n);
scanf("%s", a + 1);
memset(dp, 0x3f, sizeof(dp));
// for (int i = 1; i <= n; i++) printf("%d ", a[i]);
// puts("");
for (int i = 1; i <= n; i++) dp[i][i] = 1;
for (int i = 1; i < n; i++)
dp[i][i + 1] = 1 + (a[i] != a[i + 1]), same[i][i + 1] = (a[i] == a[i + 1]);
for (int d = 3; d <= n; d++) {
for (int i = 1; i + d - 1 <= n; i++) {
int j = i + d - 1;
// if (i == 2 && j == n) {
// debug(dp[i + 1][j]);
// debug(dp[i][j - 1]);
// debug(dp[i + 1][j - 1]);
// }
if (a[i] == a[j]) {
dp[i][j] = min(dp[i][j - 1], dp[i + 1][j]);
}
for (int k = i; k < j; k++) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]);
}
}
}
// for (int l = 1; l <= n; l++) {
// for (int r = l; r <= n; r++) {
// printf("%d ", dp[l][r]);
// }
// puts("");
// }
printf("%d\n", dp[1][n]);
return 0;
}

浙公网安备 33010602011771号