CF1914F Programming Competition
- 我觉得这个题的难度比 \(1900\) 要大吧,感觉细节挺多的
-
先说一个官方题解的做法
-
我们从上到下的考虑,对于以 \(u\) 为根时我们把 \(u\) 的所有儿子的 \(siz_x\) 记录下来,让不同儿子的 \(siz_x\) 两两匹配,这里会分成两种情况
-
如果 \(siz_{mx} \leq siz_u-1-siz_{mx}\),则我们一定可以把这些合并成 \(\lfloor \frac{siz_u-1}{2} \rfloor\) 对
-
如果 \(siz_{mx} > siz_u-1-siz_{mx}\),那我们在把 \(mx\) 和其他所有儿子合并后依然还剩下 \(siz_{mx}-(siz_u-1-siz_mx)\) 个没有合并,而且都在 \(mx\) 子树里,这时候我们就递归到 \(mx\) 里继续寻找答案
-
-
现在的细节就是我们继续寻找答案的时候我们要以什么方式去匹配
-
答案是优先匹配祖先节点的
-
考虑如果存在一种最优解是先考虑了 \(mx\) 子树内的匹配再去考虑 \(mx\) 和祖先的匹配
-
如果祖先还能继续匹配的个数是偶数,那我们把 \(mx\) 的一对匹配拆开匹配给祖先显然等价
-
如果祖先还能继续匹配的个数为奇数,那我们如果把 \(mx\) 的一对匹配拆开后把一个和祖先匹配,另一个剩下,到达这个祖先节点的父节点时两者没有被匹配的节点数是相同的,也就是说两者匹配数相同,依然是最优解
-
而且我们变换为先匹配祖先节点的原因是先考虑子孙节点比较“片面”,典型的就是第 \(5\) 个样例,如果先考虑子孙节点就会出现 \((5,6),(2,3)\) 这样的组合,答案容易更劣;而考虑祖孙节点更“整体”
-
现在我们既然递归到 \(mx\) 里,我们就需要把祖孙节点和他匹配的情况给挑出来。我们在递归进去时记一个 \(K\) 表示 \(mx\) 的祖先节点已经用掉了 \(K\) 个点,因此情况变为:
-
如果 \(siz_{mx}-k \leq siz_u-1-siz_{mx}\),则我们一定可以把这些合并成 \(\lfloor \frac{siz_u-1}{2} \rfloor\) 对
-
如果 \(siz_{mx}-k > siz_u-1-siz_{mx}\),那我们在把 \(mx\) 和其他所有儿子合并后依然还剩下 \(siz_{mx}-(siz_u-1-siz_mx)\) 个没有合并,而且都在 \(mx\) 子树里,这时候答案为 \(siz_u-1-siz_mx+solve(mx,\max(k+siz_u-1-siz[mx]-1,0))\)
-
-
其中注意的是 \(mx\) 子树内是没有节点和 \(mx\) 自身匹配的,因此当往下递归时要让 \(K\) 减一,不用白不用
-
最终复杂度为 \(O(n)\)
#include<bits/stdc++.h>
#define ll long long
using namespace std;
template<typename T>T &read(T &x){
cin>>x;
return x;
}
const int maxn=2e5+50;
int n;
struct E{int v,nxt;}e[maxn];
int hd[maxn],cnt=1;
int siz[maxn];
void ade(int u,int v){
e[++cnt]=E{v,hd[u]};
hd[u]=cnt;
}
void dfs1(int u){
siz[u]=1;
for(int i=hd[u];i;i=e[i].nxt){
int v=e[i].v;
dfs1(v);
siz[u]+=siz[v];
}
}
int solve(int u,int k){
int mx=-1;
for(int i=hd[u];i;i=e[i].nxt){
int v=e[i].v;
if(mx==-1||siz[v]>siz[mx]){
mx=v;
}
}
if(siz[u]==1){
return 0;
}
if(siz[mx]-k<=siz[u]-1-siz[mx]){
return (siz[u]-1-k)/2;
}
return siz[u]-1-siz[mx]+solve(mx,max(k+siz[u]-1-siz[mx]-1,0));
}
void mian(int TwT){
read(n);
int fa;
for(int i=2;i<=n;++i){
read(fa);
ade(fa,i);
}
dfs1(1);
printf("%d\n",solve(1,0));
}
void init(){
for(int i=1;i<=n;++i){
hd[i]=0;
}
cnt=1;
}
int main(){
int T=1;
read(T);
for(int TwT=1;TwT<=T;++TwT){
init();
mian(TwT);
}
return 0;
}
-
还有洛谷大佬的第二种方法
-
由于要求 \((u,v)\) 中 \(u\) 和 \(v\) 必须不互为祖先,容易想到贪心只要选两个叶子节点就行了。
-
因为如果 \((u,v)\) 不互为祖先,那么选一个 \(u\) 的子节点 \(u_0\),\((u_0,v)\) 满足条件且更优。
-
会发现,取两个深度最大的节点时,能保证这颗树剩下的节点中深度尽量平衡,也就是能构成祖先的 \((u,v)\) 会更少,故贪心可以那么做。
-
复杂度会多优先队列带来的一个 \(\log\)