Codeforces-348E Pilgrims

#4342. CF348 Pilgrims

此题同UOJ#11 ydc的大树

Online Judge:Bzoj-4342,Codeforces-348E,Luogu-CF348E,Uoj-#11

Label:树的直径,树形dp,神题

题目描述

在很久以前有一片土地被称为 Dudeland。Dudeland 包含 n 个城镇,它们用 n − 1 条双向道路连接起来。这些城镇通过道路可以两两互达。这里有 m 个修道院坐落在 m 个不同的城镇。每个修道院有一个教徒。在一年之始,每个教徒会选择离他最远的一个修道院。如果有多个,他会把所有的都列入清单。在 “BigLebowskiday” 里,每个教徒会随机选择一个清单里的城镇开始走去。Walter 讨厌教徒。他想尽可能的通过阻止他们的行程来让尽可能多的人不开心。他计划摧毁一个没有修道院的城镇。一个教徒如果在他的清单里没有任何一个城镇能去,他就会不开心。你需要求出 Walter 最多能让几个教徒不开心。除此之外,你还要计算他有多少种方法

输入格式

第一行包含两个整数 n,m,满足 (3 ≤ n ≤ 10^5), (2 ≤ m<n)

接下来一行,有 m 个互不相同的整数,他们代表了有修道院的城镇的编号。

接下来 n − 1 行,每行三个整数 (a_i,b_i,c_i),表示 (a_i,b_i) 之间有一条边权为 (c_i) 的边。((1 ≤ a_i,b_i ≤ n,c_i ≤ 1000))

输出格式

输出两个数:最多能让几个教徒不开心,以及有多少种方式达到这种效果。

样例

输入

8 5
7 2 5 4 8
1 2 1
2 3 2
1 4 1
4 5 2
1 6 1
6 7 8
6 8 10

输出

5 1

题解:

这道题有两种解法:

  • 一种是复杂度为(O(NlogN))的树形dp,详见CF上的标程
  • 下面一种(O(N))做法,参考了这篇blog,uoj上出题人的O(N)思路,网上O(N)解法的思路都大致差不多,都根据树的直径来分类讨论但都没我写的详细

O(N)解法:

这道题要找树上离某点距离最远的点,联想到树的直径以及包含的性质。

我们知道树的直径可能不止一条,但他们都交于一点——直径的中点。

我们发现有时候直径上的中点可能会有两个——就是有两个点到两端的距离都相等,那么随便选哪一个其实都没影响,为了区分,我们将选择的那个中点重新命名为类中点,并且存在如下性质:

对于树上的任意一点x,x在树上的最远路径必定经过上面所说的类中点。

查询类中点代码如下,其实那三个搜索可以并为一个,但为了区分意义所以打三个

bool is[N];//是不是修道院
int len[N][2],P1=0,P2=0,diameter=0,root=0;
void findpoint1(int x,int fa,int nowd){//找到直径的一个端点P1 
	if(is[x]&&diameter<nowd)P1=x,diameter=nowd;
	for(int i=0;i<e[x].size();i++){
		int y=e[x][i].to;if(y==fa)continue;
		findpoint1(y,x,nowd+e[x][i].d);
	}
}
void findpoint2(int x,int fa){//找到直径的另一个端点P2,顺便预处理每个点离P1的距离 
	if(is[x]&&diameter<len[x][0])P2=x,diameter=len[x][0];
	for(int i=0;i<e[x].size();i++){
		int y=e[x][i].to;if(y==fa)continue;
		len[y][0]=len[x][0]+e[x][i].d;
		findpoint2(y,x);
	}	
}
void makedis(int x,int fa,int nowd){//预处理每个点离P2的距离 
	len[x][1]=nowd;
	for(int i=0;i<e[x].size();i++){
		int y=e[x][i].to;if(y==fa)continue;
		makedis(y,x,nowd+e[x][i].d);		
	}
}
void choose_Root(){//找直径上的中点做root,那么每个点的最远路径必经过root 
	findpoint1(1,0,0);
	diameter=0;
	findpoint2(P1,0);
	makedis(P2,0,0);
	int midata=1e9;
	For(i,1,n){//在直径上选中点 
		if(len[i][0]+len[i][1]!=len[P1][1])continue;
		int d1=abs(len[i][0]-len[i][1]);
		if(d1<midata){midata=d1;root=i;}
	}
}

所以有什么用呢?我们可以把这个类中点提到树根位置,然后重新建树。这样子每个教徒要去树上距离它最远的修道院必须得经过树根。然后我们在dfs重新建树的时候预处理出一下东西:

  • (cnt[x]):以x为根的子树中含修道院的个数
  • (link[x]):以x为根的子树中的最大深度——也就是离树根的最长链长
  • (nums[x]):以x为根的子树中深度等于(link[x])的节点个数
  • (id[x]):我们断掉root后不是会形成一片森林吗,对森林中的每棵树进行编号,(id[x])就表示它属于哪棵树

至于有什么用,,先继续往下看

int cnt[N];//cnt[x]以x为根的子树中含修道院个数 
int link[N],nums[N];//link[x]:x以及其子孙中离根的最长距离,nums有几个 
int id[N]; 
void dfs(int x,int fa,int dis){
	if(fa==root)id[x]=x;
	else id[x]=id[fa];
	cnt[x]=is[x]?1:0;
	for(int i=0;i<e[x].size();i++){
		int y=e[x][i].to;if(y==fa)continue;
		dfs(y,x,dis+e[x][i].d);
		cnt[x]+=cnt[y];
	}
	if(cnt[x]){
		link[x]=dis,nums[x]=1;
		for(int i=0;i<e[x].size();i++){
			int y=e[x][i].to;if(y==fa)continue;
			if(link[y]>link[x])link[x]=link[y],nums[x]=nums[y];
			else if(link[y]==link[x])nums[x]+=nums[y];
		}
	}
}

那么我们可以开始yy分类讨论了。

(这一段的内容在上面关于(id[x])的定义中就提到过了)假设断掉root,这样就会形成森林(至少有两棵树,因为我们把类中点提做根),那么一棵树中的节点x想去到森林中距离它最远的点y,x必须从x所在的树到y所在的树。我们发现对答案真正有用的是含最长链的树,以及可能会用到含次长链的树——下面为了方便描述用A代替最长链,B代替次长链。

于是下面代码中在part1部分先去找A的长度ma1,B的长度ma2——(直接一起更新树的id也可以,但细节比较多)。

part2中,根据刚刚找到的ma1,ma2去找对应的树的编号id1,id2。其中注意几个细节:

  1. (ma1cnt):表示最长链个数,如果(ma1cnt>=3)那就表示,我们摧毁某个点x后,只能够让x子树中的教徒不开心,而对于其他所有教徒,他们至少还能够去一条最长链,他们不会不开心。
  2. if(link[y]==ma2&&!id2)id2=y这个if判断中注意(!id2),一开始没加在CF上wa了,因为这样会漏掉ma1==ma2的情况,也即A个数>=2的情况,而加上后,如果存在>=2条的A,我们就可以把id1,id2都赋上正确的树的编号。

part3中,我们对于每个(不是根节点&&没建修道院)的节点进行分类讨论,然后得出CNT——表示摧毁这个城镇能让多少教徒不开心,然后更新一下答案就可以了。至于分类讨论的细节详见下面注释,可以根据代码画画图自行yy:

int ma1=0,ma2=0,id1=0,id2=0,ma1cnt=0;
//part1
for(int i=0;i<e[root].size();i++){
	int y=e[root][i].to;
	if(!cnt[y])continue;
	if(link[y]>ma1)ma2=ma1,ma1=link[y];
	else if(link[y]>ma2)ma2=link[y];
}	
//part2
for(int i=0;i<e[root].size();i++){
	int y=e[root][i].to;
	if(!cnt[y])continue;
	if(link[y]==ma1)ma1cnt++,id1=y;
	if(link[y]==ma2&&!id2)id2=y;
}
if(ma1cnt>=3)id1=id2=0;//存在三条最长链
//part3
int ret1=0,ret2=0;
if(!is[root])ret1=m,ret2=1;//因为下面只包含对森林中每棵树中的节点的讨论,故如果能够摧毁根节点,要先单独提出来处理
for(int i=1;i<=n;i++){
	if(i==root||is[i])continue;
	int CNT=cnt[i],belong=id[i];//CNT=cnt[i]:一定能拦截子树中的所有修道院 	
	if(CNT&&(link[belong]==link[i])&&(nums[belong]==nums[i])){
		if(belong==id1){
			if(ma1!=ma2)CNT+=m-cnt[belong];//+=只有一条最长链->还可拦截(除了belong及其子孙)外的所有修道院 
			else CNT+=cnt[id2];//存在两条最长链,+=只有另一条最长链上的修道院会被拦截 
		}
		else if(belong==id2)CNT+=cnt[id1];//1条最长链,1条次长链 
	}
	if(CNT>ret1)ret1=CNT,ret2=1;
	else if(CNT==ret1)ret2++;
}
printf("%d %d
",ret1,ret2);

贴上完整代码

原文地址:https://www.cnblogs.com/Tieechal/p/11232287.html