树形DP/换根DP 习题

Part 1:树形DP

选边

题意

一棵树有 n n n 个结点, n − 1 n-1 n1 条边,第 i i i 条边是: u [ i ] , v [ i ] , w [ i ] u[i],v[i],w[i] u[i],v[i],w[i] 表示结点 u [ i ] u[i] u[i] 与结点 v [ i ] v[i] v[i] 有一条权值为 w [ i ] w[i] w[i] 的无向边。

你需要从这 n − 1 n-1 n1 条边当中选取若干条边(可以不选),使得被选中的边的权值权值总和最大,但是有一些限制:

对于第 i i i 个点来说,与点 i i i 相连的所有边当中最多只能有 d [ i ] d[i] d[i] 条边被选中

解题思路
  • f [ x ] [ 0 ] f[x][0] f[x][0] 表示 x x x 子树最大的选边权值和,且 x x x 不与它的父亲连边; f [ x ] [ 1 ] f[x][1] f[x][1] 表示 x x x 子树最大的选边权值和,且 x x x 与它的父亲连边
  • x x x 的儿子为 y y y,对于任意的 x x x,我们可以想让 f [ x ] [ 0 ] f[x][0] f[x][0] f [ x ] [ 1 ] f[x][1] f[x][1] 加上 f [ y ] [ 0 ] f[y][0] f[y][0],表示 y y y 不与 x x x 连边,再来考虑 y y y x x x 连边的情况
  • 对于任意的 y y y,若 f [ y ] [ 1 ] > f [ y ] [ 0 ] f[y][1]>f[y][0] f[y][1]>f[y][0],则可以考虑连边。我们可以将 f [ y ] [ 1 ] − f [ y ] [ 0 ] f[y][1]-f[y][0] f[y][1]f[y][0] 压入一个 v e c t o r vector vector,并从大到小排序。 f [ x ] [ 1 ] f[x][1] f[x][1] 取前 d [ x ] − 1 d[x]-1 d[x]1 个, f [ x ] [ 0 ] f[x][0] f[x][0] 取前 d [ x ] d[x] d[x] 个即可
代码
#include<bits/stdc++.h>
#define LL long long
using namespace std;

const int N=300010;

int n,d[N];
LL f[N][2];
int head[N],edge[2*N],nxt[2*N],ver[2*N],tot;

void add(int x,int y,int z)
{
	ver[++tot]=y;  edge[tot]=z;
	nxt[tot]=head[x];  head[x]=tot;
}

void dp(int x,int fa)
{
	vector <LL> a;
	
	for(int i=head[x]; i; i=nxt[i])
	{
		int y=ver[i],z=edge[i];
		if(y==fa)
			continue;
			
		dp(y,x);
		
		f[x][0]+=f[y][1];
		f[x][1]+=f[y][1];
		if(d[y] && (LL)z+f[y][0]>f[y][1])
			a.push_back((LL)z+f[y][0]-f[y][1]);
	}
	
	sort(a.begin(),a.end(),greater<int>());
	
	int len=a.size();
	for(int i=0; i<min(d[x],len); i++)
		f[x][1]+=a[i]; 
	for(int i=0; i<min(d[x]-1,len); i++)
		f[x][0]+=a[i]; 
}

int main()
{
	scanf("%d",&n);
	for(int i=1; i<=n; i++)
		scanf("%d",&d[i]);
	for(int i=1; i<n; i++)
	{
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
		add(y,x,z);
	}
	
	dp(1,0);
	
	printf("%lld",f[1][1]);
	
	return 0;
}

交通违法

题意

禅城区有 N N N 条双向道路和 N N N 个路口,路口的编号从 1 1 1 N N N。每条道路连接两个路口。

两个路口之间不会有重边。保证任意两个路口都是相互到达的。

现在觉得在一些路口装上摄像头,检测路面的违法情况。

装在路口的摄像头可以监测到所有连接到这个路口的道路。

现在的问题是:最少需要在多少个路口安装摄像头才能监测所有的道路?

解题思路
  • 容易看出这是一个基环树,对于环上两个相邻的节点 x x x y y y,至少要有一个节点安装了摄像头
  • 所以我们可以用一次 DFS 找出 x x x y y y,并分别对以 x x x y y y 为根时的情况进行树形DP,两者答案取最小值即可
  • DP过程较简单,可自行思考
代码
#include<bits/stdc++.h>
using namespace std;

const int N=100010;

int n,f[N][2],a,b,ans;
bool v[N],flag;
vector <int> g[N];

void dfs(int x,int fa)
{
	if(flag)
		return;
	
	v[x]=1;
	
	for(int i=0; i<g[x].size(); i++)
	{
		int y=g[x][i];
		if(y==fa)
			continue;
		
		if(v[y])
		{
			a=x,b=y;
			flag=1;
			return;
		}
			
		dfs(y,x);
		if(flag)
			return;
	}
}

void dp(int x,int fa)
{
	f[x][1]=1;
	f[x][0]=0;
	
	for(int i=0; i<g[x].size(); i++)
	{
		int y=g[x][i];
		if(y==fa)
			continue;
		
		dp(y,x);
		
		f[x][1]+=min(f[y][1],f[y][0]);
		f[x][0]+=f[y][1];
	}
}

int main()
{
	scanf("%d",&n);
	for(int i=1; i<=n; i++)
	{
		int tot,son;
		scanf("%d",&tot);
		for(int j=1; j<=tot; j++)
		{
			scanf("%d",&son);
			g[i].push_back(son);
		}
	}
	
	dfs(1,0);
	
	g[a].erase(remove(g[a].begin(),g[a].end(),b),g[a].end());
	g[b].erase(remove(g[b].begin(),g[b].end(),a),g[b].end());

	dp(a,0);
	ans=f[a][1];
	
	dp(b,0);
	ans=min(ans,f[b][1]);
	
	printf("%d",ans);
	
	return 0;
}

Part 2:树形背包

修复道路

题意

有一颗树, N N N 个结点,那么至少要删除多少条边之后,使得存在一颗子树,该子树恰好有 P P P 个结点?

解题思路
  • f [ x ] [ j ] f[x][j] f[x][j] 表示 x x x 子树有 j j j 个节点至少要删几条边。对于节点 x x x 和 它的儿子 y y y 来说,有两种情况:

    • 如果删掉 x x x y y y 之间的边,则所有的 f [ x ] [ j ] + + f[x][j]++ f[x][j]++
    • 如果不删掉,则对 f [ x ] f[x] f[x] 做树形背包。容易得出状态转移方程为
      f [ x ] [ j + k ] = min ⁡ { f [ x ] [ j ] + f [ y ] [ k ] }   ( j ≤ s i z e [ x ] , k ≤ s i z e [ y ] ) f[x][j+k]=\min\{f[x][j]+f[y][k]\}\:(j\leq size[x],k\leq size[y]) f[x][j+k]=min{f[x][j]+f[y][k]}(jsize[x],ksize[y])
  • 统计 a n s ans ans 时,先让 a n s = f [ 1 ] [ p ] ans=f[1][p] ans=f[1][p],之后再让 a n s = min ⁡ ( a n s , f [ 2 … n ] [ p ] ) ans=\min(ans,f[2 \dots n][p]) ans=min(ans,f[2n][p])

代码
#include<bits/stdc++.h>
using namespace std;

const int N=160,M=110;

int n,p,f[N][N],size[N],tmp[N];
vector <int> g[N];

void dp(int x,int fa)
{
	f[x][1]=0;
	size[x]=1;
	
	for(int i=0; i<g[x].size(); i++)
	{
		int y=g[x][i];
		if(y==fa)
			continue;
		
		dp(y,x);
		
		for(int j=1; j<=size[x]; j++)
			tmp[j]=f[x][j];
			
		for(int j=1; j<=size[x]; j++)
			f[x][j]++;
			
		for(int j=1; j<=size[x]; j++)
			for(int k=1; k<=size[y]; k++)
				f[x][j+k]=min(f[x][j+k],tmp[j]+f[y][k]);
				
		size[x]+=size[y];	
	}
}

int main()
{
	scanf("%d%d",&n,&p);
	for(int i=1; i<n; i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		g[x].push_back(y);
		g[y].push_back(x);
	}
	
	memset(f,0x3f,sizeof(f));
	
	dp(1,0);
	
	int ans=f[1][p];
	for(int i=2; i<=n; i++)
		ans=min(ans,f[i][p]+1);
	
	printf("%d",ans);
	
	return 0;
}

无向图染色

题意

有一个 n n n 个结点 m m m 条边的无向图,不存在环。

一开始所有的结点都是白色的。

如果想把第 i i i 个结点染成黑色,需要 c o s t [ i ] cost[i] cost[i] 元。

对于第 i i i 个结点来说,与它直接有边相连的结点被称为结点 i i i 的朋友。

对于第 i i i 个结点来说,如果把第 i i i 个结点染成黑色,而且它的朋友当中,至少也有一个结点是黑色,那么第 i i i 个结点就是“黑色开心”结点。

现在有 Q Q Q 个问题,第 i i i 个问题给出一个整数 a [ i ] a[i] a[i],你需要回答:

假设给你 a [ i ] a[i] a[i] 元,你应该如何染色,使得“黑色开心”结点的数量最多?输出最多的黑色开心”结点的数量。

解题思路
  • 注意到整个图并不联通,所以需要对每个连通块进行操作

  • d p [ j ] dp[j] dp[j] 表示有 j j j 个开心节点的最小花费,容易想到需要对每个连通块进行操作再合并

  • 对每个连通块使用树形DP,设 f [ x ] [ j ] [ 0 / 1 ] f[x][j][0/1] f[x][j][0/1] 表示 x x x 子树有 j j j 个开心节点且 x x x 是/不是黑色开心节点的最小话费

  • y y y x x x 的一个儿子, k ∈ [ 0 , s i z e [ y ] ] k \in [0,size[y]] k[0,size[y]] t m p = min ⁡ ( f [ y ] [ k ] [ 0 ] , f [ y ] [ k ] [ 1 ] ) tmp=\min(f[y][k][0],f[y][k][1]) tmp=min(f[y][k][0],f[y][k][1])

  • 下面考虑 f [ x ] [ j ] [ 0 ] f[x][j][0] f[x][j][0] 的转移

    • 因为 x x x 不是黑色开心节点,所以容易得出 f [ x ] [ j ] [ 0 ] = min ⁡ ( f [ x ] [ j ] [ 0 ] , f [ x ] [ j − k ] [ 0 ] + t m p ) f[x][j][0]=\min(f[x][j][0],f[x][j-k][0]+tmp) f[x][j][0]=min(f[x][j][0],f[x][jk][0]+tmp)
  • 下面考虑 f [ x ] [ j ] [ 1 ] f[x][j][1] f[x][j][1] 的转移

    • x x x 本身就是黑色开心节点,则 f [ x ] [ j ] [ 1 ] = min ⁡ ( f [ x ] [ j ] [ 1 ] , f [ x ] [ j − k ] [ 1 ] + t m p ) f[x][j][1]=\min(f[x][j][1],f[x][j-k][1]+tmp) f[x][j][1]=min(f[x][j][1],f[x][jk][1]+tmp)

    • x x x 原先是白色,则现在需要染成黑色,且它有一个儿子是黑色,那么有两种情况:

      • y y y 是黑色开心节点,则 f [ x ] [ j ] [ 1 ] = min ⁡ ( f [ x ] [ j ] [ 1 ] , f [ x ] [ j − k − 1 ] [ 0 ] + c o s t [ x ] + f [ y ] [ k ] [ 1 ] ) f[x][j][1]=\min(f[x][j][1],f[x][j-k-1][0]+cost[x]+f[y][k][1]) f[x][j][1]=min(f[x][j][1],f[x][jk1][0]+cost[x]+f[y][k][1])

      • y y y 是白色节点,现在需要染成黑色,则 f [ x ] [ j ] [ 1 ] = min ⁡ ( f [ x ] [ j ] [ 1 ] , f [ x ] [ j − k − 2 ] [ 0 ] + c o s t [ x ] + c o s t [ y ] + f [ y ] [ k ] [ 0 ] ) f[x][j][1]=\min(f[x][j][1],f[x][j-k-2][0]+cost[x]+cost[y]+f[y][k][0]) f[x][j][1]=min(f[x][j][1],f[x][jk2][0]+cost[x]+cost[y]+f[y][k][0])

    • x x x 是黑色开心节点,但 y y y 是白色,则可以考虑将 y y y 染成黑色, f [ x ] [ j ] [ 1 ] = min ⁡ ( f [ x ] [ j ] [ 1 ] , f [ x ] [ j − k − 1 ] [ 1 ] + c o s t [ y ] + f [ y ] [ k ] [ 0 ] ) f[x][j][1]=\min(f[x][j][1],f[x][j-k-1][1]+cost[y]+f[y][k][0]) f[x][j][1]=min(f[x][j][1],f[x][jk1][1]+cost[y]+f[y][k][0])

  • 之后,对于每个连通块进行合并。设 i i i 是当前连通块的根, k ∈ [ 0 , s i z e [ i ] ] k\in [0,size[i]] k[0,size[i]],则状态转移方程为 d p [ j ] = min ⁡ ( d p [ j ] , d p [ j − k ] + m i n ( f [ i ] [ k ] [ 0 ] , f [ i ] [ k ] [ 1 ] ) ) ; dp[j]=\min(dp[j],dp[j-k]+min(f[i][k][0],f[i][k][1])); dp[j]=min(dp[j],dp[jk]+min(f[i][k][0],f[i][k][1]));

  • 但是注意到, d p [ j ] dp[j] dp[j] 不连续且没有单调性,所以需要将 d p [ j ] dp[j] dp[j] 从小到大排序,再将询问离线,也是从小到大排序,最后利用单调队列求解

代码
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=1010,Q=200010,M=10000000000000;

struct node
{
	int w,num;
} dp[N];
struct ask
{
	int id,money;
} a[Q];
int n,m,cost[N];
int q,ans[Q];
int size[N],f[N][N][2];
int head[N],ver[2*N],nxt[2*N],tot;
bool vis[N];

bool cmp1(node x,node y)
{
	return x.w<y.w;
}

bool cmp2(ask x,ask y)
{
	return x.money<y.money;
}

void add(int x,int y)
{
	ver[++tot]=y;
	nxt[tot]=head[x];
	head[x]=tot;
}

void dfs(int x,int fa)
{
	vis[x]=1;
	size[x]=1;
	f[x][0][0]=0;
	
	for(int i=head[x]; i; i=nxt[i])
	{
		int y=ver[i];
		if(y==fa || vis[y])
			continue;
		
		dfs(y,x);
		
		size[x]+=size[y];
		for(int j=size[x]; j>=1; j--)
		{
			for(int k=0; k<=min(size[y],j); k++)
			{
				int tmp=min(f[y][k][0],f[y][k][1]);
				
				if(f[x][j-k][0]<=M)
					f[x][j][0]=min(f[x][j][0],f[x][j-k][0]+tmp);
				
				if(f[x][j-k][1]<=M)
					f[x][j][1]=min(f[x][j][1],f[x][j-k][1]+tmp);
				if(j-k-1>=0 && f[x][j-k-1][0]<=M && f[y][k][1]<=M)
					f[x][j][1]=min(f[x][j][1],f[x][j-k-1][0]+cost[x]+f[y][k][1]);
				if(j-k-2>=0 && f[x][j-k-2][0]<=M && f[y][k][0]<=M)
					f[x][j][1]=min(f[x][j][1],f[x][j-k-2][0]+cost[x]+cost[y]+f[y][k][0]);
				if(j-k-1>=0 && f[x][j-k-1][1]<=M && f[y][k][0]<=M)
					f[x][j][1]=min(f[x][j][1],f[x][j-k-1][1]+cost[y]+f[y][k][0]);
			}
		}
	}
}

signed main()
{
	memset(f,0x7f,sizeof(f));
	memset(dp,0x7f,sizeof(dp));
	
	scanf("%lld%lld",&n,&m);
	for(int i=1; i<=n; i++)
		scanf("%lld",&cost[i]);
	for(int i=1; i<=m; i++)
	{
		int x,y;
		scanf("%lld%lld",&x,&y);
		add(x,y);
		add(y,x);
	}
	
	dp[0].w=0;
	for(int i=0; i<=n; i++)
		dp[i].num=i;
	
	int len=0;	
	for(int i=1; i<=n; i++)
	{
		if(vis[i])
			continue;
		
		dfs(i,0);
		len+=size[i];
		
		for(int j=len; j>=1; j--)
		{
			for(int k=0; k<=size[i]; k++)
				if(j>=k && dp[j-k].w<=M && min(f[i][k][0],f[i][k][1])<=M)
					dp[j].w=min(dp[j].w,dp[j-k].w+min(f[i][k][0],f[i][k][1]));
		}
	}
	
	sort(dp,dp+1+n,cmp1);
	
	scanf("%lld",&q);
	for(int i=1; i<=q; i++)
	{
		scanf("%lld",&a[i].money);
		a[i].id=i;
	}
	
	sort(a+1,a+1+q,cmp2);
	
	int l=0,val=0;
	for(int i=1; i<=q; i++)
	{
		while(l<=n && dp[l].w<=a[i].money)
		{
			val=max(val,dp[l].num);
			l++;
		}
		
		ans[a[i].id]=val;
	}
	
	for(int i=1; i<=q; i++)
		printf("%lld\n",ans[i]);

	return 0;
}

Part 3:换根DP

Maximum White Subtree

题目传送门

题意

给定一棵 n n n 个节点无根树,每个节点 u u u 有一个颜色 a u a_u au,若 a u a_u au 0 0 0 u u u 是黑点,若 a u a_u au 1 1 1 u u u 是白点。

对于每个节点 u u u,选出一个包含 u u u 的连通子图,设子图中白点个数为 c n t 1 cnt_1 cnt1,黑点个数为 c n t 2 cnt_2 cnt2,请最大化 c n t 1 − c n t 2 cnt_1 - cnt_2 cnt1cnt2。并输出这个值。

解题思路

一、DFS1

  • a [ i ] a[i] a[i] 表示 i i i 的子树对 i i i 的贡献

  • 则对于 x x x 的儿子 y y y a [ x ] + = a [ y ] a[x]+=a[y] a[x]+=a[y]

二、DFS2

  • f [ x ] f[x] f[x] 表示以 x x x 为根时的答案,则对于 x x x 的孩子 y y y

  • a [ y ] > 0 a[y]>0 a[y]>0,则它对 f [ x ] f[x] f[x] 有贡献,所以 f [ y ] = max ⁡ ( a [ y ] , f [ x ] ) f[y]=\max(a[y],f[x]) f[y]=max(a[y],f[x])

  • a [ y ] < 0 a[y]<0 a[y]<0,则它对 f [ x ] f[x] f[x] 无贡献,所以 f [ y ] = max ⁡ ( a [ y ] , a [ y ] + f [ x ] ) f[y]=\max(a[y],a[y]+f[x]) f[y]=max(a[y],a[y]+f[x])

代码
#include<bits/stdc++.h>
#define LL long long
using namespace std;

const int N=200010;

int n,a[N],f[N];
vector <int> g[N];

void dfs1(int x,int fa)
{
	for(int i=0; i<g[x].size(); i++)
	{
		int y=g[x][i];
		if(y==fa)
			continue;
			
		dfs1(y,x);
		
		if(a[y]>0)
			a[x]+=a[y];
	}
}

void dfs2(int x,int fa)
{
	for(int i=0; i<g[x].size(); i++)
	{
		int y=g[x][i];
		if(y==fa)
			continue;
			
		if(a[y]>0)
			f[y]=max(a[y],f[x]);
		else
			f[y]=max(a[y],a[y]+f[x]);
		dfs2(y,x);
	}
}

int main()
{
	scanf("%d",&n);
	for(int i=1; i<=n; i++)
	{
		scanf("%d",&a[i]);
		if(a[i]==0)
			a[i]=-1;
	}
	for(int i=1; i<n; i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		g[x].push_back(y);
		g[y].push_back(x);
	}
	
	dfs1(1,0);
	
	f[1]=a[1];
	
	dfs2(1,0);
	
	for(int i=1; i<=n; i++)
		printf("%d ",f[i]);
	
	return 0;
}

蚂蚁聚会

题意

n n n 个蚁巢,这 n n n 个蚁巢形成一颗树形结构,第 i i i 个蚁巢有 a [ i ] a[i] a[i] 只蚂蚁。现在蚂蚁们想举行一个大型的聚会。

但是这些蚂蚁比较懒惰,都不想走太远,每只蚂蚁最多只愿意走 t t t 步(每一步就是走一条边)。
它们要计算:如果选择第 i i i 个蚁巢作为举行聚会的地点,可以有多少只蚂蚁参加聚会?记该数量为 p [ i ] p[i] p[i]

你的任务就是帮助计算: p [ 1 ] , p [ 2 ] , p [ 3 ] , … p [ n ] p[1],p[2],p[3],\dots p[n] p[1],p[2],p[3],p[n]

解题思路

考虑换根DP

  • f [ x ] [ i ] f[x][i] f[x][i] 表示 x x x 的子树中距离 x x x i i i 的蚂蚁总数量

  • g [ x ] [ i ] g[x][i] g[x][i] 表示以 x x x 为根,距离 x x x i i i 的蚂蚁总数量

一、DFS1

  • 先以 1 1 1 为根,用一次 DFS 求出 f [ 1 … n ] [ 1 … t ] f[1\dots n][1\dots t] f[1n][1t]
  • 初始化: f [ x ] [ 0 ] = a [ x ] f[x][0]=a[x] f[x][0]=a[x]

二、DFS2

  • 易得 g [ 1 ] [ 1 … t ] = f [ 1 ] [ 1 … t ] g[1][1\dots t]=f[1][1\dots t] g[1][1t]=f[1][1t]

  • y y y x x x 的儿子,那么下面考虑如何由 g [ x ] [ 1 … t ] g[x][1\dots t] g[x][1t] 推出 g [ y ] [ 1 … t ] g[y][1\dots t] g[y][1t]

  • g [ y ] [ i ] g[y][i] g[y][i] 由两部分组成,第一部分是在 y y y 子树内走 i i i 步上去,为 f [ y ] [ i ] f[y][i] f[y][i]

  • 第二部分是走 i − 1 i-1 i1 步到 x x x 节点再往下走 1 1 1 步到 y y y,为 g [ x ] [ i − 1 ] − f [ y ] [ i − 2 ] g[x][i-1]-f[y][i-2] g[x][i1]f[y][i2]

  • 所以 g [ y ] [ i ] = f [ y ] [ i ] + g [ x ] [ i − 1 ] − f [ y ] [ i − 2 ] g[y][i]=f[y][i]+g[x][i-1]-f[y][i-2] g[y][i]=f[y][i]+g[x][i1]f[y][i2]

  • 注意下标不要越界

代码
#include<bits/stdc++.h>
using namespace std;

const int N=100010,X=25;

int n,t,a[N],f[N][X],ans[N][X];//ans数组就是g数组
vector <int> g[N];

void dfs1(int x,int fa)
{
	f[x][0]=a[x];
	for(int i=0; i<g[x].size(); i++)
	{
		int y=g[x][i];
		if(y==fa)
			continue;
			
		dfs1(y,x);
		
		for(int j=1; j<=t; j++)	
			f[x][j]+=f[y][j-1];
	}	
}

void dfs2(int x,int fa)
{
	for(int i=0; i<g[x].size(); i++)
	{
		int y=g[x][i];
		if(y==fa)
			continue;

		for(int j=0; j<=t; j++)
		{
			if(j>=2)
				ans[y][j]=f[y][j]+ans[x][j-1]-f[y][j-2];
			else if(j>=1)
				ans[y][j]=f[y][j]+ans[x][j-1];
			else
				ans[y][j]=f[y][j];
		}
		
		dfs2(y,x);
	}
}


int main()
{
	scanf("%d%d",&n,&t);
	for(int i=1; i<n; i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		g[x].push_back(y);
		g[y].push_back(x);
	}
	for(int i=1; i<=n; i++)
		scanf("%d",&a[i]);
	
	dfs1(1,0);
	
	for(int i=0; i<=t; i++)
		ans[1][i]=f[1][i];
		
	dfs2(1,0);
		
	for(int i=1; i<=n; i++)
	{
		int sum=0;
		for(int j=0; j<=t; j++)	
			sum+=ans[i][j];
		
		printf("%d\n",sum);
	}
	
	return 0;
}

Kamp

题目传送门

题意

一颗树 n n n 个点, n − 1 n-1 n1 条边,经过每条边都要花费一定的时间,任意两个点都是联通的。

K K K 个人(分布在 K K K 个不同的点)要集中到一个点举行聚会。

聚会结束后需要一辆车从举行聚会的这点出发,把这 K K K 个人分别送回去。

请你回答,对于 i = 1 ∼ n i=1 \sim n i=1n ,如果在第 i i i 个点举行聚会,司机最少需要多少时间把 K K K 个人都送回家。

解题思路

考虑换根DP

  • s i z e [ i ] size[i] size[i] 表示:在子树 i i i 内的顾客的数量

  • f [ i ] [ 0 / 1 ] f[i][0/1] f[i][0/1] 表示:在子树 i i i 内,最远/第二远的顾客到达节点 i i i 的所需时间

    说明: f [ i ] [ 0 ] f[i][0] f[i][0] f [ i ] [ 1 ] f[i][1] f[i][1] 是来自于 i i i 的不同儿子子树

  • a [ i ] a[i] a[i] 表示: 司机从 i i i 点出发,送完 i i i 子树内的所有顾客,然后司机返回到节点 i i i ,所需时间的总和。

  • g [ i ] [ 0 / 1 ] g[i][0/1] g[i][0/1] 表示:若整棵树以 i i i 为根,最远/第二远的顾客到达节点 i i i 的所需时间。

    说明: g [ i ] [ 0 ] g[i][0] g[i][0] g [ i ] [ 1 ] g[i][1] g[i][1] 是来自于 i i i 的不同儿子子树

  • b [ i ] b[i] b[i] 表示: 若整棵树以 i i i 为根, 司机从 i i i 号点出发,送完所有顾客,然后司机返回节点 i i i,所需时间的总和。

那么第 i i i 个节点的答案就是 b [ i ] − g [ i ] [ 0 ] b[i]-g[i][0] b[i]g[i][0]。(最长时间的那个人最后送,无需返回根结点)

一、DFS1

  • 先以 1 1 1 号节点为根,DFS一次,求出所有的 f [ i ] [ 0 … 1 ] f[i][0\dots1] f[i][01] s i z e [ i ] size[i] size[i] a [ i ] a[i] a[i]

  • 考虑边界,若 i i i 是叶子节点, f [ i ] [ 0 ] = 0 ,    f [ i ] [ 1 ] = 0 ,    a [ i ] = 0 f[i][0] = 0,\:\:f[i][1] = 0,\:\:a[i] = 0 f[i][0]=0,f[i][1]=0,a[i]=0

二、DFS2

  • 对于 1 1 1 号节点来说, b [ 1 ] = a [ 1 ] ,    g [ 1 ] [ 0 / 1 ] = f [ 1 ] [ 0 / 1 ] b[1]=a[1],\:\:g[1][0/1]=f[1][0/1] b[1]=a[1],g[1][0/1]=f[1][0/1]

  • 对于 x x x 的儿子 y y y 来说,设 l e n ( x , y ) = z len(x,y)=z len(x,y)=z

  • 下面讨论 b [ y ] b[y] b[y] 的取值情况:

    • x x x 的其它儿子节点没有顾客,即 s i z e [ y ] = k size[y]=k size[y]=k,此时 b [ y ] = a [ y ] ; b[y]=a[y]; b[y]=a[y];

    • y y y 的儿子节点没有顾客,即 s i z e [ y ] = 0 size[y]=0 size[y]=0,此时 b [ y ] = b [ x ] + 2 ∗ z b[y]=b[x]+2*z b[y]=b[x]+2z

    • x x x 的其他儿子和 y y y 的儿子都有顾客,此时 b [ y ] = b [ x ] b[y]=b[x] b[y]=b[x]

  • 下面讨论 g [ y ] [ 0 / 1 ] g[y][0/1] g[y][0/1] 的取值情况

    • s i z e [ y ] = k size[y]=k size[y]=k,此时 x x x 号节点为根的树当中,只有 y y y 号子树内有顾客,故 g [ 2 ] [ 0 ] = f [ 2 ] [ 0 ] ,    g [ 2 ] [ 1 ] = f [ 2 ] [ 1 ] g[2][0] = f[2][0],\:\:g[2][1] = f[2][1] g[2][0]=f[2][0],g[2][1]=f[2][1]

    • 否则,要先从以 x x x 号结点为整棵树的根时,找出不是来自于 y y y 号子树的顾客到达 x x x 号结点的最长时间路径,不妨记为 p p p

    • 若能计算出 p p p,则 g [ y ] [ 0 ] = max ⁡ ( p + z , f [ y ] [ 0 ] ) g[y][0]=\max(p+z,f[y][0]) g[y][0]=max(p+z,f[y][0]) g [ y ] [ 1 ] g[y][1] g[y][1]等于 { f [ 2 ] [ 0 ] ,   f [ 2 ] [ 1 ] ,   p + z } \{f[2][0],\:f[2][1],\:p+z\} {f[2][0],f[2][1],p+z} 的第二大。(因为 g [ i ] [ 0 ] g[i][0] g[i][0] g [ i ] [ 1 ] g[i][1] g[i][1] 是来自于 i i i 的不同儿子子树,所以 g [ y ] [ 1 ] g[y][1] g[y][1] 无需考虑从非 y y y x x x 儿子节点取第 2 2 2 大的)

  • 下面讨论 p p p 的取值

    • s i z e [ y ] = 0 size[y]=0 size[y]=0,则易得 p = g [ x ] [ 0 ] p=g[x][0] p=g[x][0]

    • f [ y ] [ 0 ] + z = g [ x ] [ 0 ] f[y][0]+z=g[x][0] f[y][0]+z=g[x][0],则 p = g [ x ] [ 1 ] p=g[x][1] p=g[x][1]

    • 否则, p = g [ x ] [ 0 ] p=g[x][0] p=g[x][0]

代码
#include<bits/stdc++.h>
#define LL long long
using namespace std;

const int N=500010;

struct node
{
	int ver,edge;
};
int n,k,size[N];
LL f[N][2],a[N],g[N][2],b[N];
vector <node> gg[N];

void dfs1(int x,int fa)
{
	for(int i=0; i<gg[x].size(); i++)
	{
		int y=gg[x][i].ver,z=gg[x][i].edge;
		if(y==fa)
			continue;
		
		dfs1(y,x);
		
		if(size[y]==0)
			continue;
		size[x]+=size[y];
		a[x]+=a[y]+1LL*2*z;
		if(f[y][0]+z>f[x][0])
		{
			f[x][1]=f[x][0];
			f[x][0]=f[y][0]+z;
		}
		else if(f[y][0]+z>f[x][1])
			f[x][1]=f[y][0]+z;
	}
} 

void dfs2(int x,int fa)
{
	for(int i=0; i<gg[x].size(); i++)
	{
		int y=gg[x][i].ver,z=gg[x][i].edge;
		if(y==fa)
			continue;
		
		if(size[y]==k)
			b[y]=a[y];
		else if(size[y]==0)
			b[y]=b[x]+1LL*2*z;
		else
			b[y]=b[x];
			
		if(k==size[y])
		{
			g[y][0]=f[y][0];
			g[y][1]=f[y][1];
		}
		else
		{
			LL p=0;
			if(size[y]==0)
				p=g[x][0];
			else if(f[y][0]+z==g[x][0])
				p=g[x][1];
			else p=g[x][0];
			
			if(p+z>f[y][0])
			{
				g[y][0]=p+z;
				g[y][1]=f[y][0];
			}
			else
			{
				g[y][0]=f[y][0];
				g[y][1]=max(f[y][1],p+z);
			}
		}
		
		dfs2(y,x); 
	}
}

int main()
{
	scanf("%d%d",&n,&k);
	for(int i=1; i<n; i++)	
	{
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		gg[x].push_back(node{y,z});
		gg[y].push_back(node{x,z});
	}
	for(int i=1; i<=k; i++)
	{
		int x;
		scanf("%d",&x);
		size[x]=1;
	}
	
	dfs1(1,0);
	
	g[1][0]=f[1][0];
	g[1][1]=f[1][1];
	b[1]=a[1];
	
	dfs2(1,0);
	
	for(int i=1; i<=n; i++)
		printf("%lld\n",b[i]-g[i][0]);
	
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值