区间DP总结与DP技巧汇总
1简介
区间DP是一类模板DP,事实上,模板DP的状态设计非常有限,基本上有一定的套路。所以模板DP的难处在于判断出这是某模板DP,以及在模板的基础上进行扩展。
基本上,状态\(f_{lr}\)是区间DP基本状态。下面对区间DP题目进行总结。
2题目
2.1石子合并
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ll long long
#define ull unsigned long long
#define N 101
#define M number
using namespace std;
int n,a[N*2];
ll f[2][N*2][N*2];
ll sum[N*2];//0->max 1->min
const ll INF=0x3f3f3f3f;
inline ll Max(ll a,ll b){
return a>b?a:b;
}
inline ll Min(ll a,ll b){
return a>b?b:a;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
a[i+n]=a[i];
}
for(int i=1;i<=2*n;i++) sum[i]=sum[i-1]+a[i];
memset(f[0],0,sizeof(f[0]));
memset(f[1],INF,sizeof(f[1]));
for(int i=1;i<=2*n;i++) f[0][i][i]=f[1][i][i]=0;
for(int j=2;j<=n;j++){
for(int i=1;i<=2*n-j+1;i++){
int r=i+j-1;
for(int k=i;k<=r-1;k++){
// printf("%lld %d %d %d\n",f[0][2][2],i,r,k);
f[0][i][r]=Max(f[0][i][r],f[0][i][k]+f[0][k+1][r]+sum[r]-sum[i-1]);
// printf("%lld %d %d %d\n",f[0][2][2],i,r,k);
f[1][i][r]=Min(f[1][i][r],f[1][i][k]+f[1][k+1][r]+sum[r]-sum[i-1]);
}
// printf("%d %d %lld\n__________\n",i,r,f[0][i][r]);
}
}
ll maxx=-1,minn=INF;
for(int i=1;i<=n;i++){
maxx=Max(maxx,f[0][i][i+n-1]);
minn=Min(minn,f[1][i][i+n-1]);
}
printf("%lld\n%lld",minn,maxx);
return 0;
}
这是最规矩的区间DP,主要思路在于对于一个区间,通过枚举断点进行转移,区间DP的特性是其DP顺序,即从短的区间开始更新,因此,第一个循环枚举的是区间长度,第二个是左端点,第三个是右端点。
以及,在处理环状时,通常是在后面在接上若干串,对每个长度为n的区间进行DP,取最值
2.2括号匹配
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ll long long
#define ull unsigned long long
#define N 51
#define M number
using namespace std;
int f[N][N];
char s[N];
const int INF=0x3f3f3f3f;
inline int Min(int a,int b){
return a>b?b:a;
}
int main(){
scanf("%s",s+1);
int len=strlen(s+1);
memset(f,INF,sizeof(f));
for(int i=1;i<=len;i++) f[i][i]=1;
for(int i=2;i<=len;i++){
for(int j=1;j<=len-i+1;j++){//l
int l=j,r=j+i-1;
if(s[l]==s[r]) f[l][r]=Min(f[l][r-1],f[l+1][r]);
else{//f[l][r]=Min(f[l][r-1]+1,f[l+1][r]+1);
for(int k=l;k<=r;k++) f[l][r]=Min(f[l][r],f[l][k]+f[k+1][r]);
}
}
}
printf("%d\n",f[1][len]);
}
这个题是在转移上有创新,其创新在于,如果新加的节点和右边能够匹配,那么方案数加1,否则,枚举断点,两种转移方式。
2.3 P1043 [NOIP2003 普及组] 数字游戏
先吐槽这竟然是普及组的题?
这道题在初始化上非常麻烦,练了练我的初始化水平。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ll long long
#define ull unsigned long long
#define N 101
#define M 10
using namespace std;
const int INF=0x3f3f;
int n,m,a[N],sum[N];
int f[2][N][N][M];
inline int Max(int a,int b){
return a>b?a:b;
}
inline int Min(int a,int b){
return a>b?b:a;
}
int main(){
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),a[i+n]=a[i];
for(int i=1;i<=2*n;i++) sum[i]=sum[i-1]+a[i];
memset(f[1],INF,sizeof(f[1]));
// for(int i=1;i<=2*n;i++) f[0][i][i][1]=f[1][i][i][1]=(a[i]%10+10)%10;
for(int i=1;i<=2*n;i++){
for(int j=i;j<=2*n;j++){
f[1][i][j][1]=0;
for(int k=i;k<=j;k++){
f[0][i][j][1]+=a[k];
f[1][i][j][1]+=a[k];
}
f[0][i][j][1]=(f[0][i][j][1]%10+10)%10;
f[1][i][j][1]=(f[1][i][j][1]%10+10)%10;
}
}
// printf("spec%d++++++\n",f[1][2][2][1]);
for(int j=2;j<=n;j++){
for(int i=1;i<=2*n-j+1;i++){
int l=i,r=i+j-1;
for(int k=2;k<=m;k++){
for(int q=l+k-2;q<=r-1;q++){
f[0][l][r][k]=Max(f[0][l][r][k],f[0][l][q][k-1]*(((sum[r]-sum[q])%10+10)%10));
f[1][l][r][k]=Min(f[1][l][r][k],f[1][l][q][k-1]*(((sum[r]-sum[q])%10+10)%10));
// printf("change:%d %d %d %d\n",l,r,k,f[1][l][r][k]);
// printf("who?:%d %d %d %d %d\n",l,q,k-1,f[1][l][q][k-1],(((sum[r]-sum[q])%10+10)%10));
}
}
// printf("end:%d %d %d %d\n--------------------\n",l,r,m,f[1][l][r][m]);
}
}
int maxx=-1,minn=INF;
for(int i=1;i<=n;i++){
maxx=Max(maxx,f[0][i][i+n-1][m]);
minn=Min(minn,f[1][i][i+n-1][m]);
}
printf("%d\n%d\n",minn,maxx);
}
总结
DP的一般套路是:
1.设计状态,要注意一定要不重不漏,所有能影响到答案的数据都要包含到状态里面。
2.初始化,基本上是第一项
3.转移,要注意无后效性,面面俱到。
4.可以关注数据范围,有时候范围会给我们以提醒。
基本技巧:
1.状态设计:一个条件,一个维度
2.增加条件,增加维度,改变状态(无后效性)。
3.求方案数的一般思路是在开一个与之对应的数组。
4.改变顺序,
5.消除冗余状态,简化空间(乌龟棋)
6.循环顺序的界定由转移的顺序决定。
7.把转移方程写下来之后再去写代码。
8.用所学过数学模型进行分析。