树上染色-题解(贪心+DP+二分)

树的上色

题意简述

树上有两个黑点,在每个单位时间内,每个黑点可以把自己相邻的一个白点变为黑色,求把整棵树所有点变为黑色的最短时间。

n 个点,两个黑点分别为 x,y

题解

遇到两个点的题的套路,一般是先讨论简化版情况。现在我们来考虑如果只有一个黑点该怎么办

简化版问题

首先因为树是无根树,我们可以将唯一的黑点变成根节点,此时由于需要最短时间,于是就可以保证我们每一次的操作都是往下进行的,即每个节点的子节点之间不会相互影响,只能由这个节点来一个个更新,这启发我们考虑树形DP。具体的,我们设 fi 表示在以 i 为根的子树中,若最初 i 为黑色,其余点为白色,将整棵子树变为黑色的最短时间。

现在我们考虑求解子树 u,设它的子节点为 v1,v2vc,此时我们只能够以某种顺序去选择这 c 个节点的染黑时间。说人话就是给 v1vc 赋权,将 1c 分别赋给某个节点,在赋权之后,设为 fv=fv+x,其中 x 表示给节点 v 赋权的值,则 fu=max1ic{fvi}。所以在树形DP的转移过程,我们就需要求解一个优秀的赋权方案,使得所有赋权之后的节点的权值最小。

引理:按照 fv 的大小,从大到小的给每个 fv 从小到达赋权。

说详细点,就是先按 fvi 从小到达排序,则 fvi=fvi+ci+1

证明:食用贪心的邻项交换法,考虑两个节点 j,k,有 fj<fk,设需要给他们赋权为 p,q(p<q),则我们需要证明的即为 fj+q,fk+p 更优,用数学语言表达,即需证:

max{fj+q,fk+p}max{fj+p,fk+q}

首先对于右边,因为 fjfk,p<q,所以右式值本就为 fk+q

所以即需证 max{fj+q,fk+p}fk+q,此时显然有 fk+pfk+q,而因为 fjfk,所以 fj+qfk+q,故原式成立。所以贪心策略成立。

那么我们就得到了这个简化版问题的解:

f 初始为 0,枚举 f 的每一个子节点,并将其 f 从小到大排序,从大到小的赋权 1n,在赋权后的 f 取最大值。

写成代码即为:

void solve(int u,int fa){
	int sum=0;
	f[u]=0;
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(v!=fa&&vis[i]!=1){
			solve(v,u);
		}
	}
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(v!=fa&&vis[i]!=1)b[++sum]=f[v];
	}
	sort(b+1,b+sum+1);
	reverse(b+1,b+sum+1);//为了代码的方便
	for(int i=1;i<=sum;i++){
		f[u]=max(f[u],b[i]+i);
	}
}

扩展到原问题

对于原问题,将两个黑点来看,可以将树拆成两部分进行处理。而拆树的部分一定就在 x>y 的这条路径上。朴素的思想是枚举这一条路径断开哪一条边,然后对于断开后的两棵树的解分别求 max。时间复杂度 O(n2logn),可以过64分。

以单调性再优化

再来审视这个枚举的过程,设总共 x>ylen 条树边,设定义域为 [1,len] 的整数的函数 g1,g2g1(k) 表示切断从 x 出发的第 k 条边的答案,g2 类似,则显然 g1,g2 都是非严格单调递增的函数,那么我们要求得一个 k 使得 max{g1(k),g2(lenk+1)} 最小。这样考虑,随着 k 的不断增大,g1(k) 会逐步增大,g2(lenk+1) 会逐步减小,则必然存在一个值 k,使得 g1(k)g2(lenk+1)g1(k+1)>g2(lenk)。(不存在的时候显然直接取 k=1 即为最优解。)
显然此时的 k1k 中最优秀的决策,而 k+1k+1len 中最优秀的决策,所以最终解一定是在 k,k+1 中产生。而 k 的值显然可以二分。至此,本题就将做法优化为了一个上界为 O(nlog2n) 的优秀算法。

实际上,排序的 log 完全看树的层数,随机数据下绝对跑得很快

最终的Code

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
#define N 5000005
int head[N],in,n,m,ver[N],list[N],tot=1,nxt[N],ans=1e9,x,y,vis[N],cnt,f[N],b[N],lst[N];
void add(int u,int v){
	nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
void dfs(int u,int in){
	list[u]=in;
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if((i^1)==in){
			continue;
		}
		dfs(v,i);
	}
}
void find(){
	int v=y;
	while(v!=x){
		lst[++cnt]=list[v];
		v=ver[list[v]^1];
	}
}
void solve(int u,int fa){
	int sum=0;
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(v!=fa&&vis[i]!=1){
			solve(v,u);
		}
	}
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(v!=fa&&vis[i]!=1)b[++sum]=f[v];
	}
	sort(b+1,b+sum+1);
	reverse(b+1,b+sum+1);
	for(int i=1;i<=sum;i++){
		f[u]=max(f[u],b[i]+i);
	}
}
int check(int mid){
	memset(f,0,sizeof f);
	vis[lst[mid]]=vis[lst[mid]^1]=1;
	solve(x,0);
	solve(y,0);
	vis[lst[mid]]=vis[lst[mid]^1]=0;
	return f[x]<=f[y];
} 
void init(){
	cin>>n>>x>>y;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		add(u,v);add(v,u);
	}
	dfs(x,0);
	find();
}
void solve(){
	int l=1,r=cnt;
	while(l<r){
		int mid=l+r>>1;
		if(check(mid)){
			r=mid;
		}	
		else {
			l=mid+1; 
		}
		ans=min(ans,max(f[x],f[y]));
	}
	check(l);
	ans=min(ans,max(f[x],f[y]));
	check(--l);
	ans=min(ans,max(f[x],f[y]));
	cout<<ans;
}
signed main(){
	ios::sync_with_stdio(false);
	init();
	solve();
	return 0;
}
posted @   spdarkle  阅读(179)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示