区间dp 和 树型dp

区间dp

递推方程是以区间的形式给出 一般套路 :枚举区间长度 区间端点 区间分界点
然后就是想怎么去对这个区间进行一定的操作 从最原始的地方开始一步步推导方程!

for(i=1;i<n;i++)//区间长度为1
{
	for(j=1;j<=n-i;j++) //区间开头
	{
	for(k=j;k<j+i;k++)  //分界点
	{
	f[j][j+i]=min(f[j][j+i],f[j][k]+f[k+1][j+i]+s[j+i]-s[j-1]);
	g[j][j+i]=max(g[j][j+i],g[j][k]+g[k+1][j+i]+s[j+i]-s[j-1]);
	}
	}
	}

如果区间是一个环形那就开2倍空间:去看从哪一个点断开的去枚举端点!!不妨为一个好思路

#include <bits/stdc++.h>
using namespace std;
int n;
int a[501],f[501][501],s[501];
int main()
{
	cin>>n;
	int i,j,k;
	for(i=1;i<=n;i++)
	{
		cin>>a[i];
		a[i+n]=a[i];
	}
	int t=n*2;
	for(i=1;i<=t;i++)
	{
		s[i]=s[i-1]+a[i]; 
	}
	memset(f,127,sizeof(f));
	for(i=1;i<=t;i++)  //长度为0 的区间
	f[i][i]=0;
	for(i=1;i<t;i++)   //因为转移方程  是由两个小区间转移到大区间
					   //故要按照区间大小进行排序先更新每一个小区间保证
					   //后面的区间更新时用到的值是有效的
	{
		for(j=1;j<=t-i;j++)//区间的开头 
		{
			for(k=j;k<j+i;k++)  //区间分界线  
			f[j][j+i]=min(f[j][j+i],f[j][k]+f[k+1][j+i]+s[j+i]-s[j-1]);
		}
	}
	int ans=1<<30;
	
	for(i=1;i<=n;i++)
	{ 
		ans=min(ans,f[i][i+n-1]);
	}
	cout<<ans;
}

这个地方就是去看那一段长度为n 的子区间最小 应为是断点从什么地方开始断开之后形成的
相当于把区间分成了两份往断口合并!
主要就是看如何对这一个区间的东西进行一定的操作!!!来达到目的的实现!!
当然也有变种也就是给定一个原始区间对该原始区间进行dp 添加或者减少数字!!

#include <bits/stdc++.h>  //区间dp的好题目
using namespace std;
typedef int64_t i64;
int mod=19650827;
int a[1005];
int f[1005][1005][2];
int n;
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	for(int i=1;i<=n;i++)
	{
		f[i][i][0]=1;
	}
	for(int k=1;k<n;k++)
	{
		for(int i=1,j=i+k;j<=n;j++,i++)
		{
			if(a[i]<a[i+1]) f[i][j][0]+=f[i+1][j][0];
			if(a[i]<a[j]) f[i][j][0]+=f[i+1][j][1];
			if(a[j]>a[i]) f[i][j][1]+=f[i][j-1][0];
			if(a[j]>a[j-1]) f[i][j][1]+=f[i][j-1][1];
			f[i][j][0]%=mod;
			f[i][j][1]%=mod;
		}
	}
	cout<<(f[1][n][0]+f[1][n][1])%mod;
}

这个就是一个原始区间然后不断去枚举添加数字!!看可以从那一个最开始的区间开始不断推导出新的东西!长度为 i+1的区间由长度为i的区间推导若开头是得到可以是j+1j+i也可以是从jj+i-1推导得到!!这个地方很好!!就是递推由小问题不断推出大问题,去写递推方程!!像递归的逆过程 递归就是大化小直至化为可以解决的东西

#include <bits/stdc++.h>
using namespace std;
int n,m;
inline __int128 read(){
   __int128 x = 0, f = 1;
   char ch = getchar();
   while(ch < '0' || ch > '9'){
   	if(ch == '-')
   		f = -1;
   	ch = getchar();
   }
   while(ch >= '0' && ch <= '9'){
   	x = x * 10 + ch - '0';
   	ch = getchar();
   }
   return x * f;
}
inline void print(__int128 x){
   if(x < 0){
   	putchar('-');
   	x = -x;
   }
   if(x > 9)
   	print(x / 10);
   putchar(x % 10 + '0');
}
#define i128 __int128
i128 a[85];
i128 f[85][85]; //区间枚举!!
i128 ans;		//答案记录!!
i128 base[85];
void solve()
{
   for(int i=1;i<=m;i++)
   {
   	for(int j=1;j<=m;j++)
   	{
   		f[i][j]=0;
   	}
   }
   for(int i=1;i<=m;i++)
   {
   	a[i]=read();
   }
   //	for(int i=1;i<=m;i++)
   //	{
   //		//f[i][i]=2*a[i];//这是每次取走一个元素!!
   //		print(a[i]);
   //		cout<<endl;
   //	}
   //print(base[5]);
   //cout<<endl;
   for(int i=1;i<=m;i++)
   {
   	for(int j=m;j>=i;j--)
   	{
   		f[i][j]=max(f[i][j],f[i-1][j]+base[m-j+i-1]*a[i-1]);
   		f[i][j]=max(f[i][j],f[i][j+1]+base[m-j+i-1]*a[j+1]);
   	}
   }
   i128 mx=0;
   for(int i=1;i<=m;i++)
   {
   	mx=max(mx,f[i][i]+base[m]*a[i]);
   }
   //	print(mx);
   //	cout<<endl;
   ans+=mx;
}
int main()
{
   cin>>n>>m;
   base[0]=1;
   for(int i=1;i<=m;i++)
   {
   	base[i]=base[i-1]*2;
   }
   for(int i=1;i<=n;i++)
   {
   	solve();
   }
   print(ans);
}

这道题用了 int 128 题目本身是不难的这也算是多了个东西!主要就是这道题记录的原因就是给一道int128使用的方法看看,以后可以参照这个模板去写,这道题倒是没什么!!像合唱队模型的变种一样!!
主要就是看是如何对区间进行操作 是一步步化还是可以将区间划分后合并的那种!!
针对不同问题写出递推方程即可

高贵的树形dp

树形dp有两种一种是根可变 一种是根不可变
其中也有一定的思维方式 根可变就是计算当一个节点为根和不为根的情况去讨论
根不可变就不断递归去解决它的子树!!! 这个很有意思

高贵的树dp

常见题目:没有上司的舞会 直接来变种 树上背包

#include <bits/stdc++.h>
using namespace std;
long long f[501][501][2];
int v[501];
int n,cnt,m;
struct node {
	int where;
	node *next;
}*head[501],a[501];
void makelist(int x,int y)
{
	a[++cnt].where=y;
	a[cnt].next=head[x];
	head[x]=&a[cnt];
}
void dfs(int i)
{
	for(node *x=head[i];x;x=x->next)
	{
		dfs(x->where);
		for(int j=m;j>=0;j--)
		for(int k=0;k<=j;k++)
		{
			f[i][j][0]=max(f[i][j][0],f[i][j-k][0]+max(f[x->where][k][0],f[x->where][k][1]));
			f[i][j][1]=max(f[i][j][1],f[i][j-k][1]+f[x->where][k][0]);
		}
	}
	for(int j=m;j;j--)
	f[i][j][1]=f[i][j-1][1]+v[i];
	f[i][0][1]=0;
}
int main()
{
	cin>>n>>m;
	int i;
	for(i=2;i<=n;i++)
	{
		int x;
		cin>>x;
		makelist(x,i);
	}
	for(i=1;i<=n;i++)
	cin>>v[i];
	
	dfs(1);
	
	cout<<max(f[1][m][0],f[1][m][1]);
}

这里主要就是有一个限制只能最多取m个人 所以要拿背包的思想做把人当作物品取枚举。故要注意背包的东西最大容量和每个物品最多有多少个!!然后枚举物品时注意枚举顺序!!要从大往下枚举!!这就是一道典型的树上背包题而且是较为简单的类型
2.
这道题就是一个很经典的另一个书上背包题

#include <bits/stdc++.h>
using namespace std;
typedef int64_t i64;
i64 dp[3005][3005];
int siz[3005];//子树大小 !! 
int n,m,cnt;
struct node{
	int v,where;
	node *next;
}a[3005],*head[3005];
int v[3005];
int ans;//计入多少个算进去的非根节点
void makelist(int x,int y,int z)
{
	a[++cnt].where=y;
	a[cnt].v=z;a[cnt].next=head[x];
	head[x]=&a[cnt];
}
void solve(int x)
{
	if(x>n-m)
	{
		siz[x]=1;
		dp[x][1]=v[x];
		return ;
	}
	for(node *y=head[x];y;y=y->next)
	{
		solve(y->where);
		siz[x]+=siz[y->where];
	//	if(x==1)
		for(int i=siz[x];i>=0;i--)
		{
			for(int j=0;j<=siz[y->where];j++)
			{
	//			if(x==1)
	//			{
	//				cout<<y->where<<' '<<y->v<<' ';
	//				cout<<dp[x][1]<<' ';
	//			}
				dp[x][i]=max(dp[x][i],dp[x][i-j]+dp[y->where][j]-(y->v));
			}
		}
	
	}
}
int main()
{
	cin>>n>>m;
	memset(dp,237,sizeof(dp));
	for(int i=1;i<=n;i++)
	{
		dp[i][0]=0;
	}
	for(int i=1;i<=n-m;i++)
	{
		int k;//有k个子节点
		cin>>k;
		for(int j=1;j<=k;j++)
		{
			int x,y;
			cin>>x>>y;//节点编号 和 节点的成本  
			makelist(i,x,y);
		}
	}
	for(int i=n-m+1;i<=n;i++)
	{
		cin>>v[i];
	}
	//cout<<dp[1][1];
	solve(1);
	//cout<<siz[5]<<'\n';
	for(int i=m;i>=1;i--)
	{
		if(dp[1][i]>=0)
		{
		//	cout<<dp[1][i]<<' '<<i<<' '<<endl;
			cout<<i;
		return 0;
		}
	}
//	cout<<endl;
//	for(int i=1;i<=n;i++)
//	{
//		for(int j=1;j<=n;j++)
//		{
//			cout<<dp[i][j]<<' ';
//		}
//		cout<<'\n';
//	}
}

去记录目标节点的数目就是用户节点的数目用用户节点的数目取枚举如果该子树有用户siz!=0否则siz==0,故用户数量是一个关键去看枚举的过程
但是如果该数是一个无根树的话

那么就会有限制条件要标记父亲就是不能一直访问的去

#include <bits/stdc++.h>
using namespace std;
int tot;
int siz[100005],st[100005];
int n,cnt,m;
struct node {
	int where;
	node *next;
}*head[100005],a[200010];
int dp[100005][55];
void makelist(int x,int y)
{
	a[++cnt].where=y;
	a[cnt].next=head[x];
	head[x]=&a[cnt];
}
void dfs(int x)
{
	st[x]=1;
	for(node *y=head[x];y;y=y->next)
	{
		if(st[y->where]) continue;
		dfs(y->where);
		siz[x]+=siz[y->where];//这一步不也是同理
		if(siz[y->where]) tot++;
		for(int i=min(m,siz[x]);i>=0;i--)
		{
			for(int j=0;j<=min(siz[y->where],i);j++)
			{
				dp[x][i]=max(dp[x][i],dp[x][i-j]+dp[y->where][j]+(siz[y->where]&&siz[y->where]==j));
			}
		}
	}
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<n;i++)
	{
		int x,y;
		cin>>x>>y;
		makelist(x,y);
		makelist(y,x);
	}
	int k;
	cin>>k;
	for(int i=1;i<=k;i++)
	{
		int x;
		cin>>x;
		siz[x]=1;
	}
	dfs(1);
	cout<<2*(tot-dp[1][m]);
}

tot 是总的节点 个数要求的话 节点个数-1为边的个数 故就枚举走了多少条边 若该节点有目标节点则要走的边先对应要++ 之后去不断筛选得出答案

此外还有一种变种题 用递归去实现 子树的区间枚举 原因:给出树的方式很奇特!!如先序给出树的情况,


这道题目便是 那就很显然去用递归的思想去弄 每一个东西去枚举l r
很显然目前还不是特别特别懂!!但是可以化为区间dp 分界线为根 左边区间为左子树 右边区间为右子树!!这样不断枚举的去!!!达到想要的目的!!

#include <bits/stdc++.h>
using namespace std;
typedef int64_t i64;
i64 f[100][100];
int n;
int root[100][100];
void print(int l,int r)
{
	if(l>r) return ;
	cout<<root[l][r]<<' ';
	if(l==r) return;
	print(l,root[l][r]-1);//左子树
	print(root[l][r]+1,r);
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>f[i][i];
		root[i][i]=i;
		f[i][i-1]=0;
	}
	for(int i=1;i<n;i++)
	{
		for(int j=1;j<=n-i;j++)
		{
			f[j][j+i]=f[j+1][j+i]+f[j][j];
			root[j][j+i]=j;
			for(int k=j+1;k<j+i;k++)
			{
				if(f[j][j+i]<f[j][k-1]*f[k+1][j+i]+f[k][k])
				{
					f[j][j+i] = f[j][k-1] * f[k+1][j+i] + f[k][k];
					root[j][j+i]=k;
				}
			}
		}
	}
	cout<<f[1][n]<<'\n';
	print(1,n);
}

故要注意

高贵的换根树dp

如何去dp呢
主要就是两步 该点为根与该点不为根的情况 up从下往上 down 从下往上弄一次!!
这种题目目前写的不多但是都很经典 !! 目前没做到多少题目!!到时候再弄弄把

posted @ 2023-04-29 11:46  _Lance  阅读(21)  评论(0编辑  收藏  举报