区间dp
区间 \(dp\) 通常解决的问题顺序有着很重要的作用,也就是说“前 \(i\) 个”的状态并不能很好地刻画出完整的状态
状态一般设计为 \(f[l][r]\),表示 \(dp\) 完 \([l,r]\) 的区间的花费
后面经常补充状态,比如 \([0/1]\),\([k]\) 来表示上一次取的是左/右端点,左右端点的状态,或者区间后面的一段情况,或者区间内数具有的性质
从中间转移
即每次扩展的是一个区间,用于解决合并类的问题
最典型的有石子合并
再补充一些题目:
发现同样是一个合并类问题,那么 \(f[i][j]=f[i][k]+1 (f[i][k]==f[k][j] )\)
\(f[l][r][0/1/2][0/1/2]\) 表示左右端点颜色分别为 \(0/1/2\) 的方案数
可以看左右端点是否是匹配括号,是则 \(dp\) \([l+1,r-1]\),否则 \(dp\) \([l,k]\) 和 \([k+1,r]\)
这道题的一个启发点是对于这种括号匹配的问题,往往写成递归版的记忆化搜索会更好实现
\(f[l][r]=f[l+1][k-1]+f[k+1][r-1]+a[l]* a[k]* a[r]\),表示 \(l,k,r\) 三个点形成了三角形并产生贡献
并不想讲这个题,来补充一个 道听途说未曾实现不保证正确 的小 \(trick\)
这种括号匹配经常有算重的问题,那么对于每一个 \(l\) 把 \(r\) 从小到大枚举据说即可解决这个问题(理论上分析比较有道理)
被 \(Cyber_Tree\) 教了一晚上
关键条件是转化关系是 \(1\) 对多的,那么设 \(f[l][r][c]\) 表示 \(T\) 串区间 \([l,r]\) 转化为字母 \(c\) 的最优方案
那么对于多步转移,由于越来越少(\(1\) 对 \(1\) 需要特判),不会转移成环
实际上就是枚举局部最大值的位置,可以发现这个是不能计数的,应该放在状态里
形式化地,为经过 \(i\) 位置的所有区间最大值均在 \(i\)
很显然有多个,那么钦定为第一个
那么转移从 \([l,k)\) 和 \((k,r]\) 转移
其中左边一个区间可以发现需要迎合 \(k\) 是最大值这个限制
那么需要保证 \(k'\) 和 \(k\) 管辖区间无交,这个可以对每个 \((l,r,k)\) 预处理出来
注意右边是没有这个限制的,因为定义了是最小的一个,而这个最大值并不唯一
发现左边的转移是一个区间,那么维护一个前缀和即可
从两侧转移
POJ 3280 Cheapest Palindrome
题意:给你长度为m的字符串,其中有n种字符,每种字符都有两个值,分别是插入这个字符的代价,删除这个字符的代价,让你求将原先给出的那串字符变成一个回文串的最小代价。
这道题的思想还是很妙的
分情况讨论:
- 如果 \(l,r\) 相等,从 \(f[l+1,r-1]\) 转移
- 在前面插入 \(r\) 或后面删除 \(r\),从 \(f[l,r-1]\) 转移
- 在后面插入 \(l\) 或前面删除 \(l\),从 \(f[l+1,r]\) 转移
很妙的题,当时不以为然,AT 又出来发现不会
分两个数组 \(f,g\) 分别维护小明和小华取完后的状态
那么 \(f[l,r]=max(f[l+1,r]+a[l],f[l,r-1]+a[r])\)
\(g\) 的转移同理
古老的题了,设 \(f[l][r][0/1]\) 表示区间 \([l,r]\),最后一次老王在左/右端点的最优值
维护后缀信息
设 \(f[l][r][s]\) 表示区间 \([l,r]\),后面紧接着有 \(s\) 个与 \(r\) 相同的方块
这样的状态使得一种转移迎刃而解:即选取中间一个点 \(k\),其中 \(k\) 与 \(r\) 颜色相同,砍掉 \([k+1,r-1]\) 的部分,让 \(r\) 以及后面的 \(s\) 个与 \(k\) 拼上
那么转移有两个:\(f[l][r][s]=f[l][r-1][0]+(s+a[r])^2\)
\(f[l][r][s]=f[l][k][a[r]+s]+f[k+1][r-1][0]\)
- 这启示我们把转移不了的东西放进状态里
代码
#include<bits/stdc++.h>
using namespace std;
int read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=x*10+ch-48;
ch=getchar();
}
return x*f;
}
const int maxn=205;
const int inf=0x3f3f3f3f;
int n,col[maxn],a[maxn],f[maxn][maxn][maxn*2],sum[maxn];
int main(){
n=read();
for(int i=1;i<=n;i++)col[i]=read();
for(int i=1;i<=n;i++)a[i]=read(),sum[i]=sum[i-1]+a[i];
// for(int i=1;i<=n;i++)f[i][i][0]=a[i]*a[i];
for(int len=0;len<=n-1;len++){
for(int l=1;l<=n-len;l++){
int r=l+len;
for(int s=0;s<=sum[n]-sum[r-1];s++){
f[l][r][s]=f[l][r-1][0]+(s+a[r])*(s+a[r]);
for(int k=l;k<=r-1;k++){
if(col[k]==col[r])
f[l][r][s]=max(f[l][r][s],f[l][k][a[r]+s]+f[k+1][r-1][0]);
}
}
}
}
cout<<f[1][n][0];
return 0;
}
发现既然一次分发只和最值有关,那么只记录区间最值即可
设 \(g[l][r][mn][mx]\) 表示 \([l,r]\) 中最值分别为 \(mn\) 和 \(mx\) 的最优情况
设 \(f[l][r]\) 表示区间 \([l,r]\) 的最优解
那么 \(f[l][r]=min{g[l][r][a][b]+B*(b-a)^2+A}\)
考虑枚举断点 \(k\) 来转移 \(g\)
\(g[l][r][a][b]=min(f[k+1][r]+g[l][k][a][b])\)
\(g[l][r][a][b]->g[l][r+1][min(a,w_{r+1})][max(b,w_{r+1})]\)
于是离散化后用 \(n^5\) 的优秀复杂度完成了此题
代码
#include<bits/stdc++.h>
using namespace std;
int read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=x*10+ch-48;
ch=getchar();
}
return x*f;
}
const int maxn=55;
int n,m,aa,b,a[maxn],lsh[maxn],tot,f[maxn][maxn],g[maxn][maxn][maxn][maxn];
void placemin(int &a,int b){
if(a>b)a=b;
return ;
}
int main(){
n=read(),aa=read(),b=read();
for(int i=1;i<=n;i++)lsh[i]=a[i]=read();
sort(lsh+1,lsh+n+1);
tot=unique(lsh+1,lsh+n+1)-lsh-1;
for(int i=1;i<=n;i++)a[i]=lower_bound(lsh+1,lsh+tot+1,a[i])-lsh;
memset(g,0x3f,sizeof g),memset(f,0x3f,sizeof f);
for(int i=1;i<=n;i++)g[i][i][a[i]][a[i]]=0;
for(int len=0;len<=n-1;len++){
for(int l=1;l<=n-len;l++){
int r=l+len;
for(int mn=1;mn<=tot;mn++){
for(int mx=mn;mx<=tot;mx++){
for(int k=l;k<r;k++){
placemin(g[l][r][mn][mx],g[l][k][mn][mx]+f[k+1][r]);
}
placemin(g[l][r+1][min(mn,a[r+1])][max(mx,a[r+1])],g[l][r][mn][mx]);
placemin(f[l][r],g[l][r][mn][mx]+aa+b*(lsh[mx]-lsh[mn])*(lsh[mx]-lsh[mn]));
}
}
}
}
cout<<f[1][n];
return 0;
}
这是区间 \(dp\) 放到二维平面上的例子
设 \(f[l][r][i][j][k]\) 表示顶点为 \((l,r)\) 与 \((i,j)\) 的矩形分割了 \(k\) 次的最小值
每次只扩展横纵坐标之一
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=16;
int n,a[maxn][maxn],sum[maxn][maxn],f[maxn][maxn][maxn][maxn][maxn];
void tomin(int &a,int b){
if(a>b)a=b;
return ;
}
int main(){
cin>>n;
for(int i=1;i<=8;i++){
for(int j=1;j<=8;j++){
cin>>a[i][j];
sum[i][j]=sum[i-1][j]+sum[i][j-1]+a[i][j]-sum[i-1][j-1];
}
}
memset(f,0x3f,sizeof f);
for(int i=1;i<=8;i++){
for(int j=1;j<=8;j++){
for(int l=i;l<=8;l++){
for(int r=j;r<=8;r++){
int w=sum[l][r]-sum[i-1][r]-sum[l][j-1]+sum[i-1][j-1];
f[i][j][l][r][0]=w*w;
}
}
}
}
for(int k=1;k<=n;k++){
for(int i=1;i<=8;i++){
for(int j=1;j<=8;j++){
for(int l=1;l<=8;l++){
for(int r=1;r<=8;r++){
for(int a=i;a<l;a++){
tomin(f[i][j][l][r][k],min(f[i][j][a][r][0]+f[a+1][j][l][r][k-1],f[i][j][a][r][k-1]+f[a+1][j][l][r][0]));
}
for(int b=j;b<r;b++){
tomin(f[i][j][l][r][k],min(f[i][j][l][b][0]+f[i][b+1][l][r][k-1],f[i][j][l][b][k-1]+f[i][b+1][l][r][0]));
}
}
}
}
}
}
cout<<f[1][1][8][8][n-1];
return 0;
}
根据题目的限制,不能更改数据值,也就是说这棵 \(BST\) 的中序遍历是一定了,要做的是通过改变权值来旋转
设 \(f[l][r][s]\) 表示中序 \([l,r]\) 的节点,值 \(\ge s\) 的最小值
那么枚举一个树根 \(k\),转移分情况讨论:
如果 \(k\) 的值大于等于 \(s\),那么 \(f[l][r][s]=f[l][k-1][a[k].val]+f[k+1][j][a[k].val]+sum(i,j)\)
否则,\(f[l][r][s]=f[l][k-1][s]+f[k+1][r][s]+sum(i,j)+K\)
代码
#include<bits/stdc++.h>
using namespace std;
int read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=x*10+ch-48;
ch=getchar();
}
return x*f;
}
const int maxn=75;
const int inf=1e9;
int n,m,f[maxn][maxn][maxn],sum[maxn],lsh[maxn];
struct Node{
int val,val1,cost;
}a[maxn];
bool cmp(Node a,Node b){
return a.val<b.val;
}
int main(){
n=read();m=read();
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)for(int k=1;k<=n;k++)if(j!=i-1)f[i][j][k]=inf;
for(int i=1;i<=n;i++)a[i].val=read();
for(int i=1;i<=n;i++)lsh[i]=a[i].val1=read();
for(int i=1;i<=n;i++)a[i].cost=read();
sort(a+1,a+n+1,cmp);
sort(lsh+1,lsh+n+1);
for(int i=1;i<=n;i++)a[i].val1=lower_bound(lsh+1,lsh+n+1,a[i].val1)-lsh;
for(int i=1;i<=n;i++)sum[i]=sum[i-1]+a[i].cost;
// for(int i=1;i<=n;i++){
// for(int j=a[i].val1;j<=n;j++)f[i][i][j]=a[i].cost;
// for(int j=1;j<a[i].val1;j++)f[i][i][j]=a[i].cost+m;
// }
for(int len=0;len<=n-1;len++){
for(int l=1;l<=n-len;l++){
int r=l+len;
for(int s=1;s<=n;s++){
for(int k=l;k<=r;k++){
if(a[k].val1>=s)f[l][r][s]=min(f[l][r][s],f[l][k-1][a[k].val1]+f[k+1][r][a[k].val1]+sum[r]-sum[l-1]);
f[l][r][s]=min(f[l][r][s],f[l][k-1][s]+f[k+1][r][s]+sum[r]-sum[l-1]+m);
}
}
}
}
cout<<f[1][n][1];
return 0;
}
题意:每次可以删除串中的一个 \(S\),最少剩下多长
其意义在于这种消除的模式并不能贪心,其具有区间合并性
而 \(dp\) 状态相当于也是维护了后缀信息
详见 这里
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=205;
int t,n,lenans,nxt[maxn],cnt,sum[30],sum1[30];
bool f[maxn][maxn];
char a[maxn],b[maxn],c[maxn],ans[maxn];
void solve(int l,int r){
int len=r-l+1;
for(int i=l;i<=r;i++)b[i-l+1]=a[i];
memset(f,0,sizeof f);
for(int i=1;i<=n;i++){
f[i][i-1]=true;
}
for(int len1=0;len1<=n-1;len1++){
for(int l=1;l<=n-len1;l++){
int r=l+len1;
f[l][r]|=f[l][r-1]&(a[r]==b[len1%len+1]);
for(int k=1;k*len<=len1;k++){
f[l][r]|=f[l][r-k*len]&f[r-k*len+1][r];
if(f[l][r])break;
}
// cout<<l<<" "<<r<<" "<<f[l][r]<<endl;
}
}
if(f[1][n]){
lenans=len;
for(int i=1;i<=len;i++)ans[i]=b[i];
}
return ;
}
bool cmp(int len,int l,int r){
if(!len)return true;
for(int i=l;i<=r;i++){
if(a[i]!=ans[i-l+1])return a[i]<ans[i-l+1];
}
return false;
}
bool check(int l,int r){
memset(sum1,0,sizeof sum1);
int base=n/(r-l+1);
for(int i=l;i<=r;i++){
sum1[a[i]-'a']++;
}
// cout<<"hhh"<<endl;
for(int i=0;i<26;i++){
if(sum1[i]*base!=sum[i])return false;
}
return true;
}
int main(){
freopen("string.in","r",stdin);
freopen("string.out","w",stdout);
cin>>t;
while(t--){
memset(sum,0,sizeof sum);
scanf("%s",a+1);
n=strlen(a+1);lenans=0;
for(int i=1;i<=n;i++){
sum[a[i]-'a']++;
}
for(int len=0;len<=n-1;len++){
// cout<<n<<" "<<len+1<<endl;
if(n%(len+1))continue;
// cout<"hhh";
for(int l=1;l<=n-len;l++){
int r=l+len;
if(cmp(lenans,l,r)){
// cout<<"hhh";
if(check(l,r))solve(l,r);
}
}
if(lenans)break;
}
for(int i=1;i<=lenans;i++){
printf("%c",ans[i]);
}
puts("");
}
return 0;
}
- 资料
- 咕咕咕
区间 \(dp\) 与四边形不等式连接紧密,继续往后咕