区间DP(一)
还是刷了一些题,总结一下吧。
枚举顺序
AT_arc108_e
用 \(f_{l,r}\) 表示选择了 \(l,r\) 之后的期望长度。转移上枚举下一个数是谁,有方程:
拆开来有:
有一种妙的枚举顺序,左端点从大到小,右端点从小到大,这样一来就可以用树状数组来维护了。统计答案可以用哨兵元素,复杂度 \(O(N^2\log N)\)。
可以推广,也就是说我们的枚举顺序应该服务于我们的转移顺序。另一个经典的例子是 邮局,根据四边形不等式,\(c(x,y-1)\le c(x,y)\le c(x+1,y)\) 的,也就说我们希望 \((x+1,y),(x,y-1)\) 要先于 \((x,y)\) 计算,同时由于 DP 自己的需要 \((x-k,y-1)\) 也要先行计算,所以我们确定出了先按 \(y\) 从小到大,然后按 \(x\) 从大到小的枚举顺序,这样就可以满足各方的需求了。
P2339
一道应该挺经典的题目。从大区间往小区间转移即可,复杂度平方。
状态设计
这是这几道题做下来的最深的感悟。总结起来就是说,如果一个状态发现无法正确转移,那就说明缺点什么;至于缺点什么,可以从决策过程来思考。在区间 DP 中,缺的这个东西常常是值。然后以几道题为例子说一下。
一个结论是,如果确定了一个题是区间 DP,那么就可以通过数据规模来猜。\(1000\) 应该是决策很少的傻逼题或者数据结构等优化的巧妙题,\(300\) 应该就是常规的 \(O(N^3)\) 的 DP,\(100\) 大概就是要加点状态了,\(50\) 的话如果出题人神志正常的话那么肯定就要加个一两维状态了。
P3607
首先有个思路是把一个子序列翻转等同于选出一些位置对,当然了这些位置对需要形成一个套娃的关系,并交换对应位置对上的数。这很好理解。既然是套娃的关系,那么似乎就可以用区间 DP 来解决了。
朴素地用 \(f_{l,r}\) 代表区间 \([l,r]\) 的数已经翻转完毕了的答案。当前决策无非两种,交换 \(a_l,a_r\) 和不交换。然而发现两种决策似乎都没法计算贡献,毕竟你不知道中间那块究竟形成了一个怎样的不降序列,所以可以用最经济实惠的方式,也就是记录上下界,来定义状态。于是 \(f_{l,r,x,y}\) 代表区间 \([l,r]\) 形成了一个第一个数不小于 \(x\),最后一个数不大于 \(y\) 的最大答案。基础转移就是从同区间小值域,或者从同值域小区间转移,不基础的就是考虑 \(a_l\) 和 \(a_r\) 的贡献。此时问题就显得非常简单了,就没必要多说了。代码:
read(m);
for(int i=1;i<=m;i++){
read(a[i]);
for(int j=1;j<=a[i];j++)for(int k=a[i];k<N;k++)f[i][i][j][k]=1;
}
for(int len=1;len<m;len++)for(int l=1;l+len<=m;l++){
int r=l+len;
for(int y=1;y<N;y++)for(int x=y;x;x--){
check(f[l][r][x][y],f[l+1][r][x][y]+(a[l]==x));
check(f[l][r][x][y],f[l][r-1][x][y]+(a[r]==y));
check(f[l][r][x][y],f[l+1][r-1][x][y]+(a[l]==y)+(a[r]==x));
check(f[l][r][x][y],f[l][r][x+1][y]);
check(f[l][r][x][y],f[l][r][x][y-1]);
}
}
printf("%d\n",f[1][m][1][N-1]);
P4766
也是一个非常巧妙的题目。首先粗略地用 \(f_{l,r}\) 定义 \([l,r]\) 的答案,然后发现似乎没法合并,因为假如粗略的把外星人分成 \([l,k]\) 和 \([k+1,r]\) 两类分别消灭的话,那么那些时间横跨中点的外星人可能就会被消灭两次,显然不优。于是从合并的角度来看,会发现有一个外星人比较特殊,那就是这个区间中距离最远的那个。这个外星人会被消灭,并且被消灭时使用的代价肯定是它的距离。既然如此,我们不妨钦定一开始就消灭这个外星人,然后考虑剩下的外星人如何处理。枚举消灭外星人王的位置 \(t\),那么显然凡是生命值和 \(t\) 有交的外星人都会被消灭,于是剩下的都是在 \(t\) 之前死掉的和在 \(t\) 之后出生的。在这个时候可以修改一下定义,定义 \(f_{l,r}\) 表示消灭完全处在 \([l,r]\) 中的外星人需要的代价,就是顺着上面那种方式转移即可。
void solve(){
read(m);n=0;
for(int i=1;i<=m;i++){
read(a[i].l);read(a[i].r);read(a[i].d);
b[++n]=a[i].l,b[++n]=a[i].r;
}
sort(b+1,b+n+1);n=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=m;i++){
a[i].l=lower_bound(b+1,b+n+1,a[i].l)-b;
a[i].r=lower_bound(b+1,b+n+1,a[i].r)-b;
}
for(int len=0;len<n;len++){
for(int l=1;l+len<=n;l++){
int r=l+len,pl=0;f[l][r]=inf;
for(int i=1;i<=m;i++)
if(a[i].l>=l&&a[i].r<=r&&a[i].d>a[pl].d)pl=i;
if(pl==0){f[l][r]=0;continue;}
for(int i=a[pl].l;i<=a[pl].r;i++)
f[l][r]=min(f[l][r],f[l][i-1]+f[i+1][r]+a[pl].d);
}
}
printf("%d\n",f[1][n]);return;
}
P5336
也是一道非常好的区间 DP 题目。会发现直接做非常不好做,所以还是按照上面的那种思路,从决策的角度来设计状态。考虑对于一个区间,肯定是未卜先知地选定一个值域区间 \([x,y]\),把其它的数用最少的代价删掉,只保留其中的数。最后,用一个代价为 \(a+b(y-x)^2\) 的操作带走。然后开始思考如何做到保留特定的数,于是想到再加两维状态,用 \(f_{l,r,x,y}\) 代表把 \([l,r]\) 删的只剩下值在范围 \([x,y]\) 中的最少代价,同时定义 \(g_{l,r}\) 表示把区间删完的最少代价。\(f\) 到 \(g\) 有简单的转移,而 \(f\) 的转移,直接枚举区间的断点,左右两半分别有两种决策,即同样剩下 \([x,y]\) 和全部删完。复杂度 \(O(N^5)\)。注意初始化。
signed main(){
read(m);read(aa);read(bb);
for(int i=1;i<=m;i++)read(a[i]),b[i]=a[i];
sort(b+1,b+m+1);n=unique(b+1,b+m+1)-b-1;
memset(f,0x3f,sizeof(f));memset(g,0x3f,sizeof(g));
for(int i=1;i<=m;i++){
a[i]=lower_bound(b+1,b+n+1,a[i])-b;f[i][i]=aa;
for(int ll=1;ll<=a[i];ll++)for(int rr=a[i];rr<=n;rr++)g[i][i][ll][rr]=0;
}
for(int len=1;len<m;len++)for(int l=1;l+len<=m;l++){
int r=l+len,nowData=inf;
for(int sa=1;sa<=n;sa++)for(int sb=sa;sb<=n;sb++){
int nowVal=inf;
for(int i=l;i<r;i++)
check(nowVal,min(f[l][i],g[l][i][sa][sb])+min(f[i+1][r],g[i+1][r][sa][sb]));
g[l][r][sa][sb]=nowVal;
check(f[l][r],nowVal+(b[sb]-b[sa])*(b[sb]-b[sa])*bb+aa);
}
}
printf("%d\n",f[1][m]);return 0;
}