动态规划·树形DP

好东西,因为曾经在一次模拟赛的20分暴力中用到了,而我因为不会痛失20暴力分

树形DP

树形DP一般在一棵有根树上递归操作

嗯还是啥都不知道,上例题


【YbtOj】例题

A.树上求和

小小求和,拿捏

在从子节点转到父节点时,有着“二选一”的限制,所以设置状态时需要表示当前节点是否被选

于是乎,设状态 \(f_{x,opt}\) ,当 \(opt=0\) 时表示不选当前节点的最大和,反之则是选当前节点的最大和。于是乎就有转移方程

\[f_{x,0}=\Sigma\ max(f_{v,0},f_{v,1})\ ,f_{x,1}=\Sigma{f_{v,0}}+r_{x} \]

然后就做完了

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=6e3+5;
int n,r[N];
vector <int> tr[N];
int d[N],root,f[N][2];

void dfs(int x)
{
	int res1=0,res2=0;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		dfs(v);
		res1+=max(f[v][0],f[v][1]);
		res2+=f[v][0];
	}
	f[x][0]=res1,f[x][1]=r[x]+res2;
}
signed main()
{
	scanf("%lld",&n);
	for (int i=1;i<=n;i++) scanf("%lld",&r[i]);
	for (int i=1,u,v;i<n;i++)
	{
		scanf("%lld%lld",&u,&v);
		tr[v].push_back(u);
		d[u]++;
	}
	for (int i=1;i<=n;i++) if (!d[i]) { root=i; break; }
	
	dfs(root);
	printf("%lld",max(f[root][0],f[root][1]));
	return 0;
}

B.结点覆盖

小小覆盖,拿捏

一个点被覆盖到只有三种情况:选它的子节点,选自己,选它的父节点。于是乎,状态 \(f_{x,pot}\)\(opt\) 分别代表上三种情况,然后对应的转移就行了

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1505;
int n,k[N];
int d[N],root;
vector <int> tr[N];
int f[N][3];

void dfs(int x)
{
	int res0=0,res1=0,res2=0;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		dfs(v);
		
		res0+=min(f[v][1],f[v][0]);
		res1+=min(f[v][0],min(f[v][1],f[v][2]));
		res2+=min(f[v][0],f[v][1]);
	}
	
	f[x][1]=res1+k[x],f[x][2]=res2;
	if (_size==0) return ;
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		f[x][0]=min(f[x][0],res0-min(f[v][0],f[v][1])+f[v][1]);
	} 
}
signed main()
{
	memset(f,0x3f,sizeof f);
	scanf("%lld",&n);
	for (int i=1,x,m;i<=n;i++)
	{
		scanf("%lld",&x);
		scanf("%lld%lld",&k[x],&m);
		for (int j=1,v;j<=m;j++)
		{
			scanf("%lld",&v);
			tr[x].push_back(v);
			d[v]++;
		}
	}
	for (int i=1;i<=n;i++) if (!d[i]) { root=i; break; }
	
	dfs(root);
	printf("%lld",min(f[root][0],f[root][1]));
	return 0;
}

C.最长距离

我:嗯这看起来要跑两遍dfs
学姐:这题不用啊
于是乎,一顿思考怎样一个dfs跑出答案后无果
学姐:哦,好像要跑两遍dfs

小小距离,拿 n (×1) ,拿 ni (×2),拿捏

一个点的最长距离要么是从它的子节点们过来的,要么是从它的父亲过来的,子节点转移很好转移,跑一遍dfs即可;父节点转移也很好转移,用一个数组 \(mx_{fa}\) 记录一下它的父节点距离最长的一个子节点 \(u\),然后跑 dfs 让 \(v\)\(u\) 转移即可。若 \(u\) 就是 \(v\) 自己呢?这不好玩,这就得也记录一下次大值,从次大值转移来就行了

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int n;
struct node { int nxt,val; };
vector <node> tr[N];
int mx[N][2],f[N][2];//0:下/次大 1:上/最大 

void init()
{
	memset(mx,0,sizeof mx);
	memset(f,0,sizeof f);
	for (int i=1;i<N;i++) tr[i].clear();
}
void dfs1(int x)
{
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt;
		dfs1(v);
		f[x][0]=max(f[x][0],f[v][0]+tr[x][i].val);
		if (mx[x][1]<=f[v][0]+tr[x][i].val)//记录x节点的最大值/次大值
		{
			mx[x][0]=mx[x][1];
			mx[x][1]=f[v][0]+tr[x][i].val;
		}
		else mx[x][0]=max(mx[x][0],f[v][0]+tr[x][i].val);
	}
}
void dfs2(int x)
{
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt;
		if (f[v][0]+tr[x][i].val!=mx[x][1]) f[v][1]=mx[x][1]+tr[x][i].val;
		else f[v][1]=mx[x][0]+tr[x][i].val;
		f[v][1]=max(f[v][1],f[x][1]+tr[x][i].val);
		dfs2(v);//先转移再去深
	}
}
signed main()
{
	while (scanf("%lld",&n)!=EOF)
	{
		init();
		for (int i=2,fa,x;i<=n;i++)
		{
			scanf("%lld%lld",&fa,&x);
			tr[fa].push_back({i,x});
		}
		
		dfs1(1);
		dfs2(1);
		for (int i=1;i<=n;i++) printf("%lld\n",max(f[i][0],f[i][1]));
	}
	return 0;
}

D.选课方案

\(n\) 的范围很小,所以直接在树上跑背包就行

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=310;
int n,m,s[N];
vector <int> tr[N];
int f[N][N],ans;

void dfs(int x)
{
	f[x][1]=s[x];
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		dfs(v);
		for (int j=m+1;j>1;j--) //普通背包同款滚动数组
		for (int k=1;k<j;k++)
		{
			f[x][j]=max(f[x][j],f[v][k]+f[x][j-k]);
		}
	}
}
signed main()
{
	scanf("%lld%lld",&n,&m);
	for (int i=1,fa;i<=n;i++)
	{
		scanf("%lld%lld",&fa,&s[i]);
		tr[fa].push_back(i);
	}
	dfs(0);
	
	printf("%lld",f[0][m+1]);
	return 0;
}

E.路径求和

呃呃呃思维固化了

一条边,它会贡献多少次呢

是一侧的叶子节点个数乘另一侧的个数次

所以跑两遍 \(dfs\) 分别统计一下就好了

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,m;
struct node { int nxt,val; };
vector <node> tr[N];
int ans;

int sz[N],lf[N];//以i为根的子树中的叶子结点个数 
void dfs(int x,int _fa)
{
	sz[x]=1;
	int _size=tr[x].size();
	if (_size==1) lf[x]=1;
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt;
		if (v==_fa) continue;
		dfs(v,x);
		sz[x]+=sz[v],lf[x]+=lf[v];
	}
}
void dfs2(int x,int _fa)
{
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt;
		if (v==_fa) continue;
		dfs2(v,x);	
		ans+=tr[x][i].val*(lf[v]*(sz[1]-sz[v])+(lf[1]-lf[v])*sz[v]);
	}
}
signed main()
{
	scanf("%lld%lld",&n,&m);
	for (int i=1,u,v,w;i<=m;i++)
	{
		scanf("%lld%lld%lld",&w,&u,&v);
		tr[u].push_back({v,w});
		tr[v].push_back({u,w});
	}
	
	dfs(1,0);
	dfs2(1,0);
	printf("%lld",ans);
	return 0;
}

F.树上移动

想起来了之前模拟赛的一道T2

对于 \(u\) 的子树,如果在遍历完整个子树后还需要回到点 \(u\) ,手模后发现每条边会正好贡献两次;但对于整棵树,它最后不用回到根节点,为了使路径最短,那么让最后不回去的那条路径为从根节点出发的最长路径即可。

第二个同理,但并不是从根节点出发的最长路径,而是减去直径(刚开始没想成直径,怒调n小时),然后就做完了

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
const int inf=0x3f3f3f3f3f3f3f3f; 
int n,s;
struct node { int nxt,val; };
vector <node> tr[N];
int sz[N],mx[N][2];//0:次大 1:最大
int ans1,ans2=inf;

void dfs(int x,int _fa)
{
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt,w=tr[x][i].val;
		if (v==_fa) continue;
		dfs(v,x);
		sz[x]+=sz[v]+w;
		if (mx[x][1]<=mx[v][1]+w)
		{
			mx[x][0]=mx[x][1];
			mx[x][1]=mx[v][1]+w;
		}
		else mx[x][0]=max(mx[x][0],mx[v][1]+w);
	}
}
signed main()
{
	scanf("%lld%lld",&n,&s);
	for (int i=1,a,b,c;i<n;i++)
	{
		scanf("%lld%lld%lld",&a,&b,&c);
		tr[a].push_back({b,c});
		tr[b].push_back({a,c});
	}
	
	dfs(s,0);
	ans1=sz[s]*2-mx[s][1];
	for (int i=1;i<=n;i++)
	{
		ans2=min(ans2,sz[s]*2-mx[i][0]-mx[i][1]);
	}
	printf("%lld\n%lld",ans1,ans2);
	return 0;
}

G.块的计数

怎么转移呢,这一点都不好转移啊,我怎么知道它有多少联通块这些联通块是否合法

没法弄的话那就反过来求:\(f_{u}\) 表示以 \(u\) 为根的子树中联通块的个数, \(g_{u}\) 表示以 \(u\) 为根的子树中不合法的联通块的个数,那么最终的答案就是 \(\Sigma f_{u}-g_{u}\),然后根据状态转移方程

\[f_{u}=\prod(f_{v}+1),g_{u}=\prod(g_{v}+1) \]

转移就好了

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
const int MOD=998244353;
int n;
vector <int> tr[N];
int val[N],mx=0xc1c1c1c1c1c1c1c1;
int f[N],g[N];
int tol1,tol2;

void dfs(int x,int _fa)
{
	if (val[x]!=mx) g[x]=1;
	f[x]=1;
	
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa) continue;
		dfs(v,x);
		f[x]=(f[x]*(f[v]+1))%MOD;
		g[x]=(g[x]*(g[v]+1))%MOD;
	}
	tol1=(tol1+f[x])%MOD;
	tol2=(tol2+g[x])%MOD;
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n;
	for (int i=1;i<=n;i++) 
	{
		cin>>val[i];
		mx=max(mx,val[i]);
	}
	for (int i=1,u,v;i<n;i++)
	{
		cin>>u>>v;
		tr[u].push_back(v),tr[v].push_back(u);
	}
	
	dfs(1,0);
	cout<<(tol1-tol2+MOD)%MOD;
	return 0;
}

H.权值统计

先考虑将 \(u\) 的子树内所有到 \(u\) 的路径的乘积和表示为 \(f_{u}\),手模后发现它的转移方程就是

\[f_{u}=\Sigma (f_{v}\times val_{u}) \]

那么从一个子节点到另一个子节点的呢,就是\(f_{v1}\times f_{v2}\times val_x\),然后答案就是 \(ans=\sum (f_{u}+\Sigma (f_{v1}\times f_{v2}\times val_x))\)

嗯嗯

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
const int MOD=10086;
int n,val[N];
vector <int> tr[N];
int f[N],ans;

void dfs(int x,int _fa)
{
	int aaa=0,aaaa=0;
	f[x]+=val[x];
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa) continue;
		dfs(v,x);
		f[x]=(f[x]+f[v]*val[x])%MOD;
		aaa+=f[v],aaaa+=f[v]*f[v];
	}
	ans=(ans+f[x]+(aaa*aaa-aaaa)/2*val[x])%MOD;
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n;
	for (int i=1;i<=n;i++) cin>>val[i];
	for (int i=1,u,v;i<n;i++)
	{
		cin>>u>>v;
		tr[u].push_back(v),tr[v].push_back(u);
	}
	dfs(1,0);
	cout<<ans;
	return 0;
}

I.周年纪念日

啊每次推式子都是差最后一步——

首先,需要最小生成树

其次,换根DP

设状态 \(f_{u}\) 表示 \(u\) 子树的所有结点到 \(u\) 的代价和,转移 \(f_{u}\) 需要子树内的人流数,所以记 \(cnt_u\) 表示该子树内的人流量,所以状态转移方程就是

\[f_{u}=\sum (f_{v}+ w_{u,v}\times cnt_{v}),cnt_{x}=\Sigma \ cnt_{v} \]

但这样除了根节点,其他的都不符合题意。于是开始换根。

然后就做完了

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
const int M=2e5+5;
const int inf=0x3f3f3f3f3f3f3f3f;
int n,m,p[N];
struct node { int u,v,w; }e[M];
struct NODE { int nxt,val; };
vector <NODE> tr[N];
int cnt[N],f[N];
int ans1,ans2;

int fa[N];
bool cmp(node a,node b) { return a.w<b.w; }
int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
void kruskal()
{
	sort(e+1,e+1+m,cmp);
	int tol=0;
	for (int i=1;i<=n;i++) fa[i]=i;
	for (int i=1;i<=m;i++)
	{
		int u=e[i].u,v=e[i].v,w=e[i].w;
		int fa1=find_fa(u),fa2=find_fa(v);
		if (fa1==fa2) continue;
		
		fa[fa1]=fa2;
		tr[u].push_back({v,w});
		tr[v].push_back({u,w});
		ans1+=w,ans2=max(ans2,w);
		if (++tol>=n-1) break;
	}
}
void dfs(int x,int _fa)
{
	cnt[x]=p[x];
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt;
		if (v==_fa) continue;
		dfs(v,x);
		cnt[x]+=cnt[v];
		f[x]+=f[v]+tr[x][i].val*cnt[v];
	}
}
void dfs2(int x,int _fa)
{
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt;
		if (v==_fa) continue;
		f[v]=f[x]+(cnt[1]-cnt[v])*tr[x][i].val-cnt[v]*tr[x][i].val;
		dfs2(v,x);		
	}
	if (f[x]<ans2) ans2=f[x],ans1=x;
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>m;
	for (int i=1;i<=n;i++) cin>>p[i];
	for (int i=1;i<=m;i++) cin>>e[i].u>>e[i].v>>e[i].w;
	
	kruskal();
	cout<<ans1<<" "<<ans2<<"\n";
	
	ans1=0,ans2=inf;
	dfs(1,0);
	dfs2(1,0);
	cout<<ans1<<" "<<ans2;
	return 0;
}
posted @ 2024-12-25 13:44  还是沄沄沄  阅读(4)  评论(0编辑  收藏  举报