动态规划算法零基础区间DP自学笔记

Posted karshey

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划算法零基础区间DP自学笔记相关的知识,希望对你有一定的参考价值。

一个初学者的笔记。

区间dp的两种写法和一般模板

迭代式&记忆化搜索
迭代式:

//第一维循环区间长度 第二维循环左端点(范围是右端点<=n) 
for(int len=1;len<=n;len++)
	for(int L=1;L+len-1<=n;L++)
		R=L+len-1;//求右端点 

或:
来自

for(int len = 1;len<=n;len++)//枚举长度
        for(int j = 1;j+len<=n+1;j++)//枚举起点,ends<=n
            int ends = j+len - 1;
            for(int i = j;i<ends;i++)//枚举分割点,更新小区间最优解
                dp[j][ends] = min(dp[j][ends],dp[j][i]+dp[i+1][ends]+something);
            
        
    

模板

AcWing 1068. 环形石子合并(环形区间dp)

看了视频,分析一下这道题的思路:
我们有n堆石子,假设n堆石子没有合并就是n个点:

两堆石子合并了之后就是两个点连成一条边:

那么合并完是这样的,形成了一个有一个缺口的环:

于是我们可以产生一个朴素的想法:枚举缺口
但是这样会超时O(n4)。

这里提出一种优化方法:取n条长度为n的链
取n条长度为n 的链,其实就是每两个点都缺口一次。将链画成2n形式的,如果要1 5之间断开,那其实就是1-5的链,想要1 2之间断开,那就是2-(下一个)1之间的链,以此类推。
时间复杂度O(n3).
这种方式可以做绝大多数的环形区间dp

代码:

#include<bits/stdc++.h>
using namespace std;
#define mem(a,x) memset(a,x,sizeof(a));
const int N=410;//两倍的n 
int n,a[N];
int dpmax[N][N],dpmin[N][N];
int sum[N];
int main()

	cin>>n;
	for(int i=1;i<=n;i++)
	
		cin>>a[i];
		a[i+n]=a[i];
	
	for(int i=1;i<=n*2;i++) sum[i]=sum[i-1]+a[i];
	
	mem(dpmax,-0x3f);
	mem(dpmin,0x3f);
	
	//dp
	for(int len=1;len<=n;len++)
		for(int l=1;l+len-1<=n*2;l++)
		
			int r=len+l-1;
			if(l==r) dpmax[l][r]=dpmin[l][r]=0;
			else
			
				for(int k=l;k<r;k++)//不能等于r 
				
					dpmax[l][r]=max(dpmax[l][r],dpmax[l][k]+dpmax[k+1][r]+sum[r]-sum[l-1]);
					dpmin[l][r]=min(dpmin[l][r],dpmin[l][k]+dpmin[k+1][r]+sum[r]-sum[l-1]);					
				
			
		
	
	int minn=0x3f3f3f3f,maxn=-0x3f3f3f3f;
	for(int i=1;i<=n;i++)
	
		minn=min(minn,dpmin[i][i+n-1]);
		maxn=max(maxn,dpmax[i][i+n-1]);
	
	cout<<minn<<endl<<maxn;
	return 0;

AcWing 320. 能量项链(环形区间dp)

看完这个大佬的题解就可以直接冲这道题了
一个跟这道题有点像的矩阵链相乘问题

#include<bits/stdc++.h>
using namespace std;
#define mem(a,x) memset(a,x,sizeof(a));
const int N=210;
int n,a[N];
int dp[N][N];
int main()

	cin>>n;
	for(int i=1;i<=n;i++)
	
		cin>>a[i];	
		a[i+n]=a[i];	
	
		
	mem(dp,0);
	
	//dp
	for(int len=2;len<=n+1;len++)
		for(int l=1;len+l-1<=2*n;l++)
					
			int r=l+len-1;
			if(len==2) dp[l][r]=0;
			else
			
				for(int k=l+1;k<r;k++)
				
					dp[l][r]=max(dp[l][r],dp[l][k]+dp[k][r]+a[l]*a[k]*a[r]);
				
						
		
	
	int ans=-1;
	for(int i=1;i<=n;i++)
	
		ans=max(ans,dp[i][i+n]);
	
	cout<<ans;
	return 0;

AcWing 479. 加分二叉树

经典题
y总的视频绝了,清晰明了。
y总本y的题解

#include<bits/stdc++.h>
using namespace std;
#define fir(i,a,n) for(int i=a;i<=n;i++)
typedef long long ll;
const int N=30+10;
int a[N],n;
int dp[N][N];//dp[i][j]表示[i,j]区间内最大的二叉树加分 
int root[N][N];//root[i][j]表示区间为[i,j]的根节点 

void print(int l,int r)

	if(l>r) return;
	cout<<root[l][r]<<" ";
	print(l,root[l][r]-1);
	print(root[l][r]+1,r);

int main()

	cin>>n;
	fir(i,1,n) cin>>a[i];
	
	for(int len=1;len<=n;len++)
	
		for(int l=1;l+len-1<=n;l++)
		
			int r=l+len-1;
			if(l==r)//叶子节点 
			
				dp[l][r]=a[l];
				root[l][r]=l;
			
			else //不是叶子节点,会有左/右子树 
			
				for(int k=l;k<=r;k++)//枚举[l,r]范围内的根节点 
				
					//可能会出现左/右子树其中一个为空的情况 
					int left=(k==l?1:dp[l][k-1]);
					int right=(k==r?1:dp[k+1][r]);
					
					int score=left*right+a[k];
					
					//严格大于才更新,则在相同情况下是字典序的 
					if(score>dp[l][r])
					
						dp[l][r]=score;
						root[l][r]=k;
					
				
			
		
	
	
	cout<<dp[1][n]<<endl;
	
	print(1,n);
	return 0;

洛谷P3205 [HNOI2010]合唱队


题解看的是:zhaohaikun’s blog

要注意:

  1. dp[i][j]表示理想队形为[i,j]区间的个数,[0]表示放到左边,[1]表示放到右边。一共三维
  2. 初始化:dp[i][i]表示的是只有一个人的时候的方案数,只有一个人i,则之前没有别的人,那么到左边和到右边都是一样的,方案数是1,不能初始化两次(即同时初始化[0]和[1])
  3. 进来的人放在左边的话,就是i,比前面的人小;前面的人有两种放法,分别是i+1或j;之前的区间[i,j]就会变成[i+1,j],放在右边同理
  4. 输出的结果是左右加载一起的结果

代码:

#include<bits/stdc++.h>
using namespace std;
#define fir(i,a,n) for(int i=a;i<=n;i++)
typedef long long ll;
const int N=1e3+10;
const int MOD=19650827;
int a[N],n;
int dp[N][N][2];//dp[i][j]表示理想队形为[i,j]区间的个数  
//[0]表示放到左边,[1]表示放到右边 
int main()

	cin>>n;
	fir(i,1,n) cin>>a[i];
	
	//初始化
	fir(i,1,n) 
	
		dp[i][i][0]=1; 
	//	dp[i][i][1]=1;  
	//只有一个人的时候方案只有一种,所以只能一个1 
	//不管是左1还是右1都可以 但是只能是一个 
	
	
	for(int len=1;len<=n;len++)//枚举区间长度
	
		for(int l=1;l+len-1<=n;l++)//枚举左端点,注意右端点不能出界
		
			int r=l+len-1;//右端点 
			
			//放到左边就是比前一个数小的两种情况:
			// 前一个数在左边 即i+1 
			// 前一个数在右边 即r
			if(a[l]<a[l+1]) dp[l][r][0]+=dp[l+1][r][0];
			if(a[l]<a[r]) dp[l][r][0]+=dp[l+1][r][1]; 
			
			//同理,放在右边就是比前一个数字大:
			//前一个数字在最左边l 或右边r-1
			if(a[r]>a[l]) dp[l][r][1]+=dp[l][r-1][0];
			if(a[r]>a[r-1]) dp[l][r][1]+=dp[l][r-1][1]; 
			
			dp[l][r][0]%=MOD;
			dp[l][r][1]%=MOD;
		  
	 
	
	cout<<(dp[1][n][0]+dp[1][n][1])%MOD;
	return 0;

P4302 [SCOI2003]字符串折叠


zhaohaikun’s blog题解


枚举的思想+区间dp:

#include<bits/stdc++.h>
using namespace std;
#define fir(i,a,n) for(int i=a;i<=n;i++)
typedef long long ll;
const int N=1e2+10;
int dp[N][N];//dp[i][j]表示从i到j的最小折叠长度 
string a; 

int zhedie(int l,int r,int len) //开头 结尾 长度

	for(int i=l;i<=r;i++)
	
		//算出i到起点的距离 
		if(a[i]!=a[(i-l)%len+l]) return 0;
	
	return 1;
  
int main()

	cin>>a;
	a=" "+a;
	int n=a.size()-1;
	//初始化 自己到自己的折叠就是不折叠——1
	memset(dp,0x3f,sizeof(dp));
	fir(i,1,n) 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;//算出结尾 
			
			//len从2开始 ij不会重叠
			
			for(int k=i;k<j;k++)//作为跳板,先合并 
			
				dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]);
			 
			
			for(int k=i;k<j;k++) //从哪里断
			
				int lenn=k-i+1;
				if(zhedie(i,j,lenn)) 
									
					if(len%lenn) continue;//无法整除的必然无法合并
					int temp=0; 
					if(len/lenn<10) temp=1;
					else以上是关于动态规划算法零基础区间DP自学笔记的主要内容,如果未能解决你的问题,请参考以下文章

《算法零基础100例》(第100例) 动态规划 - 区间DP

动态规划:区间DP问题零神基础精讲

算法动态规划DP自学笔记 入门:基本知识+经典例题

《算法零基础100例》(第99例) 动态规划 - 路径DP

零基础理解动态规划(DP) - 开篇 - 01

学习笔记:动态规划