[CF468D]Tree

98 篇文章 0 订阅

题目

传送门 to CF

传送门 to luogu

题目概要
求一个序列 { p i } \{p_i\} {pi} ,使得 ∑ i = 1 n d i s t ( i , p i ) \sum_{i=1}^{n}{\tt dist}(i,p_i) i=1ndist(i,pi) 最小。输出字典序最小的解。

数据范围与约定
边权 w ≤ 1 0 5 w\le 10^5 w105 n ≤ 1 0 5 n\le 10^5 n105

思路

d ( i ) d(i) d(i) 表示点 i i i 的深度,以此来去掉 d i s t \tt dist dist 函数。写出式子

a n s = ∑ i = 1 n [ d ( i ) + d ( p i ) − 2 ⋅ d ( l c a ) ] = 2 ∑ i = 1 n d ( i ) − 2 ∑ i = 1 n d [ l c a ( i , p i ) ] \begin{aligned} ans&=\sum_{i=1}^{n}[d(i)+d(p_i)-2\cdot d(lca)]\\ &=2\sum_{i=1}^{n}d(i)-2\sum_{i=1}^{n}d\big[lca(i,p_i)\big] \end{aligned} ans=i=1n[d(i)+d(pi)2d(lca)]=2i=1nd(i)2i=1nd[lca(i,pi)]

注意这一点:树根不影响树上距离,所以说 树根只需要随便确定一个即可。不想清楚这一点,就会对后面确定树根的操作感到疑惑——确定树根的目的只是构造解,树根不影响答案。

任意取一个树根 r o o t root root ,答案的上界是 l c a lca lca 始终为 r o o t root root 。也就是说, i i i p i p_i pi 不是同一个儿子的子树,或者其中一个是根(甚至两个都是根)。

说明:接下来的叙述中,“子树”代表的是根节点的某个儿子的子树,并将根单独视为一棵子树。如果不特殊说明,则 ∑ s t h \sum sth sth 均视作 ∑ i ∈ s o n ( r o o t ) s t h \sum_{i\in son(root)}sth ison(root)sth

⟨ i , p i ⟩ \langle i,p_i\rangle i,pi 视为一条树上路径,那么我们用 f ( x ) f(x) f(x) 表示子树内未匹配的出度数量,用 g ( x ) g(x) g(x) 表示子树内未匹配的入度数量。此时, l c a lca lca 能够始终为 r o o t root root 的充要条件是:对于任意一棵不是根的子树 t t t ,因为其出度都不得不与子树外的入度匹配,所以 f ( t ) ≤ ∑ i ≠ t g ( i ) f(t)\le\sum_{i\ne t}g(i) f(t)i=tg(i) ,也即,

f ( t ) + g ( t ) ≤ ∑ g ( i ) f(t)+g(t)\le\sum g(i) f(t)+g(t)g(i)

在任意时刻, ∑ f ( i ) = ∑ g ( i ) \sum f(i)=\sum g(i) f(i)=g(i) 。所以说,能够满足 f ( t ) + g ( t ) = ∑ g ( i ) f(t)+g(t)=\sum g(i) f(t)+g(t)=g(i) t t t 至多有两个(如果有两个,则只有这两个子树满足 f ( i ) 2 + g ( i ) 2 ≠ 0 f(i)^2+g(i)^2\ne 0 f(i)2+g(i)2=0 ,也就是说,其他的子树都“用光”了)。

如果满足这个条件,我们每次找到使 f ( t ) + g ( t ) f(t)+g(t) f(t)+g(t) 最大的一个 t t t ,并将子树内一点与另一棵子树内一个点匹配。这样一来, f ( t ) + g ( t ) f(t)+g(t) f(t)+g(t) 减小了一,而 ∑ g ( i ) \sum g(i) g(i) 也减小了一,所以一定可行。

怎么确定这个 r o o t root root 呢?答案是重心。在最初的时候, ∑ g ( i ) = 2 n \sum g(i)=2n g(i)=2n ,且 f ( i ) + g ( i ) = 2 × s i z e f(i)+g(i)=2\times size f(i)+g(i)=2×size ,所以目的就是 s i z e ≤ ⌊ n 2 ⌋ size\le\lfloor\frac{n}{2}\rfloor size2n 总成立。这就是重心的充要条件啊!

由于能够取到上界,所以 r o o t root root 为重心时,可以用此方法构造出一个答案最大的情况。

接下来的目的是找到字典序最小的一个。考虑一个一个的确定 p p p

如果其中一个子树 t t t 满足 f ( t ) + g ( t ) = ∑ g ( i ) f(t)+g(t)=\sum g(i) f(t)+g(t)=g(i) ,并且 i i i 不属于子树 t t t ,那么 p i p_i pi 必须属于子树 t t t 。其他情况下,只需要满足 p i p_i pi i i i 不在同一棵子树中。

这可以用 s e t \tt{set} set 很简单的维护,毕竟 ∑ g ( i ) = n − ( \sum g(i)=n-( g(i)=n( 已经匹配的数量 ) ) ) 。时间复杂度 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn)

代码

#include <cstdio>
#include <iostream>
#include <vector>
#include <set>
#include <queue>
using namespace std;
inline int readint(){
	int a = 0; char c = getchar(), f = 1;
	for(; c<'0'||c>'9'; c=getchar())
		if(c == '-') f = -f;
	for(; '0'<=c&&c<='9'; c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}

const int MaxN = 100005;

struct Edge{
	int to, nxt, val;
	Edge(int T=0,int N=0,int V=0){
		to = T, nxt = N, val = V;
	}
};
Edge edge[MaxN<<1];
int head[MaxN], n, cntEdge;
void addEdge(int a,int b,int c){
	edge[cntEdge] = Edge(b,head[a],c);
	head[a] = cntEdge ++;
	edge[cntEdge] = Edge(a,head[b],c);
	head[b] = cntEdge ++;
}

void input(){
	n = readint();
	for(int i=1; i<=n; ++i)
		head[i] = -1;
	for(int i=1,a,b; i<n; ++i){
		a = readint(), b = readint();
		addEdge(a,b,readint());
	}
}

int bel[MaxN]; // 每个点属于哪个儿子子树 
int siz[MaxN], root;
long long ans;
/** @brief 三合一:求答案、赋值bel、找重心 */
void dfs(int x,int pre,long long depth,int b){
	ans += depth<<1, bel[x] = b, siz[x] = 1;
	for(int i=head[x]; ~i; i=edge[i].nxt)
		if(edge[i].to != pre){
			dfs(edge[i].to,x,depth+edge[i].val,b);
			siz[x] += siz[edge[i].to];
		}
	if(root == 0){ // 确定重心
		int mx = n-siz[x];
		for(int i=head[x]; ~i; i=edge[i].nxt)
			if(edge[i].to != pre)
				mx = max(mx,siz[edge[i].to]);
		if(mx <= (n>>1)) root = x;
	}
}

set< pair<int,int> > pq; // 找 f+g 的最大值
set<int> tree[MaxN]; // 每个儿子子树中的所有点
set<int> all; // 每颗子树的最小值(用以构造字典序小的解)
int deg[MaxN];
void solve(){
	dfs(1,-1,0,-1); // 找重心 
	ans = 0, dfs(root,-1,0,-1); // 算答案 
	printf("%lld\n",ans);
	for(int i=head[root]; ~i; i=edge[i].nxt)
		dfs(edge[i].to,root,0,edge[i].to); // 赋值bel 
	bel[root] = root; // 自成一棵子树 
	for(int i=1; i<=n; ++i)
		tree[bel[i]].insert(i);
	all.insert(root); // "根子树" 
	for(int i=head[root]; ~i; i=edge[i].nxt){
		int x = edge[i].to;
		deg[x] = tree[x].size()<<1;
		pq.insert(make_pair(deg[x],x));
		all.insert(*tree[x].begin());
	}
	for(int i=1,match; i<=n; ++i){
		auto it = pq.end(); if(not pq.empty()) -- it;
		/* 箭在弦上(f+g=n-已匹配 且 i不属于该子树),不得不发 */
		if(not pq.empty() and it->first == n-i+1 and it->second != bel[i])
			match = *tree[it->second].begin();
		else{ // 构造字典序最小
			auto j = all.begin();
			if(bel[*j] == bel[i] and i != root and *j != root)
				++ j; // invalid
			match = *j;
		}
		printf("%d ",match);
		tree[bel[match]].erase(match);
		all.erase(match);
		pq.erase(make_pair(deg[bel[i]],bel[i]));
		pq.insert(make_pair(--deg[bel[i]],bel[i]));
		pq.erase(make_pair(deg[bel[match]],bel[match]));
		pq.insert(make_pair(--deg[bel[match]],bel[match]));
		if(not tree[bel[match]].empty())
			all.insert(*tree[bel[match]].begin());
	}
	puts("");
}

int main(){
	input(), solve();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值