区间dp笔记
引:石子合并
设有 $N(N \le 300)$ 堆石子排成一排,其编号为 $1,2,3,\cdots,N$。每堆石子有一定的质量 $A_i\ (A_i \le 1000)$。现在要将这 $N$ 堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。合并时由于选择的顺序不同,合并的总代价也不相同。试找出一种合理的方法,使总的代价最小,并输出最小代价。
若最初的第$l$堆石子和第$r$堆石子被合并成一堆,则说明$l \sim r$之间的每堆石子也已经被合并,这样$l$和$r$才有可能相邻。因此,在任意时刻,任意一堆石子均可以用一个闭区间$[l,r]$来描述,表示这堆石子是由最初的第$l \sim r$堆石子合并而成的,其重量为$\sum_{i=l}^{r}A_i$。另外,一定存在一个整数$k(l \leq k < r)$,在这堆石子形成之前,先有第$l \sim k$堆石子(闭区间$[l,r]$)被合并成一堆,第$k+1 \sim r$堆石子(闭区间$[k+1,r]$)被合并成一堆,然后这两堆石子才合并成$[l,r]$。
对应到动态规划中,就意味着两个长度较小的区间上的信息向一个更长的区间发生了转移,划分点$k$就是转移的决策。自然地,应该把区间长度$len$作为$DP$的阶段。不过,区间长度可以由左端点和有端点表示出,即$len=r-l+1$。本着动态规划“进行选择最小的能覆盖状态空间的维度集合”的思想,可以只用从左、右端点表示$DP$的状态。
设$F[l,r]$表示把最初的第$l$堆到第$r$堆石子合并成一堆,需要的最少体力,根据上述分析,容易写出状态转移方程:
$F[l,r]=\min\limits_{l \leq k <r}{F[l,k]+F[k+1,r]}+\sum_{i=l}^{r}A_i$
初值:$\forall l\in[1,N],f[l,l]=0$,其余为正无穷。
目标:$F[1,N]$
我们发现$\sum_{i=l}^{r}A_i$可使用前缀和计算。
memset(f,0x3f,sizeof(f));
for(int i=1;i<=n;i++){
f[i][i]=0;
sum[i]=sum[i-1]+a[i];
}
for(int len=2;len<=n;len++){
for(int l=1,r=l+len-1;r<=n;l++,r++){
for(int k=l;k<r;k++)
f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]);
f[l][r]+=sum[r]-sum[l-1];
}
}
例1:能量项链(NOIP)
题目描述
在 Mars 星球上,每个 Mars 人都随身佩带着一串能量项链。在项链上有 $N$ 颗能量珠。能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样,通过吸盘(吸盘是 Mars 人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。如果前一颗能量珠的头标记为 $m$,尾标记为 $r$,后一颗能量珠的头标记为 $r$,尾标记为 $n$,则聚合后释放的能量为 $m \times r \times n$(Mars 单位),新产生的珠子的头标记为 $m$,尾标记为 $n$。
需要时,Mars 人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。
例如:设 $N=4$,$4$ 颗珠子的头标记与尾标记依次为 $(2,3)(3,5)(5,10)(10,2)$。我们用记号 $\oplus$ 表示两颗珠子的聚合操作,$(j \oplus k)$ 表示第 $j,k$ 两颗珠子聚合后所释放的能量。则第 $4$,$1$ 两颗珠子聚合后释放的能量为:
$(4 \oplus 1)=10 \times 2 \times 3=60$。
这一串项链可以得到最优值的一个聚合顺序所释放的总能量为:
$(((4 \oplus 1) \oplus 2) \oplus 3)=10 \times 2 \times 3+10 \times 3 \times 5+10 \times 5 \times 10=710$。
思路
考虑区间$DP$,这道题的状态设计有一丝特别,设$F[i][j]$表示合并闭区间$[i,i+j-1]$后得到的最大值,区间长度为$2$的单独处理。
很容易写出转移方程:
$F[i][j]=\max\limits_{l \leq k \leq r}{F[i,k]+F[i+k,j-k]+a[i]*a[i+j]*a[i+k]} $
现在唯一有点棘手的问题是本题是求环上的最大值,最朴素的做法就是枚举每个断点进行$DP$,这样时间复杂度为$O(n^4)$。结合以前处理环的方式可以想到再把序列复制一遍,区间$[i,i+n-1]$即为以$i$为断点的最大值,时间复杂度$O(n^3)$。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=410;
int n,a[N],f[N][N];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=n+1;i<=2*n;i++)a[i]=a[i-n];
for(int i=1;i<=2*n;i++)f[i][2]=a[i]*a[i+1]*a[i+2];
for(int j=3;j<=2*n;j++){
for(int i=1;i<=2*n;i++){
if(i+j-1>=2*n)break;
for(int k=1;k<j;k++){
f[i][j]=max(f[i][j],f[i][k]+f[i+k][j-k]+a[i]*a[i+j]*a[i+k]);
}
}
}
int ans=0;
for(int i=1;i<=2*n;i++)ans=max(ans,f[i][n]);
printf("%d\n",ans);
return 0;
}
例2:【USACO3.3.5】A Game游戏 IOI'96
题目描述
有如下一个双人游戏:$N(2 \le N \le 100)$个正整数的序列放在一个游戏平台上,游戏由玩家$1$开始,两人轮流从序列的任意一端取一个数,取数后该数字被去掉并累加到本玩家的得分中,当数取尽时,游戏结束。以最终得分多者为胜。
编一个执行最优策略的程序,最优策略就是使玩家在与最好的对手对弈时,能得到的在当前情况下最大的可能的总分的策略。你的程序要始终为第二位玩家执行最优策略。
思路
状态只有$2$种:从左边拿和从右边拿。
假设当前状态$a_1,a_2,a_3,a_4,a_5$,如果第一个人选最左边的,则问题转化为四个数$a_2,a_3,a_4,a_5$,然后第二个人先选,由于题目说第二个人方案也最优,所以选的也是最优方案,即$f[i+1][j]$;先选右边同理。
$f[i][j]$表示$i \sim j$区间段第一个人选的最优方案。
所以$dp$转移方程为:
$f[i][j]= \max sum[i+1][j]-f[i+1][j]+a_i,sum[i][j-1]-f[i][j-1]+a_j $
$sum[i][j]$其实就等于$sum[1][j]-sum[1][i-1]$,于是我们用一个s数组,$s[i]$表示前$1\sim i$个数的和,就好了。But,你如果想偷懒直接$O(n^3)$预处理每个区间的和也是可以的,毕竟$n$最大才$100$。
所以$dp$转移方程也可写成$f[i][j]=\max s[j]-s[i-1]-f[i+1][j],s[j]-s[i-1]-f[i][j-1]$;
根据$dp$转移方程我们可以发现,要得到状态$f[i][j]$,必须要得到状态$f[i+1][j]$和$f[i][j-1]$。然后我们就可以写出程序了。
代码
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]),sum[i]=sum[i-1]+a[i],f[i][i]=a[i];
for(int len=1;len<n;len++){
for(int i=1;i+len<=n;i++){
int j=len+i;
f[i][j]=max(sum[j]-sum[i-1]-f[i+1][j],sum[j]-sum[i-1]-f[i][j-1]);
}
}
printf("%d %d\n",f[1][n],sum[n]-f[1][n]);
这道题妙在状态转移,本质上是类似于取反的关系,现在先手选,那么剩下的区间先手就变为后手,后手变为先手,以此类推。
例3:做错的作业
题目大意
给定一个长度为$n$括号序列,求最少添加多少个括号使得它为合法的括号序列?$n \le 300$
思路分析
此题虽然很简单,但它分析的思路成为了以后这种括号序列的套路,如果凭借自己的思考在高强度的考场上大概率是想不出来的。
很明显,对于原括号序列的每个括号最后它要么是单独添加一个括号,要么是与之前的括号进行匹配。
设状态$F[i][j]$表示区间$[i,j]$成为合法括号序列的最少需要添加的括号数量。第一种状态转移很简单$F[i,j]=F[i,j-1]+1$,可第二种转移怎么办呢?考虑这个区间被这对括号划分成了两个区间长度更小的部分,$[i,k-1]$与$[k+1,j-1]$两部分,而且我们发现这两个区间是不可能存在交集的,所以第二种转移就可以从$F[i,k-1]+F[k+1,j-1]$转移过来。
综上,转移方程为:
$F[i][j]=\text{max} \left\{ \begin{aligned} &\text{F[i][j-1]+1——不与前面区间括号进行匹配,单独添加一个括号与之匹配}\\ &\text{F[i][$k-1$]+$F[k+1][j-1]$ $k \in i\sim j$} ——\text{与前面区间括号进行匹配} \end{aligned} \right. $
代码
for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++)f[i][j]=9999999999;
for(int i=1;i<=n;i++)f[i][i]=1;
for(int len=1;len<n;len++){
for(int i=1;i+len<=n;i++){
int j=i+len;
f[i][j]=min(f[i][j],f[i][j-1]+1);
for(int k=i;k<=j-1;k++){
if((s[k]=='('&&s[j]==')')||(s[k]=='['&&s[j]==']')||(s[k]=='<'&&s[j]=='>')||(s[k]=='{'&&s[j]=='}')){
f[i][j]=min(f[i][j],f[i][k-1]+f[k+1][j-1]);
}
}
}
}
printf("%d\n",f[1][n]);
例4:【CQOI2007】涂色
题目描述
假设你有一条长度为 $5$ 的木板,初始时没有涂过任何颜色。你希望把它的 $5$ 个单位长度分别涂上红、绿、蓝、绿、红色,用一个长度为 $5$ 的字符串表示这个目标:$\texttt{RGBGR}$。
每次你可以把一段连续的木板涂成一个给定的颜色,后涂的颜色覆盖先涂的颜色。例如第一次把木板涂成 $\texttt{RRRRR}$,第二次涂成 $\texttt{RGGGR}$,第三次涂成 $\texttt{RGBGR}$,达到目标。
用尽量少的涂色次数达到目标。
思路
典型的区间$DP$模板,设$F[i,j]$表示区间$[i,j]$的最少达成目标的次数。
很容易写出状态转移方程:
$ F[i][j]=\text{min} \left\{ \begin{aligned} &\text{F[i][k]+F[k+1][j]}\\ &\text{min F[i+1][$j$],F[i][j-1]}(if a[i]==a[j])\text{} \end{aligned} \right. $
代码
for(int i=1;i<=n;i++)f[i][i]=1;
for(int len=1;len<n;len++){
for(int l=1;len+l<=n;l++){
int r=len+l;
if(a[l]==a[r])f[l][r]=min(f[l][r-1],f[l+1][r]);
for(int k=l;k<r;k++){
f[l][r]=min(f[l][k]+f[k+1][r],f[l][r]);
}
}
}
printf("%d\n",f[1][n]);
例5:凸多边形的三角剖分
题目描述
给定一具有$N$个顶点(按顺时针方向$1$到$N$编号)的凸多边形,每个顶点的权均已知。问如何把这个凸多边形划分成$N-2$个互不相交的三角形,使得这些三角形顶点的权的乘积之和最小?$N \le 50$
思路
考虑区间$DP$,
Part 1 状态设计
设状态$F[i,j]$表示从$(l,l+1),(l+1,l+2),....(r-1,r)$的划分的最小代价。
Part 2 状态转移
结合题目特性,我们发现只会用一种转移,就是合并两个更小的凸多边形,那么就写出状态转移方程。
$F[i][j]=\max\text{F[i][k]+F[k][j]} (k\in i \sim j-1)$
代码
注意开long long
#include<bits/stdc++.h>
using namespace std;
const int N=305;
int n,t;
struct node{
int x,y;
}a[N];
int f[N][N],dis[N][N];
int dist(int i,int j){
if(abs(j-i)==1)return 0;
return abs(a[i].x+a[j].x)*abs(a[i].y+a[j].y)%t;
}
int main(){
scanf("%d %d",&n,&t);
for(int i=1;i<=n;i++)scanf("%d %d",&a[i].x,&a[i].y);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)f[i][j]=0x3f3f3f3f3f3f;
if(i+1<=n)f[i][i+1]=0;
else f[i][1]=0;
}
for(int len=2;len<n;len++){
for(int i=1;i+len<=n;i++){
int j=i+len;
for(int k=i+1;k<j;k++){
f[i][j]=min(f[i][j],f[i][k]+f[k][j]+dist(i,k)+dist(j,k));
}
}
}
printf("%d\n",f[1][n]);
return 0;
}
想必到现在读者一定会对区间$DP$有更深的认识,下面来看一下更有深度的题目。
例6:字符串折叠
题目描述
折叠的定义如下:
-
一个字符串可以看成它自身的折叠。记作S =S
-
X(S)是X(X>1)个S连接在一起的串的折叠。记作X(S) = SSSS…S(X个S)。
-
如果A = A’, B=B’,则AB = A’B’
例如,因为3(A) = AAA, 2(B) = BB,所以3(A)C2(B) = AAACBB,而2(3(A)C)2(B)=AAACAAACBB给一个字符串,求它的最短折叠。 例如AAAAAAAAAABABABCCD的最短折叠为:9(A)3(AB)CCD。
思路
这道题也是一样先设状态$F[i,j]$表示表示区间$[i,j]$需要的最少长度。
考虑转移,第一种转移即为不加括号和数字,区间$[i,j]$可以分成两个更小的区间进行转移即$F[i][j]=\min F[i][k]+F[k+1][j](k \in i \sim j-1)$
第二种转移为加括号和数字,比如一段小区间$[i,k](k \in i \sim j-1)$它的区间长度为$k-i+1$,很显然它必须得整除原区间长度$j-i+1$,其次这段区间为原区间的循环节。只有在这个时候才可以发生转移。例如,此时若存在一个数$k$使得区间$[i,k]$为原区间$[i,j]$的循环节且区间长度整除原区间长度,那么$F[i][j]= \min F[i][j],F[i][k]+num[(j-i+1)/(k-i+1)]+2$即可。
Code
#include<bits/stdc++.h>
using namespace std;
const int N=200;
string s;
int f[N][N],num[N];
int main(){
cin>>s;
int n=s.length();
s=" "+s;
memset(f,0x3f,sizeof(f));
for(int i=1;i<=n;i++)f[i][i]=1;
for(int i=1;i<=9;i++)num[i]=1;
for(int i=10;i<=99;i++)num[i]=2;
num[100]=3;
for(int len=1;len<n;len++){
for(int i=1;i+len<=n;i++){
int j=i+len;
for(int k=i;k<j;k++){
f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
if((len+1)%(k-i+1)==0){
bool flag=true;
int lenn=k-i+1;
for(int h=i;h<=j;h++)
if(s[h]!=s[(h-i)%lenn+i]){
flag=false;
break;
}
if(flag)f[i][j]=min(f[i][j],2+num[(len+1)/(k-i+1)]+f[i][k]);
}
}
}
}
printf("%d\n",f[1][n]);
return 0;
}
例7:加分二叉树(NOIP)
思路
和前面状态设计差不多,设$F[l,r]$表示$[l,r]$之间的最多的得分,则每次枚举一个小区间和它的根节点,那么状态转移为$F[l,r]=F[l,k-1]*F[k+1,r]+a[k]$。
现在考虑怎么求具体方案,再开一个数组$G[l][r]$表示区间$[l,r]$的最优决策点,也就是根节点然后再分为两部分进行$dfs$。
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=35;
int f[N][N],n,a[N],root[N][N];
void print(int l,int r){
if(l>r)return;
printf("%lld ",root[l][r]);
if(l==r)return;
print(l,root[l][r]-1);print(root[l][r]+1,r);
}
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++)scanf("%lld",&a[i]),f[i][i]=a[i],root[i][i]=i;
for(int len=1;len<n;len++){
for(int i=1;i+len<=n;i++){
int j=i+len;
f[i][j]=f[i][i]+f[i+1][j];root[i][j]=i;
for(int k=i+1;k<j;k++){
if(f[i][j]<f[i][k-1]*f[k+1][j]+f[k][k]){
f[i][j]=f[i][k-1]*f[k+1][j]+f[k][k];
root[i][j]=k;
}
}
}
}
printf("%lld\n",f[1][n]);
print(1,n);
return 0;
}
例8:[SCOI2007] 压缩
思路
此题最大的问题是在有无$M$的情况,那么就设$F[i][j][0]$表示区间$[i,j]$没有$M$,$F[i][j][1]$表示区间$[i,j]$有$M$
现在考虑转移:
设$mid=(i+j)/2$
tip!
进行此种转移的前提必须是$(i+j)$能整除$2$
$F[i][j][0]=min(f[i][mid][0]+1)$
剩余是常规操作:$F[i][j][0]=min(F[i][k][0]+j-k)$
$F[i][j][1]=min(min(F[i][k][0]+F[i][k][1])+min(F[k+1][j][0],F[k+1][j][1])+1)$
#include<bits/stdc++.h>
using namespace std;
const int N=55;
int f[N][N][2];
char a[N];
int n;
bool check(int l,int r){
int len=(r-l+1)>>1;
if((r-l+1)%2==1)return false;
for(int i=l,j=l+len;i<l+len;i++,j++){
if(a[i]!=a[j])return false;
}
return true;
}
int main(){
scanf("%s",(a+1));
n=strlen((a+1));
for(int i=1;i<=n;i++){
f[i][i][0]=1;
f[i][i][1]=2;
}
for(int len=2;len<=n;len++){
for(int i=1,j=i+len-1;j<=n;i++,j++){
f[i][j][0]=f[i][j][1]=0x3f3f3f3f;
if(check(i,j)){
int mid=(i+j)>>1;
f[i][j][0]=min(f[i][j][0],f[i][mid][0]+1);
}
for(int k=i;k<j;k++){
f[i][j][0]=min(f[i][j][0],f[i][k][0]+j-k);
f[i][j][1]=min(f[i][j][1],min(f[i][k][1],f[i][k][0])+min(f[k+1][j][0],f[k+1][j][1])+1);
}
}
}
printf("%d\n",min(f[1][n][0],f[1][n][1]));
return 0;
}
例9:【JXOI2018】守卫
可以证明,在一个区间中,无法被右端点看到的每一个点构成的区间一定是无法被这个区间外除右端点的右边一个端点之外的点看到
因此状态可以直接用$F[i][j]$表示覆盖区间 $[l,r]$ 所需的守卫,右端点必放,剩下的每个区间$ [i,j]$以 $f[i][j]$或$ f[i][j+1]$ 进行转移