定义:
区间dp思想是当我们求取某个区间的信息时,发现他可以由他的子区间(们)得到,那么,我们由子区间又发现可以推到子区间的子区间,最后推到初始状态。根据此思想来获取答案。
我们主要靠例题来介绍
一,区间合并类
这种典型的就是我们求取当前区间时,发现只有子区间通过某种代价/操作/条件就可以合并成它
例题一:来到入门经典:Problem - 5115 (hdu.edu.cn)
思路:
我们设dp[i][j]表示消灭完[i,j]区域的狼花费的最小代价,那么当我们消灭区间的最后一头狼时,是不是有
k是这个区间最后一头狼,显然他的攻击是a[k]+b[k-1]+b[k+1]
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
const int INF=0x3f3f3f3f;
const int N = 210;
int a[N],b[N],dp[N][N];
int32_t main()
{
int t, n, t1 = 0;
cin>>t;
while (t--)
{
cin>>n;
for (int i=1; i<=n; ++i)for (int j=i; j<=n; ++j)dp[i][j]=INF;
for (int i=1; i<=n; ++i)cin>>a[i];
for (int i=1; i<=n; ++i)cin>>b[i];
b[0]=b[n+1]=0;
for (int len=1; len<=n; ++len)for (int i=1; i+len-1<=n; ++i)//区间合并通常是枚举每一个长度len,求出每个长度的最优解,最后推出答案最优
{//这里从长度1开始,注意,dp[i][i]不一定==a[i],因为i左右可能有狼
int j=i+len-1;
for (int k=i; k<=j; ++k)dp[i][j]=min(dp[i][j],dp[i][k-1]+dp[k+1][j]+a[k]+b[i-1]+b[j+1]);
}
printf("Case #%lld: ", ++t1);
cout<<dp[1][n]<<endl;
}
}
例题二:Problem - 5900 (hdu.edu.cn)
思路:
- 我们还是用dp[i][j]表示这个区间可以获得的最大值,那么合并时,我们显然dp[i][j]可以由子区间合并,我们每次合并只考虑左右端是否不互质(不需要考虑内端而是外端是因为,内端其实就是两个相邻的点,那长度为2时,这两相邻点不就是外端吗,可以由它推出来,不用将情况复杂化)。
- 外端不互质时,我们再判断一下是不是中间的数都消去,怎么判断呢——区间和,然后如果中间全部消去就可以判断是不是外端两点可以互消。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
const int N = 310;
int dp[N][N];
int a[N],g[N],sum[N];
int32_t main()
{
int t,n;
cin>>t;
while (t--)
{
cin>>n;
for (int i=1; i<=n; ++i)cin>>g[i];
for (int i=1; i<=n; ++i)cin>>a[i],sum[i]=sum[i-1]+a[i];
memset(dp,0,sizeof(dp));
for (int len=2; len<=n; ++len)for (int i=1; i+len-1<=n; ++i)
{
int j=i+len-1;
for (int k=i; k<j; ++k)
{
dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]);
}
if (__gcd(g[i],g[j])!=1)//如果最外端不互质
{
if (len==2)dp[i][j]=a[i]+a[j];//特判长度为2
else if (dp[i+1][j-1]==sum[j-1]-sum[i])dp[i][j]=dp[i+1][j-1]+a[i]+a[j];
}
}
cout<<dp[1][n]<<endl;
}
return 0;
}
例题三:Problem - 5396 (hdu.edu.cn)
思路:
- 既然求所有方案数的值的和,那么我们设dp[i][j]表示区间所有方案数的值的和
- dp[i][j]的方案数数量显然是由它里面的运算符合决定的,所以,他的符号排列就是方案数,即A[j-i]
- 那么我们每次合并区间是直接加法吗?(屁话,怎么可能)
- 当两个区间中间的符号是+/-,显然,a与b合并,那么a里面每一个方案都需要加上b的所有方案,所以整个操作加b一共加了b*(a的方案数),加a同理,就是加了a*(b的方案数)。
- 如果中间符号是*,显然每个a*b的方案,那不就是直接a*b了
- 我们确定合并区间后,虽然左右区间各自内部的运算顺序已经决定,但是合并后还没决定,我可以先运算左区间几次,再跑到右区间运算几次,不断反复横跳。那么我们显然必须确定两个区间的相对运算顺序,换句话说,只要两个区间有一个区间的运算相对运算顺序被决定即可(即组合数,
,如j-i-1个运算符(因为中间的k不用确定,他是最后运算的)确定左区间k-i个运算符位置)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
const int INF=0x3f3f3f3f;
const int N = 110;
const int mod=1e9+7;
int C[N][N],A[N],dp[N][N];
int32_t main()
{
int n;
C[0][0]=1,A[0]=1;
for (int i=1; i<=100; ++i)//数据小,杨辉三角求组合数即可
{
C[i][0]=1;
for (int j=1; j<=i; ++j)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
for (int i=1; i<=100; ++i)A[i]=A[i-1]*i%mod;
while (cin>>n)
{
memset(dp,0,sizeof(dp));
string s;
for (int i=1; i<=n; ++i)cin>>dp[i][i];
cin>>s;
s=' '+s;
for (int len=2; len<=n; ++len)for (int i=1; i<=n; ++i)
{
int j=i+len-1;
for (int k=i; k<j; ++k)
{
if (s[k]=='+')
{
dp[i][j]=(dp[i][j]+(dp[i][k]*A[j-k-1]+dp[k+1][j]*A[k-i])%mod*C[j-i-1][k-i]%mod)%mod;
}
else if (s[k]=='-')
{
dp[i][j]=(dp[i][j]+(dp[i][k]*A[j-k-1]-dp[k+1][j]*A[k-i])%mod*C[j-i-1][k-i]%mod)%mod;
}
else
{
dp[i][j]=(dp[i][j]+dp[i][k]*dp[k+1][j]%mod*C[j-i-1][k-i]%mod)%mod;
}
}
}
dp[1][n]=(dp[1][n]+mod)%mod;//保证答案正数
cout<<dp[1][n]<<endl;
}
return 0;
}
二,子区间递推类(回文)
例题4:Problem - 4745 (hdu.edu.cn)
思路:
- 绕着圈走,不回头,每次两兔子点相同,如果我们吧他们踩的点抄下来会发现说明?回文串!
- 所以我们需要记录最长回文串(经典就是用区间递推的)
- 因为是个环,可以从尾部跳回头部,所以我们可以找两个区间合成最大答案
- 我们用dp[i][j]记录[i,j]区间最长回文串,那么是不是有
还有
。而如果a[i]==a[j],显然
- 注意,我们这个回文不需要连续(他是跳着的)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
const int INF=0x3f3f3f3f;
const int N = 1010;
int dp[N][N],a[N];//dp记录区间最长
int32_t main()
{
int n;
while (cin>>n&&n)
{
memset(dp,0,sizeof(dp));
for (int i=1; i<=n; ++i)cin>>a[i],dp[i][i]=1;
for (int len=2; len<=n; ++len)for (int i=1; i+len-1<=n; ++i)
{
int j=i+len-1;
dp[i][j]=max(dp[i][j-1],dp[i+1][j]);
if (a[i]==a[j])dp[i][j]=dp[i+1][j-1]+2;
}
ll ans=0;
for (int i=1; i<=n; ++i)ans=max(dp[1][i]+dp[i+1][n],ans);
cout<<ans<<endl;
}
return 0;
}
例题5:Problem - 5617 (hdu.edu.cn)
思路:
- 我们可以从起点与终点推到中间的反对角线。(由对称性)
- 因为数据500,我们开3维(的500)会爆炸,更不用说4维。首先,我们在走了i不的情况下,知道上面与下面各自的x坐标就可以推出对应的y坐标,又因为dp转移性,我们可以开个滚动数组
- 最后答案就是反对角线是点的和
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
const int INF=0x3f3f3f3f;
const int N = 510;
const int mod=5201314;
int dp[3][N][N];//第一维用于滚动转换
char a[N][N];
int32_t main()
{
int t,n;
cin>>t;
while (t--)
{
cin>>n;
memset(dp,0,sizeof(dp));
for (int i=1; i<=n; ++i)for (int j=1; j<=n; ++j)cin>>a[i][j];
if (a[1][1]==a[n][n])dp[0][1][n]=1;
int p=1;//通过p为0,1进行转移
for (int i=1; i<n; ++i)//走n-1步到副对角线
{
memset(dp[p],0,sizeof(dp[p]));//每次清空之前转移后留下的数据再更新它,这样就代替了i
for (int j=1; j<=n&&i+1-j>=0; ++j)for (int k=n; k+i>=n&&k>=1; --k)
{
if (a[j][i+2-j]==a[k][2*n-i-k])dp[p][j][k]=(dp[p][j][k]+dp[p^1][j-1][k]+dp[p^1][j][k]+dp[p^1][j][k+1]+dp[p^1][j-1][k+1])%mod;//如果当前两点相等
}
p^=1;
}
p^=1;
ll ans=0;
for (int i=1; i<=n; ++i)
ans=(ans+dp[p][i][i])%mod;
cout<<ans<<endl;
}
return 0;
}