动态规划之区间DP
一、区间DP
区间 D P DP DP 是指在一段区间上进行的一系列动态规划。
对于区间 D P DP DP 这一类问题,我们需要计算区间 [ 1 , n ] [1,n] [1,n] 的答案,通常用一个二维数组 d p dp dp 表示,其中 d p [ x ] [ y ] dp[x][y] dp[x][y] 表示区间 [ x , y ] [x,y] [x,y] 。
有些题目, d p [ l ] [ r ] dp[l][r] dp[l][r] 由 d p [ l ] [ r − 1 ] dp[l][r−1] dp[l][r−1] 与 d p [ l + 1 ] [ r ] dp[l+1][r] dp[l+1][r] 推得;也有些题目,我们需要枚举区间 [ l , r ] [l,r] [l,r] 内的中间点,由两个子问题合并得到,也可以说 d p [ l ] [ r ] dp[l][r] dp[l][r] 由 d p [ l ] [ k ] dp[l][k] dp[l][k] 与 d p [ k + 1 ] [ r ] dp[k+1][r] dp[k+1][r] 推得,其中 l ⩽ k ⩽ r l \leqslant k\leqslant r l⩽k⩽r。
对于长度为 n n n 的区间 DP,我们可以先计算 [ 1 , 1 ] , [ 2 , 2 ] … [ n , n ] [1,1],[2,2]\dots[n,n] [1,1],[2,2]…[n,n] 的答案,再计算 [ 1 , 2 ] , [ 2 , 3 ] … [ n − 1 , n ] [1,2],[2,3]\ldots[n-1,n] [1,2],[2,3]…[n−1,n],以此类推,直到得到原问题的答案。
一般来说,区间 D P DP DP 的状态设计有两种:一种是左右端点,用 d p [ l ] [ r ] dp[l][r] dp[l][r] 表示 l ∼ r l\sim r l∼r 的最优解;一种是左端点和区间长度,用 d p [ l ] [ l e n ] dp[l][len] dp[l][len] 表示 l ∼ l + l e n − 1 l\sim l+len-1 l∼l+len−1 的最优解。
二、区间DP例题
1.洛谷P1880 [NOI1995]石子合并
题目大意
在一个圆形操场的四周摆放 N N N 堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出一个算法,计算出将 N N N 堆石子合并成 1 1 1 堆的最小得分和最大得分。
解析
首先,这种环形问题就应该先破环成链,即将环转换成链。例如,下图这种情况:
我们把它转换成下图情况:
转换好之后,就可以开始转移了。这里,我们就以最大值为例进行说明。
首先,设计状态。我们还是用传统的区间 D P DP DP 的状态设计方式, 用 d p [ l ] [ r ] dp[l][r] dp[l][r] 表示 l ∼ r l\sim r l∼r 的最大分数。自然而然,我们就可以轻轻松松地得出 d p [ i ] [ i ] = a [ i ] dp[i][i]=a[i] dp[i][i]=a[i] 。同时,最终的答案就是 m a x 1 ⩽ i ⩽ n d p [ i ] [ i + n − 1 ] max_{1\leqslant i\leqslant n}\,dp[i][i+n-1] max1⩽i⩽ndp[i][i+n−1]
其次,设计状态转移方程。我们想,要合并 l ∼ r l\sim r l∼r 的所有石子,就可以把它分成两个部分,把两个部分分别合成一堆。我们假设要合并的是 [ l , k ] [l,k] [l,k] 和 [ k + 1 , r ] [k+1,r] [k+1,r] 两堆。那么,首先得到这两堆前,必须要用 d p [ l ] [ k ] dp[l][k] dp[l][k] 和 d p [ k + 1 ] [ r ] dp[k+1][r] dp[k+1][r] 的代价。其次,合并在一起,还要花 a [ l ] + a [ l + 1 ] + ⋯ + a [ r ] a[l]+a[l+1]+\dots+a[r] a[l]+a[l+1]+⋯+a[r] 的力气。所以,我们可以得到如下转移方程: d p [ l ] [ r ] = m a x l ⩽ k < r ( d p [ l ] [ k ] + d p [ k + 1 ] [ r ] ) + a [ l ] + a [ l + 1 ] + ⋯ + a [ r ] dp[l][r]=max_{l\leqslant k < r}(dp[l][k]+dp[k+1][r])+a[l]+a[l+1]+\dots+a[r] dp[l][r]=maxl⩽k<r(dp[l][k]+dp[k+1][r])+a[l]+a[l+1]+⋯+a[r]。其中, a [ l ] + a [ l + 1 ] + ⋯ + a [ r ] a[l]+a[l+1]+\dots+a[r] a[l]+a[l+1]+⋯+a[r] 可以用前缀和优化。
现在,这道题基本就完成了,上代码!
代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
int s = 0, w = 1;
char ch = getchar();
for(; ch < '0' || ch > '9'; w *= (ch == '-') ? -1 : 1, ch = getchar());
for(; ch >= '0' && ch <= '9'; s = 10 * s + ch - '0', ch = getchar());
return s * w;
}
const int MAXN = 205;
int n, pre[MAXN], a[MAXN], dp[MAXN][MAXN]; //dp[i][j] => i~j
signed main(){
n = read();
for(int i = 1; i <= n; i++){
a[i] = read();
a[i + n] = a[i];
}
for(int i = 1; i <= 2 * n; i++){
pre[i] = pre[i - 1] + a[i];
}
memset(dp, 0x3f3f3f3f, sizeof dp);
for(int i = 1; i <= 2 * n; i++){
dp[i][i] = 0;
}
for(int l = 2; l <= 2 * n; l++){
for(int i = 1; i + l - 1 <= 2 * n; i++){
int j = i + l - 1;
for(int k = i; k < j; k++){
dp[i][j] = min(dp[i][k] + dp[k + 1][j], dp[i][j]);
}
dp[i][j] += pre[j] - pre[i - 1];
}
}
int ans_min = 0x3f3f3f3f;
for(int i = 1; i <= n; i++){
ans_min = min(ans_min, max(0ll, dp[i][i + n - 1]));
}
cout << ans_min << endl;
memset(dp, 0, sizeof dp);
for(int l = 2; l <= 2 * n; l++){
for(int i = 1; i + l - 1 <= 2 * n; i++){
int j = i + l - 1;
for(int k = i; k < j; k++){
dp[i][j] = max(dp[i][k] + dp[k + 1][j], dp[i][j]);
}
dp[i][j] = dp[i][j] + pre[j] - pre[i - 1];
}
}
int ans_max = 0;
for(int i = 1; i <= n; i++){
ans_max = max(ans_max, dp[i][i + n - 1]);
}
cout << ans_max << endl;
return 0;
}
2.洛谷P4170 [CQOI2007]涂色
题目大意
假设你有一条长度为 5 5 5 的木板,初始时没有涂过任何颜色。你希望把它的 5 5 5 个单位长度分别涂上红、绿、蓝、绿、红色,用一个长度为 5 5 5 的字符串表示这个目标: RGBGR \texttt{RGBGR} RGBGR。
每次你可以把一段连续的木板涂成一个给定的颜色,后涂的颜色覆盖先涂的颜色。例如第一次把木板涂成 RRRRR \texttt{RRRRR} RRRRR,第二次涂成 RGGGR \texttt{RGGGR} RGGGR,第三次涂成 RGBGR \texttt{RGBGR} RGBGR,达到目标。
用尽量少的涂色次数达到目标。
解析
这道题的转移方法与上一题有所不同。我们要想的是: d p [ l ] [ r ] dp[l][r] dp[l][r] 会怎么得来?假如两端相等时, d p [ i ] [ j ] dp[i][j] dp[i][j] 的取值应该是 d p [ i + 1 ] [ j ] dp[i+1][j] dp[i+1][j] 和 d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j−1] 中的最小值。两段相等的话,还有一种染色方法,就是 d p [ i + 1 ] [ j − 1 ] dp[i+1][j-1] dp[i+1][j−1] ,相当于两端都染,这时, d p [ i ] [ j ] dp[i][j] dp[i][j] 还要更新一下最小值。假如两端不相等,那就跟上一题一样,枚举中间端点进行转移。
代码
#include <iostream>
#include <string>
#include <algorithm>
#include <cstring>
using namespace std;
const int inf = 0x3f3f3f3f;
int dp[55][55];
int main() {
string s;
cin >> s;
memset(dp, 0x3f, sizeof(dp));
for(int i = 0; i < s.size(); i++){
dp[i][i] = 1;
}
for(int l = 2; l <= s.size(); l++){
for(int i = 0; i < s.size() - l + 1; i++){
int j = i + l - 1;
if(s[i] == s[j]){
if(l == 2){
dp[i][j] = 1;
} else {
dp[i][j] = min(min(dp[i + 1][j], dp[i][j - 1]), dp[i + 1][j - 1] + 1);
}
} else {
for(int k = i; k < j; k++){
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]);
}
}
}
}
cout << dp[0][s.size() - 1] << endl;
return 0;
}
三、总结与反思
区 间 D P { 概 念 − 在 一 段 区 间 上 进 行 的 一 系 列 动 态 规 划 状 态 设 计 { ① 左 右 端 点 : d p [ l ] [ r ] 表 示 l ∼ r 的 最 优 解 ② 左 端 点 和 长 度 : d p [ l ] [ l e n ] ( 少 见 ) 状 态 转 移 { ① 枚 举 中 间 端 点 k , 通 过 d p [ l ] [ k ] 与 d p [ k + 1 ] [ r ] 转 移 ② 通 过 d p [ l ] [ r − 1 ] 与 d p [ l + 1 ] [ r ] 转 移 例 题 分 析 { 1. 石 子 合 并 , 转 移 方 法 : ① 2. 涂 色 , 转 移 方 法 : ① 和 ② 区间DP\begin{cases}概念-在一段区间上进行的一系列动态规划\\状态设计\begin{cases} ①左右端点:dp[l][r]表示l\sim r的最优解\\②左端点和长度:dp[l][len](少见)\end{cases}\\状态转移\begin{cases} ①枚举中间端点k,通过dp[l][k]与dp[k+1][r]转移\\②通过dp[l][r-1]与dp[l+1][r]转移\end{cases}\\例题分析\begin{cases} 1.石子合并,转移方法:①\\2.涂色,转移方法:①和② \end{cases}\\ \end{cases} 区间DP⎩⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎧概念−在一段区间上进行的一系列动态规划状态设计{①左右端点:dp[l][r]表示l∼r的最优解②左端点和长度:dp[l][len](少见)状态转移{①枚举中间端点k,通过dp[l][k]与dp[k+1][r]转移②通过dp[l][r−1]与dp[l+1][r]转移例题分析{1.石子合并,转移方法:①2.涂色,转移方法:①和②