区间DP入门练习题的奇幻之旅
关于区间 \(\text{DP}\) 的那些事
区间 \(\text{DP}\) 是线性 \(\text{DP}\) 的扩展,类似分治思想。它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系。
区间 \(\text{DP}\) 有以下特点:
-
合并:即将两个或多个部分进行整合,当然也可以反过来;
-
求解:主要有两种,第一种是枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值。第二种是先手动初始化小区间(如长度为 \(1,2\) 的区间)的答案,然后扩展左右端点来得到大区间的答案。
因此,在区间 \(\text{DP}\) 的循环中经常出现从小到大枚举区间长度来转移,这是为了让大区间能够从小区间转移得到。
[NOI 1995] 石子合并
\(n\) 堆石子环形摆放,要将石子有次序地合并成一堆,规定每次只能选相邻的两堆合并成新的一堆,得分为新合并成的那一堆的石子个数。求出将这 \(n\) 堆石子合并成一堆的最小得分和最大得分。
以最大得分为例,首先考虑直线型的摆放。令 \(\text{dp}_{i,j}\) 表示将 \([i,j]\) 中的所有堆合并到一起的最大得分,状态转移方程如下:
至于环形摆放,只需复制一份首尾相连,形成长度为 \(2n\) 的序列即可,最后答案取 \(\max\limits_{i=1}^n \text{dp}_{i,i+n-1}\)。
经由前缀和处理后,可以做到 \(O(1)\) 转移,总复杂度为 \(O(n^3)\)。
for (len = 2; len <= n; len++)
for (i = 1; i + len - 1 <= 2 * n; i++) {
int j = i + len - 1;
for (k = i; k < j; k++)
f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]);
}
for (int i = 1; i <= n; i++) ans = max(ans, dp[i][i + n - 1]);
实际上本题可以通过四边形不等式优化为 \(O(n^2)\)。
[NOIP 2006 提高组] 能量项链
项链上有 \(n\) 颗珠子,每颗珠子有一个二元标记 \((a,b)\),满足对于任意相邻的珠子 \(i\) 和 \(i+1\),有 \(b_i=a_{i+1}\)。相邻两颗珠子 \((n,r)\) 和 \((r,m)\) 可以聚合,释放出 \(n \times r \times m\) 的能量。求出将所有珠子聚合成一颗珠子能够释放的能量最大值。
输入为每颗珠子的 \(a\) 标记 \(a_1,a_2, \cdots,a_n\)。
与上一题是极其相似的。同样破环成链,令 \(\text{dp}_{i,j}\) 表示将 \([i,j]\) 中的所有珠子聚合到一起后释放的最大能量,状态转移方程如下:
总复杂度 \(O(n^3)\)。
for(int len = 3; len <= n + 1; len++)
for(int i = 1; i + len - 1 <= 2 * n; i++){
int j = i + len - 1;
for(int k = i + 1; k < j; k++)
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + a[i] * a[k] * a[j]);
}
for(int i = 1; i <= n; i++) ans = max(ans, dp[i][i + n]);
[IOI 1998] Polygon
给定一个 \(n\) 个顶点的环,每个顶点 \(V\) 有一个权值 \(f(V) \in [-2^{15},2^{15})\),每条边 \(E\) 有一个属性 \(g(E)\in\{t,x\}\),分别代表“相加”和“相乘”。每次操作可以选定一条边 \(E\),将它和它连接的两个顶点 \(V_1,V_2\) 缩成一个新的顶点 \(V\),满足 \(f(V)=\begin{cases} f(V_1)+f(V_2) & [g(E)=t] \\ f(V_1) \times f(V_2) & [g(E)=x] \end{cases}\) 并且与原来 \(V_1,V_2\) 的邻居相连。在经过 \(n\) 次操作后,环将缩成一个顶点 \(V_\text{last}\)。求出 \(f(V_\text{last})\) 的最大值,并且求出在能达到这个最大值的条件下,第一次缩点可以删掉哪条边(显然答案不唯一,因此升序输出所有满足条件的边的编号)。
这题与前两题类似,做法也是万变不离其宗,还是先破环成链,然后枚举区间分割点,合并两个子区间来转移。只不过这题有一些细节需要注意,那就是值域包括了负数——两个极小负数相乘可以得到极大正数。因此我们转移最大值的同时,还得转移最小值。
设 \(X_{i,j},N_{i,j}\) 分别为区间 \([i,j]\) 最大值和最小值,转移方程如下:
- \(g(E)=t \begin{cases} X_{i,j} \leftarrow \max\{X_{i,j}, \max\limits_{k=i}^{j-1}(X_{i,k}+X_{k+1,j})\} \\ N_{i,j} \leftarrow \min\{N_{i,j}, \min\limits_{k=i}^{j-1}(N_{i,k}+N_{k+1,j})\} \end{cases}\)
- \(g(E)=x \begin{cases} X_{i,j} \leftarrow \max\{X_{i,j}, \max\limits_{k=i}^{j-1}\{X_{i,k}X_{k+1,j}, X_{i,k}N_{k+1,j},N_{i,k}X_{k+1,j},N_{i,k}N_{k+1,j}\}\} \\ N_{i,j} \leftarrow \min\{N_{i,j}, \min\limits_{k=i}^{j-1}\{X_{i,k}X_{k+1,j}, X_{i,k}N_{k+1,j},N_{i,k}X_{k+1,j},N_{i,k}N_{k+1,j}\}\} \end{cases}\)
接下来考虑第一次删掉哪条边能达到最大值——实际上这个问题巧妙地被“破环成链”的操作顺带解决了。我们在进行链上动态规划的时候,其实是从第二次缩点开始考虑的!而第一次缩点,实际上就是破环成链的那一下——我们把一条边给断开了,将环变成了链,而实际上这与直接缩点的效果是相同的。因此,我们只需要找到那些满足 \(X_{i,i+n-1}=ans\) 的边 \(E_i\) 就是满足题目条件的边。
此题的破环成链操作,乃一石二鸟!
时间复杂度:\(O(n^3)\)。
for(int i = 1; i <= 2 * n; i++) mx[i][i] = mn[i][i] = a[i];
for(int i = 1; i < 2 * n; i++) mx[i][i+1] = mn[i][i+1] = sgn[i+1] ? a[i] * a[i+1] : a[i] + a[i+1];
for (int len = 3; len <= n; len++)
for (int i = 1; i + len - 1 <= 2 * n; i++) {
int j = i + len - 1;
for (int k = i; k < j; k++){
if(sgn[k+1]){
mx[i][j] = max({mx[i][j], mx[i][k]*mx[k+1][j], mx[i][k]*mn[k+1][j], mn[i][k]*mx[k+1][j], mn[i][k]*mn[k+1][j]});
mn[i][j] = min({mn[i][j], mx[i][k]*mx[k+1][j], mx[i][k]*mn[k+1][j], mn[i][k]*mx[k+1][j], mn[i][k]*mn[k+1][j]});
}
else{
mx[i][j] = max(mx[i][j], mx[i][k]+mx[k+1][j]);
mn[i][j] = min(mn[i][j], mn[i][k]+mn[k+1][j]);
}
}
}
for (int i = 1; i <= n; i++) ans = max(ans, mx[i][i + n - 1]);
printf("%d\n", ans);
for (int i = 1; i <= n; i++) if(mx[i][i + n - 1] == ans) printf("%d ", i);
[USACO16OPEN] 248 G
给定一个序列, 每次操作可以将相邻且相同的两个数替换成比原来大 \(1\) 的一个数(例如相邻的两个 \(7\) 可以替换成一个 \(8\)),求出最大化的序列最大值。
设 \(\text{dp}_{i,j,0/1}\) 表示区间 \([i,j]\) 的答案,布尔参数表示这个区间是否被集替成了单独一个数。
首先根据定义初始化所有长度为 \(1,2\) 的区间的答案,从长度为 \(3\) 的区间开始,枚举划分点 \(k\) 来转移。
如果没有被集替成一个数,那么就像线段树那样转移最大值就好了。
如果满足 \(\text{dp}_{i,k,1}=\text{dp}_{k+1,j,1}\),那么说明两个子区间都可以集替成一个数,且这个数相等,那么就可以再次集替至当前的大区间。
复杂度 \(O(n^3)\)。
for(int i = 1; i <= n; i++) dp[i][i][0] = dp[i][i][1] = a[i];
for(int i = 1; i < n; i++) {
if(a[i] == a[i+1]) dp[i][i+1][1] = a[i] + 1;
dp[i][i+1][0] = max(a[i], a[i+1]);
}
for(int len = 3; len <= n; len++)
for(int i = 1; i + len - 1 <= n; i++){
int j = i + len - 1;
for(int k = i; k < j; k++){
dp[i][j][0] = max({dp[i][j][0], max(dp[i][k][0], dp[i][k][1]), max(dp[k+1][j][0], dp[k+1][j][1])});
if(dp[i][k][1] == dp[k+1][j][1]) dp[i][j][1] = max(dp[i][j][1], dp[i][k][1] + 1);
}
}
printf("%d", max(dp[1][n][0], dp[1][n][1]));
[IOI 2000] 邮局
数轴上有 \(n\) 个村庄,需要设置 \(m\) 个邮局,使得每个村庄到与之最近的邮局距离之和最小,求出这个最小值。约定村庄和邮局都位于整数点上,两点距离为坐标之差的绝对值。
- 有一个性质:对于序列 \(a_1,a_2,\cdots,a_n\),满足 \(\sum\limits_{i=1}^n |p-a_i|\) 最小的 \(p\) 为序列中位数 \(a_\text{mid}\)。
根据上式,记 \(\text{val}_{l,r}=\sum\limits_{i=l}^r |a_{\text{mid}}-a_i|\),表示在编号 \([l,r]\) 的村庄之间设置一个邮局,这之中所有村庄到这个邮局的距离总和最小值,所有的 \(\text{val}\) 可以 \(O(n^3)\) 预处理出来。
实际上,对序列排序,然后用前缀和可以优化到 \(O(n^2)\):
这样对于单个区间的预处理是 \(O(1)\) 的。
设 \(\text{dp}_{i,j}\) 表示在前 \(i\) 个村庄中设置 \(j\) 个邮局的答案,则有转移:
总复杂度 \(O(n^2m)\)。
int calc(int l, int r){
int mid = (l + r) >> 1;
int res = (mid - l + 1) * a[mid] - (sum[mid] - sum[l - 1]) + sum[r] - sum[mid] - (r - mid) * a[mid];
return res;
}
signed main() {
n = read(), m = read();
for(int i = 1; i <= n; i++) a[i] = read();
sort(a + 1, a + n + 1);
for(int i = 1; i <= n; i++) sum[i] = sum[i - 1] + a[i];
for(int i = 1; i <= n - 1; i++)
for(int j = i + 1; j <= n; j++)
val[i][j] = calc(i, j);
memset(dp, 0x3f, sizeof(dp));
dp[0][0] = 0;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= i; j++)
for(int k = 1; k <= min(m, i); k++)
dp[i][k] = min(dp[i][k], dp[j - 1][k - 1] + val[j][i]);
printf("%d", dp[n][m]);
return 0;
}
实际上本题可以通过四边形不等式优化为 \(O(nm)\)。
[CQOI2007] 涂色
给定一块长度为 \(n\) 的木板,初始时木板上没有任何颜色,每次可以选定木板上的一段区域涂色(可视为字符序列的区间赋值)。给定一个长度为 \(n\) 字符串 \(S\),求出将木板涂成对应字符串样式的最小涂色次数。
- 性质:显然先考虑木板的左右边缘一定不劣。
设 \(\text{dp}_{i,j}\) 表示区间 \([i,j]\) 的答案(最小涂色次数),转移分为两种情况:
- \(S_i=S_j\),此时的转移是一个易错点。端点处的颜色相同了,所以只需要考虑中间的部分,所以转移是 \(\text{dp}_{i,j} \leftarrow \text{dp}_{i+1,j-1}+1\),对吗?我一开始就栽在这里了。我忽略了一个很明显的地方:如果 \(S[i:j]\) 的字符都是相同的,那么只需要一次区间涂色就可以,但是刚才写的转移得到的答案至少会是 \(2\),所以不对。正确的转移应该为: \(\text{dp}_{i,j} \leftarrow \text{dp}_{i+1,j}\) 或者 \(\text{dp}_{i,j-1}\),只将一侧的边缘考虑进转移的范围里,然后将另一侧边缘视为处理的“附加效果”即可。例如我们仅处理 \([i,j-1]\) 的时候也是从边缘开始考虑,可以理解为让最开始从 \(i\) 开始的涂色多延伸至 \(j\),并不会影响答案。
- \(S_i \neq S_j\),枚举划分点来转移即可:\(\text{dp}_{i,j} \leftarrow \min \{\text{dp}_{i,j},\max\limits_{k=i}^{j-1} \text{dp}_{i,k}+\text{dp}_{k+1,j}\}\)。
总复杂度 \(O(n^3)\)。
for(int i = 1; i <= n; i++) dp[i][i] = 1;
for(int i = 1; i < n; i++) dp[i][i + 1] = s[i] == s[i + 1] ? 1 : 2;
for(int len = 3; len <= n; len++)
for(int i = 1; i + len - 1 <= n; i++){
int j = i + len - 1;
dp[i][j] = INF;
if(s[i] == s[j]) dp[i][j] = min(dp[i][j - 1], dp[i + 1][j]);
else
for(int k = i; k < j; k++)
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]);
}
[HNOI2010] 合唱队
定义一个重排队列的规则:
-
从头到尾依次对每个人操作。
-
第一个人直接进入新队列。
-
对于第二个及之后的人,如果比上一个人矮则加入新队列的队头,反之加入新队列的队尾。
给定一个长度为 \(n\) 队列中所有人的身高(保证互不相同),求出它可以通过多少种队列重排得到。
设新队列中第 \(i\) 个人身高为 \(h_i\),考虑区间 \([i,j]\) 最后加入新队列的人是 \(i\) 还是 \(j\)。
设 \(\text{dp}_{i,j,k}\) 表示区间 \([i,j]\) 的答案,\(k=0\) 表示最后加入的是 \(i\),\(k=1\) 表示最后加入的是 \(j\)。有如下转移:
对于任意长度为 \(1\) 的区间,答案为 \(1\);对于任意长度为 \(2\) 的区间,满足前一个人身高比后一个人矮时为 \(1\),否则为 \(0\)。
复杂度:\(O(n^2)\)。
for(int i = 1; i <= n; i++) dp[i][i][0] = dp[i][i][1] = 1;
for(int i = 1; i < n; i++) dp[i][i + 1][0] = dp[i][i + 1][1] = a[i] < a[i + 1];
for(int len = 3; len <= n; len++) {
for(int i = 1; i + len - 1 <= n; i++){
int j = i + len - 1;
if(a[i] < a[i + 1]) dp[i][j][0] = (dp[i][j][0] + dp[i + 1][j][0]) % P;
if(a[i] < a[j]) dp[i][j][0] = (dp[i][j][0] + dp[i + 1][j][1]) % P;
if(a[j] > a[i]) dp[i][j][1] = (dp[i][j][1] + dp[i][j - 1][0]) % P;
if(a[j] > a[j - 1]) dp[i][j][1] = (dp[i][j][1] + dp[i][j - 1][1]) % P;
}
}
printf("%lld", (dp[1][n][0] + dp[1][n][1]) % P);