区间DP
概念
区间类型动态规划是线性动态规划的拓展,它在分阶段划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系。(例:f[i][j]=f[i][k]+f[k+1][j])
区间类动态规划的特点:
- 合并:即将两个或多个部分进行整合。
- 特征:能将问题分解成为两两合并的形式。
- 求解:对整个问题设最优值,枚举合并点,将问题分解成为左右两个部分,最后将左右两个部分的最优值进行合并得到原问题的最优值。
题目描述
在一个圆形操场的四周摆放 N 堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出一个算法,计算出将 N 堆石子合并成 1 堆的最小得分和最大得分。
思路:
首先这是一个环形结构,我们需要把它转换为线性结构来处理会比较方便,所以我们需要把线性长度增加一倍,使得首位可以相连。
for(int i=1;i<=n;i++)
scanf("%d", &a[i]),a[n+i]=a[i],s[i]=s[i-1]+a[i];
这样我们就可以在线性结构上解决问题,这题是一道区间DP的模板题。
对应到动态规划中,就是两个长度较小的区间上的信息向一个更长的区间发生了转移,划分点k就是转移的决策,区间长度就是DP的阶段。根据动态规划“选择最小的能覆盖状态空间的维度集合”的思想,可以只用左、右端点表示DP的状态。
dp[i][j]表示区间从i到j的最小合并值;
dpp[i][j]表示区间从i到j的最大合并值;
s[i]表示前缀和;
通过枚举断点k来找出每个区间最优值再合并;
状态转移方程:
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
dpp[i][j]=max(dpp[i][j],dpp[i][k]+dpp[k+1][j]+s[j]-s[i-1]);
完整代码:
#include<bits/stdc++.h>
using namespace std;
int n,a[3005],s[3005],dp[3005][3005],dpp[3005][3005];
int main()
{
scanf("%d", &n);
for(int i=1;i<=n;i++)//断环成链
scanf("%d", &a[i]),a[n+i]=a[i],s[i]=s[i-1]+a[i];
for(int i=n+1;i<=2*n;i++)//前缀和
s[i]=s[i-1]+a[i];
for(int l=2;l<=n;l++)//区间长度
{
for(int i=1;l+i-1<=2*n;i++)//左端点
{
int j=l+i-1;//右端点
dp[i][j]=1e8;
dpp[i][j]=-1e8;
for(int k=i;k<j;k++)//断点k
{
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
dpp[i][j]=max(dpp[i][j],dpp[i][k]+dpp[k+1][j]+s[j]-s[i-1]);
}
}
}
int maxx=-1e8,minn=1e8;
for(int i=1;i<=n;i++)//遍历找最大最小值
{
maxx=max(maxx,dpp[i][i+n-1]);
minn=min(minn,dp[i][i+n-1]);
}
printf("%d\n%d",minn,maxx);
return 0;
}
#include<bits/stdc++.h>
using namespace std;
int n,maxx,a[300],dp[300][300];
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i],a[i+n]=a[i];
}
for(int i=2;i<2*n;i++)
{
for(int j=i-1;j>=1&&i-j<n;j--)
{
for(int k=j;k<i;k++)
{
//转移方程 = max(原来能量,左边能量 +右边能量 +将两区间合并的能量);
//两区间合并能量=左区间第一个珠子*右区间第一个珠子*总区间后面一个珠子;
dp[j][i]=max(dp[j][i],dp[j][k]+dp[k+1][i]+a[i+1]*a[k+1]*a[j]);
maxx=max(dp[j][i],maxx);
}
}
}
cout<<maxx;
return 0;
}
思路:
考虑每个人进队只能从左边或者右边进来,而且第i个人进来只需要与上一个人比较,所以这个人的状态可以由上一个人的状态转移而来。
对于每次比较,上一个人的状态也是在区间最左边或者最右边。
所以我们用dp[i][j][0]和dp[i][j][1]表示区间状态即方法数,0和1则表示第i个人从左边进来和从右边进来。
接下来我们具体考虑,用a[i]表示输入的身高,我们在区间 [i,j] 中对第i个人分析;
第i个人从最左边进来,那么与前一个人比较,前一个人要么在 i+1 这个位置要么在 j 这个位置(对区间[i+1,j]分析);
那么 a[i] 分别与 a[i+1]、a[j] 比较,如果小于,说明可以加进来,就有 dp[i][j][0]+=dp[i+1][j][0](前一个人左边进来) 、 dp[i][j][0]+=dp[i+1][j][1](前一个人右边进来);
那么第i个人从最右边进来(对区间[i,j-1]分析)
那么a[i] 分别与 a[i]、a[j-1] 比较,如果大于,说明可以加进来,就有 dp[i][j][1]+=dp[i][j-1][0](前一个人左边进来) 、 dp[i][j][0]+=dp[i][j-1][1](前一个人右边进来);
对于初始化dp数组,即i==j,dp[i][i][0]=1;注意只需要0或者1时等于1就可以了,因为一个人的时候方案只有一种,默认左边或者右边,不可以两个都初始化。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=19650827;
int dp[2005][2005][2],a[2005];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
dp[i][i][0]=1;
for(int len=1;len<=n;len++)
{
for(int i=1,j=i+len;j<=n;i++,j++)
{
if(a[i]<a[i+1]) dp[i][j][0]+=dp[i+1][j][0];
if(a[i]<a[j]) dp[i][j][0]+=dp[i+1][j][1];
if(a[j]>a[i]) dp[i][j][1]+=dp[i][j-1][0];
if(a[j]>a[j-1]) dp[i][j][1]+=dp[i][j-1][1];
dp[i][j][0]%=mod;
dp[i][j][1]%=mod;
}
}
cout<<(dp[1][n][0]+dp[1][n][1])%mod;
return 0;
}
最少插入几个变成回文串。
#include<iostream>
#include<cstring>
using namespace std;
int n;
char a[5005];
short dp[5005][5005];
int main()
{
cin>>n;
memset(dp,0x3f,sizeof dp);
for(int i=1;i<=n;i++)
{
cin>>a[i];
dp[i][i]=0;
}
for(int l=2;l<=n;l++)
{
for(int i=1;i+l-1<=n;i++)
{
int j=i+l-1;
if(a[i]==a[j])
{
if(l==2) dp[i][j]=0;
else dp[i][j]=dp[i+1][j-1];
}
else
{
dp[i][j]=min(dp[i+1][j],dp[i][j-1])+1;
}
}
}
cout<<dp[1][n];
return 0;
}
关灯问题
三维数组 0停左边 1停右边。
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int INF=0x7f7f7f7f;
int n,a[10005],b[10005];
int dp[2][10005][2];
int main()
{
while(~scanf("%d",&n))
{
for(int i=1;i<=n;i++)
{
scanf("%d%d",&a[i],&b[i]);
dp[1][i][0]=dp[0][i][1]=0;
}
int cur=0;
for(int i=n;i>=1;i--)
{
for(int j=i+1;j<=n;j++)
{
dp[cur][j][0]=min(dp[cur^1][j][0]+a[i+1]-a[i],dp[cur^1][j][1]+a[j]-a[i]);
if(dp[cur][j][0]>=b[i]) dp[cur][j][0]=INF;
dp[cur][j][1]=min(dp[cur][j-1][0]+a[j]-a[i],dp[cur][j-1][1]+a[j]-a[j-1]);
if(dp[cur][j][1]>=b[j]) dp[cur][j][1]=INF;
}
cur^=1;
}
int ans=min(dp[cur^1][n][0],dp[cur^1][n][1]);
if(ans==INF)
cout<<"No solution"<<endl;
else
cout<<ans<<endl;
}
return 0;
}
给定括号序列,问最少添加几个括号成为规定序列并输出。
#include<iostream>
#include<cstring>
using namespace std;
char a[105];
int dp[105][105],c[105][105];
void print(int i,int j)
{
if(i>j) return ;
if(i==j)
{
if( a[i] == '(' || a[i] == ')' ) printf("()");
else printf("[]");
return;
}
if( c[i][j] >= 0 )
{
print( i,c[i][j] );
print( c[i][j]+1,j );
}
else
{
if( a[i] == '(' )
{
printf("(");
print(i+1,j-1);
printf(")");
}
else
{
printf("[");
print(i+1,j-1);
printf("]");
}
}
}
int main()
{
cin>>a+1;
int len=strlen(a+1);
memset(dp,0x3f,sizeof(dp));
memset(c,-1,sizeof(c));
for(int i=1; i<=len; i++)
dp[i][i] = 1, dp[i][i-1] = 0;
for(int l=1;l<=len;l++)
{
for(int i=1;i+l<=len;i++)
{
int j=i+l;
for(int k=i;k<j;k++)
{
if(dp[i][j]>dp[i][k]+dp[k+1][j])
{
dp[i][j]=dp[i][k]+dp[k+1][j];
c[i][j]=k;
}
if( (a[i] == '(' && a[j] == ')' || a[i] == '[' && a[j] == ']') && dp[i][j] > dp[i+1][j-1] )
{
dp[i][j] = dp[i+1][j-1];
c[i][j] = -1;
}
}
}
}
print( 1,len );
printf("\n");
return 0;
}
总结:
基本特征:将问题分解成为两两合并的形式。
解决方法:对整个问题设最优值,枚举合并点,将问题分解成为左右两个部分,再将左右两个部分的最优值进行合并得到原问题的最优值。
设i到j的最优值,枚举剖分(合并)点,将(i,j)分成左右两区间,分别求左右两边最优值,如下图:

状态转移方程的一般形式如下:

(偷来的)
浙公网安备 33010602011771号