题目
题目概要
求一个序列
{
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
w≤105,
n
≤
1
0
5
n\le 10^5
n≤105 。
思路
用 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=1∑n[d(i)+d(pi)−2⋅d(lca)]=2i=1∑nd(i)−2i=1∑nd[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 ∑i∈son(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 size≤⌊2n⌋ 总成立。这就是重心的充要条件啊!
由于能够取到上界,所以 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;
}