#4342. CF348 Pilgrims
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
。其中注意几个细节:
- (ma1cnt):表示最长链个数,如果(ma1cnt>=3)那就表示,我们摧毁某个点x后,只能够让x子树中的教徒不开心,而对于其他所有教徒,他们至少还能够去一条最长链,他们不会不开心。
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);