「雅礼集训 2017 Day7」跳蚤王国的宰相(树的重心)

题面

来源


「 雅 礼 集 训 2017 D a y 7 」 跳 蚤 王 国 的 宰 相   传 统       2000   m s     1024   M i B {\tt「雅礼集训 2017 Day7」跳蚤王国的宰相}\\ \,_{传统~~~~~2000\,{\tt ms}~~~1024\,{\tt MiB}} 2017Day7     2000ms   1024MiB

题目描述

跳蚤王国爆发了一场动乱,国王在镇压动乱的同时,需要在跳蚤国地方钦定一个人来做宰相。

由于当时形势的复杂性,很多跳蚤都并不想去做一个傀儡宰相,带着宰相的帽子,最后还冒着被打倒并杀头的危险,然而有一只跳蚤却想得与众不同最时尚。

本来他打算去教书,他已经发表了自己在学术方面的见解,获得了很多跳蚤们的赞同,但是这时听说跳蚤国要钦定宰相,他毅然打断了想去教书的想法,他觉得只要为了国家利益,自己的生死都可以不管,哪里能因为工作能给自己带来灾祸或者福分就去避开或者接近这份工作呢?所以他决定站出来接了这份工作。

然而当时国王的钦定方式很奇怪,跳蚤王国可以看作一棵树,国王认为宰相必须更好的为跳蚤服务,所以他会选择一个到所有节点距离和最小的节点,并在这个节点中钦定,如果有多个节点满足距离和最小则任选一个。

然而跳蚤国的动乱实在是太厉害了,以至于树的形态可能也会发生改变,也就是说,树上可能会有若干条边消失,如果这个情况出现的话一定会有同样数目的边出现,以保证整个结构仍然是一棵树

现在这个跳蚤想知道每个节点中的跳蚤如果要被钦定,至少需要多少条边消失(当然也会有同样数目的边出现)。作为这只跳蚤的一名真正的粉丝,你能帮他解决这个问题吗?

输入格式

第一行一个正整数 n \tt n n 表示树中点的个数。
接下来 n − 1 \tt n-1 n1 行,每行两个正整数 u \tt u u v \tt v v,表示点 u \tt u u 与点 v \tt v v 之间有一条树边。

输出格式

输出 n \tt n n 行,第 i \tt i i 行一个数,表示第 i \tt i i 个节点如果要被钦定至少需要多少条边消失。

样例

输入

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

输出

0
4
4
4
4
4
4
4
4
4

数据范围与提示

对于 10 % \tt10\% 10% 的数据, n ≤ 10 \rm n\leq10 n10
对于 40 % \tt40\% 40% 的数据, n ≤ 2000 \rm n\leq2000 n2000
对于 70 % \tt70\% 70% 的数据, n ≤ 100000 \rm n\leq100000 n100000
对于 100 % \tt100\% 100% 的数据, 10 ≤ n ≤ 1000000 \rm 10\leq n\leq1000000 10n1000000

L i b r e O J   P o w e r e d   b y   S Y Z O J   N G _{{\rm LibreOJ}~{\tt Powered~by}~{\rm SYZOJ~NG}} LibreOJ Powered by SYZOJ NG


题解

首先,不难发现,到所有节点距离和最小的节点,就是树的重心。

用调整法证明,如果不是重心的话,往重心方向移动,一定能使到所有节点距离和减小。

然后,就可以利用很多重心的特性了。重心的定义是连出去的子树大小都不大于 n / 2 \rm n/2 n/2

断边加边,实际上只需要考虑断边,然后断掉的部分肯定接在目标点上最优。


如果有两个重心,将会变得非常简单。两个重心一定是一条边相连的相邻的两个点,这条边的两端,各有 ≤ n / 2 \rm\leq n/2 n/2 个点。

对于本就是重心的点,答案是 0,对于其他点,只需要把两个重心之间的边断掉,然后把另一部分接在自己头上,就一定能满足要求了,因此答案是 1 。


如果只有一个重心,那我们就根据这个重心来考虑。设这个重心为 h v \rm hv hv,以它为根处理一下这棵树每个点的 s i z \rm siz siz (子树大小)。

我们发现每个点只会有一个儿子的子树不符合要求,且 h v \rm hv hv 一定在这个子树里。那么我们就只需要断掉不同的与 h v \rm hv hv 相邻的边,就可以解决问题。如果不是断掉这些边,那么接上后肯定不会很优。

把与 h v \rm hv hv 相邻的子树都处理出大小、成员,按大小排个序,然后处理出前缀和。假设当前的点 i \rm i i 所在的大子树 p \rm p p ,那么我们需要从大到小考虑断掉一些子树,再接上去,保证这个点可以做重心,即断掉过后的 S I Z − s i z [ i ] ≤ n / 2 \rm SIZ-siz[i]\leq n/2 SIZsiz[i]n/2

既然从大到小考虑,那么除开 p \rm p p 本身的影响,选的应该是从大到小的连续一段,所以可以二分。

这里有一个坑点,困扰了我八个月。要使点 i i i 成为重心,可以不动子树 p \rm p p ,把其它的子树进行二分求出答案。也可以有第二种方案:把子树 p \rm p p 的那条边断掉,然后把 h v \rm hv hv 加上除了 p \rm p p 以外的子树——这一大坨,全部接到 i \rm i i 上,接着再考虑二分、选其它的子树断掉,这样,再最终答案+1 的情况下,就只需要满足断掉后的 S I Z − s i z [ p ] ≤ n / 2 \rm SIZ-siz[p]\leq n/2 SIZsiz[p]n/2 了, s i z [ p ] \rm siz[p] siz[p] 更大,条件要宽松些。

两种方案都试试,然后择优输出就是了。时间复杂度 O ( n log ⁡ n ) \rm O(n\log n) O(nlogn)

CODE

#include<vector> 
#include<cstdio> 
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
#define MAXN 1000005
#define LL long long
#define DB double
#define ENDL putchar('\n')
LL read() {
	LL f = 1,x = 0;char s = getchar();
	while(s < '0' || s > '9') {if(s=='-')f = -f;s = getchar();}
	while(s >= '0' && s <= '9') {x=x*10+(s-'0');s = getchar();}
	return f * x;
}
const int MOD = 1000000007;
int n,m,i,j,s,o,k;
vector<int> g[MAXN];
int fa[MAXN],siz[MAXN];
bool hv[MAXN];
vector<int> sb[MAXN];
int b[MAXN];
void dfs(int x,int fat) {
	fa[x] = fat;
	siz[x] = 1;
	hv[x] = 1;
	for(int i = 0;i < (int)g[x].size();i ++) {
		if(g[x][i] != fat) {
			dfs(g[x][i],x);
			if(siz[g[x][i]] > n/2) hv[x] = 0;
			siz[x] += siz[g[x][i]];
		}
	}
	if(n-siz[x] > n/2) hv[x] = 0;
	return ;
}
void dfs2(int x,int fat,int id) {
	sb[id].push_back(x);
	siz[x] = 1;
	for(int i = 0;i < (int)g[x].size();i ++) {
		if(g[x][i] != fat) {
			dfs2(g[x][i],x,id);
			siz[x] += siz[g[x][i]];
		}
	}return ;
}
int SZ(int x) {return (int)sb[b[x]].size();}
vector<int> bu[MAXN];
int AS[MAXN],sm[MAXN],cnt,hp;
int SM(int l,int x) {
	return sm[l] - (x >= l ? SZ(x):0);
}
int CNT(int l,int x) {
	return cnt-l+1 - (x >= l ? 1:0);
}
int bel[MAXN];
int main() {
	n = read();
	for(int i = 1;i < n;i ++) {
		s = read();o = read();
		g[s].push_back(o);
		g[o].push_back(s);
	}
	dfs(1,0);
	cnt = 0,hp = 0;
	for(int i = 1;i <= n;i ++) {
		if(hv[i]) cnt ++,hp = i;
	} 
	if(cnt > 1) {
		for(int i = 1;i <= n;i ++) {
			if(hv[i]) printf("0\n");
			else printf("1\n");
		}
		return 0;
	}
	cnt = 0;
	for(int i = 0;i < (int)g[hp].size();i ++) {
		dfs2(g[hp][i],hp,++ cnt);
		bu[(int)sb[cnt].size()].push_back(cnt);
	}
	cnt = 0;
	for(int i = 1;i <= n;i ++) {
		for(int j = 0;j < (int)bu[i].size();j ++) {
			b[++ cnt] = bu[i][j];
		}
	}
	sm[cnt+1] = 0;
	for(int i = cnt;i > 0;i --) sm[i] = sm[i+1] + SZ(i);
	for(int i = 1;i <= cnt;i ++) {
		for(int j = 0;j < (int)sb[b[i]].size();j ++) {
			int p = sb[b[i]][j];
			int SIZ = siz[p];
			int ad = 1,ad2 = 1;
			for(int k = 20;k >= 0;k --) {
				if(ad+(1<<k) <= cnt+1 && n-SIZ-SM(ad+(1<<k),i) <= n/2) {
					ad += (1<<k);
				}
			}
			for(int k = 20;k >= 0;k --) {
				if(ad2+(1<<k) <= cnt+1 && n-SZ(i)-SM(ad2+(1<<k),i) <= n/2) {
					ad2 += (1<<k);
				}
			}
			AS[p] = min(CNT(ad,i),CNT(ad2,i)+1);
			bel[p] = i;
		}
	}
	for(int i = 1;i <= n;i ++) {
		printf("%d\n",AS[i]);
	}
	return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值