区间dp(区间动态规划)

定义:

区间dp思想是当我们求取某个区间的信息时,发现他可以由他的子区间(们)得到,那么,我们由子区间又发现可以推到子区间的子区间,最后推到初始状态。根据此思想来获取答案。

我们主要靠例题来介绍

一,区间合并类

这种典型的就是我们求取当前区间时,发现只有子区间通过某种代价/操作/条件就可以合并成它

例题一:来到入门经典:Problem - 5115 (hdu.edu.cn)

思路:

我们设dp[i][j]表示消灭完[i,j]区域的狼花费的最小代价,那么当我们消灭区间的最后一头狼时,是不是有dp[i][j]=min(dp[i][j],dp[i][k-1]+a[k]+b[k-1]+b[k+1]+dp[k+1][j])(i<=k<=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)

思路:

  1. 我们还是用dp[i][j]表示这个区间可以获得的最大值,那么合并时,我们显然dp[i][j]可以由子区间合并,我们每次合并只考虑左右端是否不互质(不需要考虑内端而是外端是因为,内端其实就是两个相邻的点,那长度为2时,这两相邻点不就是外端吗,可以由它推出来,不用将情况复杂化)。
  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)

 思路:

  1. 既然求所有方案数的值的和,那么我们设dp[i][j]表示区间所有方案数的值的和
  2. dp[i][j]的方案数数量显然是由它里面的运算符合决定的,所以,他的符号排列就是方案数,即A[j-i]
  3. 那么我们每次合并区间是直接加法吗?(屁话,怎么可能)
    1. 当两个区间中间的符号是+/-,显然,a与b合并,那么a里面每一个方案都需要加上b的所有方案,所以整个操作加b一共加了b*(a的方案数),加a同理,就是加了a*(b的方案数)。
    2. 如果中间符号是*,显然每个a*b的方案,那不就是直接a*b了
  4. 我们确定合并区间后,虽然左右区间各自内部的运算顺序已经决定,但是合并后还没决定,我可以先运算左区间几次,再跑到右区间运算几次,不断反复横跳。那么我们显然必须确定两个区间的相对运算顺序,换句话说,只要两个区间有一个区间的运算相对运算顺序被决定即可(即组合数,_{k-i}^{j-i-1}\textrm{C},如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)


思路:

  1. 绕着圈走,不回头,每次两兔子点相同,如果我们吧他们踩的点抄下来会发现说明?回文串!
  2. 所以我们需要记录最长回文串(经典就是用区间递推的)
  3. 因为是个环,可以从尾部跳回头部,所以我们可以找两个区间合成最大答案
  4. 我们用dp[i][j]记录[i,j]区间最长回文串,那么是不是有dp[i][j]>=dp[i+1][j]还有dp[i][j]>=dp[i][j-1]。而如果a[i]==a[j],显然dp[i][j]=dp[i+1][j-1]+2
  5. 注意,我们这个回文不需要连续(他是跳着的)
#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)

 思路:

  1. 我们可以从起点与终点推到中间的反对角线。(由对称性)
  2. 因为数据500,我们开3维(的500)会爆炸,更不用说4维。首先,我们在走了i不的情况下,知道上面与下面各自的x坐标就可以推出对应的y坐标,又因为dp转移性,我们可以开个滚动数组
  3. 最后答案就是反对角线是点的和
#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;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值