3 条题解
-
0
#include<bits/stdc++.h> using namespace std; const int N=1e6+10; vector<int>G[N]; int siz[N],n,rt; void dfs(int x,int f) { siz[x]=1;int mx=0; for(int y:G[x])if(y!=f) { dfs(y,x); siz[x]+=siz[y]; mx=max(mx,siz[y]); } if(max(mx,n-siz[x])<=n/2)rt=x; } int top[N],a[N],v[N]; void dfs1(int x,int f,int tp) { siz[x]=1;top[x]=tp; for(int y:G[x])if(y!=f) dfs1(y,x,tp?tp:y),siz[x]+=siz[y]; } signed main() { cin>>n; for(int i=1;i<n;i++) { int x,y;cin>>x>>y; G[x].push_back(y); G[y].push_back(x); } dfs(1,0);dfs1(rt,0,0); int len=0;for(int y:G[rt])a[++len]=y; sort(a+1,a+len+1,[](int x,int y){return siz[x]>siz[y];}); int sum=0,p=0; while(n-sum>n/2)sum+=siz[a[++p]]; for(int i=1;i<=p;i++)v[a[i]]=1; for(int i=1;i<=n;i++) { if(i==rt) { cout<<0<<'\n'; continue; } int res=sum-(v[top[i]]?siz[top[i]]:siz[a[p]]); cout<<(n-siz[i]-res<=n/2?p-1:p)<<'\n'; } return 0; } -
0
妙哉妙哉,这里有一些题解没讲到的重要的东西。 :
一、问题分析
1.1 核心转化
题目中"到所有节点距离和最小的节点"就是树的重心(Centroid)。
重心的性质: 以重心为根时,每棵子树的大小都不超过 。反之,若以某节点 为根时所有子树大小都不超过 ,则 是重心。
因此问题转化为:对于每个节点 ,至少需要改变多少条边,使得以 为根时所有子树大小 。
1.2 操作模型
每次操作可以删除一条边、添加一条边(保持树的连通性)。将某棵子树从原位置断开、重新挂到另一个节点上,等价于1次操作。
二、算法思路
2.1 寻找当前重心
首先通过一次 DFS 找到树的重心
rt。以rt为根时,所有子树(称为"分支")的大小都 。2.2 计算关键参数
将
rt的所有分支按大小降序排列为 。定义 为满足以下条件的最小正整数:前 大分支的大小之和 (即 )。
的直观含义:如果要让
rt不再是重心(使其某个方向的节点数 ),至少需要"搬走" 个分支。2.3 对每个节点 计算答案
设 所在的分支为 , 是以
rt为根时 的子树大小。以 为根时,包含
rt的那个连通分量(称为"父侧分量")的大小为 。我们需要通过删边、加边操作,使得父侧分量被拆分成若干个大小 的子树。最优策略有两种:
- 策略 A(直接砍分支): 从
rt上砍掉若干分支(不能砍 ,因为 在里面),把它们重新挂到 下面。每次砍一个分支 ,父侧分量就减少 。 - 策略 B(砍 -
rt边 + 砍分支): 砍断 与rt之间的边,把rt挂到 下面。这样父侧分量被拆成两部分:rt及其剩余分支:大小为- 中 上方的部分:大小为 (一定 )
核心结论:答案一定是 或 。
三、正确性证明
3.1 为什么答案 ?( 次操作总是足够的)
当 在前 大分支中时,使用策略 B:
- 砍掉前 大分支中除了 以外的 个分支(总大小 )
- 砍断 -
rt边 - 把
rt挂到 上
此时 的子树有:
rt及其剩余分支:大小 $= n - siz[b] - (sum - siz[b]) = n - sum \leq \lfloor n/2 \rfloor$ ✅- 中 上方的部分:大小 $= siz[b] - siz[i] < siz[b] \leq \lfloor n/2 \rfloor$ ✅
- 砍掉的 个分支(挂到 下面):每个 ✅
共 次操作, 成为重心。
当 不在前 大分支中时,使用策略 A:
- 砍掉前 大分支(都不包含 ),总大小
- 父侧分量大小 $= n - siz[i] - sum \leq n - 1 - sum < n - sum \leq \lfloor n/2 \rfloor$ ✅
共 次操作, 成为重心。
3.2 为什么答案 ?( 次操作永远不够)
用策略 A 砍 个分支: 能砍的最大总量 (去掉最小的两个)。
由 的定义知:,所以:
$$sum - siz[a_k] - siz[a_{k-1}] < \lceil n/2 \rceil - siz[a_{k-1}] < \lceil n/2 \rceil$$而我们需要从父侧分量移走至少 $n - siz[i] - \lfloor n/2 \rfloor = \lceil n/2 \rceil - siz[i]$ 个节点。
由于 (排序性质),所需移走的节点数 。
但 个分支的总量 ,矛盾!所以策略 A 用 次操作不够。
用策略 B 砍 个分支 + 砍 -
rt边(共 次操作): 需要rt的剩余大小 ,即 。但 $S_{k-3} \leq sum - siz[a_k] - siz[a_{k-1}] - siz[a_{k-2}]$,利用 可推出:
$$S_{k-3} < \lceil n/2 \rceil + siz[a_k] - siz[a_k] - siz[a_{k-1}] - siz[a_{k-2}] < \lceil n/2 \rceil$$而 ,在大多数情况下 严格小于所需值,矛盾。
因此 次操作永远不够。
3.3 判断 是否足够的条件
用策略 A 砍 个分支(不包含 ),父侧分量大小为:
-
若 在前 大分支中:砍的是前 大中除 外的 个,总量 。 父侧分量 $= n - siz[i] - (sum - siz[b]) = n - sum + siz[b] - siz[i]$
-
若 不在前 大分支中:砍的是前 大分支,总量 。 父侧分量 $= n - siz[i] - (sum - siz[a_k]) = n - sum + siz[a_k] - siz[i]$
若该值 ,则 次操作足够;否则需要 次。
四、代码逐行解析
void dfs(ci x, ci f) { int mx = 0; siz[x] = 1; for (int y : g[x]) if (y ^ f) dfs(y, x), siz[x] += siz[y], mx = max(mx, siz[y]); mx = max(mx, n - siz[x]); if (mx <= (n >> 1)) rt = x; // 找重心 }标准求重心:递归计算子树大小,记录最大子树大小
mx。若mx ≤ n/2则为重心。void DFS(ci x, ci f, ci b) { siz[x] = 1, bel[x] = b; for (int y : g[x]) if (y ^ f) DFS(y, x, b ? b : y), siz[x] += siz[y]; }以重心
rt为根重新 DFS:siz[x]:以rt为根时 的子树大小bel[x]: 属于rt的哪个分支(用该分支的根节点编号标识)
for (int y : g[rt]) a[++m] = y; sort(a + 1, a + m + 1, cmp); // 按子树大小降序排列收集所有分支并按大小降序排序。
while (n - sum > (n >> 1)) sum += siz[a[++k]];计算 :不断累加最大分支,直到剩余节点数 。
for (int i = 1; i <= k; ++i) in[a[i]] = 1;标记前 大分支。
for (int i = 1; i <= n; ++i) { if (i == rt) cout << 0 << endl; else if (n - (sum - (in[bel[i]] ? siz[bel[i]] : siz[a[k]]) + siz[i]) <= (n >> 1)) cout << k - 1 << endl; else cout << k << endl; }对每个节点 :
- :已经是重心,输出 0
- 否则计算 次操作后的父侧分量大小:
- 若 在前 大中,去掉 (即保留 ,砍其余 个)
- 否则去掉 (即砍前 大分支)
- 若结果 ,输出 ;否则输出
五、复杂度分析
步骤 复杂度 求重心 DFS 二次 DFS 排序分支 计算 输出答案 总时间复杂度:,对于 在 2000ms 内完全可行。
空间复杂度:,满足 1024 MiB 限制。
六、样例验证
样例中 ,1 号点连接 2~10 号点(星形图)。
- 重心为 1(所有分支大小为 1)
- 9 个分支大小均为 1,降序排列后取前 个使 ,得
- 节点 1:输出 0
- 节点 2~10:
bel[i]在前 5 大中,,输出
输出完全匹配。
- 策略 A(直接砍分支): 从
-
0

#include<bits/stdc++.h> const int maxn = 1000035; const int maxm = 2000035; int n,root,cnt,leg,sum; int size[maxn],son[maxn],anc[maxn],ans[maxn]; int edgeTot,head[maxn],nxt[maxm],edges[maxm]; int read() { char ch = getchar(); int num = 0, fl = 1; for (; !isdigit(ch); ch=getchar()) if (ch=='-') fl = -1; for (; isdigit(ch); ch=getchar()) num = (num<<1)+(num<<3)+ch-48; return num*fl; } void addedge(int u, int v) { edges[++edgeTot] = v, nxt[edgeTot] = head[u], head[u] = edgeTot; edges[++edgeTot] = u, nxt[edgeTot] = head[v], head[v] = edgeTot; } void getRoot(int x, int fa, bool tag) { son[x] = 0, size[x] = 1; for (int i=head[x]; i!=-1; i=nxt[i]) { int v = edges[i]; if (v==fa) continue; getRoot(v, x, tag), size[x] += size[v]; son[x] = std::max(son[x], size[v]); } son[x] = std::max(son[x], n-size[x]); if (tag&&son[x] < son[root]) root = x; } bool cmp(int x, int y) { return size[x] > size[y]; } void dfs(int x, int fa, int c) { ans[x] = (leg-1)+((n-c-size[x])*2 > n); for (int i=head[x]; i!=-1; i=nxt[i]) if (edges[i]!=fa) dfs(edges[i], x, c); } int main() { memset(head, -1, sizeof head); n = read(); for (int i=1; i<n; i++) addedge(read(), read()); root = 0, son[root] = n; getRoot(1, 0, true); getRoot(root, 0, false); for (int i=head[root]; i!=-1; i=nxt[i]) anc[++cnt] = edges[i]; std::sort(anc+1, anc+cnt+1, cmp); for (int i=1; i<=cnt; i++) { sum += size[anc[i]], ++leg; if (sum*2 >= (n)) break; } for (int i=1; i<=cnt; i++) dfs(anc[i], root, sum-std::max(size[anc[i]], size[anc[leg]])); for (int i=1; i<=n; i++) printf("%d\n",ans[i]); return 0; }
- 1
信息
- ID
- 10100
- 时间
- 2000ms
- 内存
- 1024MiB
- 难度
- 10
- 标签
- 递交数
- 7
- 已通过
- 3
- 上传者