AHOI2022 回忆 - 自下向上之一
feecle6418 · · 题解
credit https://www.cnblogs.com/chenxiaoran666/p/Luogu8341.html
因为树上贪心一定要从下往上,考虑从下往上贪心。每个点只保留深度最浅的以其为下端点的限制。
称 路径 为直上直下的路径;称两条路径 匹配 为它们能连接起来成为一条路径。
首先,找出所有子树内没有限制下端点,自身又是限制下端点的点,容易证明:一定存在一种最优方案,所有路径的端点都属于这些点。
我们的目标是尽量少开始路径,尽量多匹配路径。
考虑在
- 尚未达到自己目标深度的路径。
- 已经达到目标深度,可以匹配的路径。
- 已经与匹配的有上有下的路径。
1 类路径只需要特殊处理目标深度最小的这一条,这一条可以用来满足
2,3 类路径可以互相转化(可以把 3 拆成两个 2)。合并两个子树
发现肯定是用
合并完子树后,处理
如果
否则,我们不得不新开一条路径来满足
整个做法时间复杂度
以下瞎扯一下正确性。这个算法自己有一些非常让人信服的点。
- 注意到每个限制我们尽量在深的地方拿出来配,越深配在之后能做的事情越多。
- 算法的部分内部都显然正确。
- 整个过程完全从下往上,符合“树上一定要从下往上而非从上往下”的教训(应该叫 经验)。
int n,m,a[200005],b[200005],d[200005],mn[200005],tag[200005],to[200005];
//a:已经匹配 b:尚未匹配
vector<int> g[200005];
void dfs1(int x,int fa){
d[x]=d[fa]+1;
for(int y:g[x]){
if(y==fa)continue;
dfs1(y,x);
}
}
void dfs2(int x,int fa){
for(int y:g[x]){
if(y==fa)continue;
dfs2(y,x),b[y]+=tag[d[x]],tag[d[x]]=0;
//y子树内延伸上来的,打在y上
if(mn[y]==d[x])b[y]++,mn[y]=0;
if(mn[x]&&mn[y]){
if(mn[x]<mn[y])tag[mn[y]]++;
else tag[mn[x]]++,mn[x]=mn[y];
}
else mn[x]|=mn[y];
if(b[x]<b[y])swap(b[x],b[y]),swap(a[x],a[y]);
if(b[y]+2*a[y]>=b[x]){
int w=b[x]+b[y]+2*a[y];
a[x]+=(w>>1),b[x]=(w&1);
}
else b[x]-=b[y]+2*a[y],a[x]+=b[y]+2*a[y];
}
if(to[x]){
if(!mn[x]){
mn[x]=to[x];
if(b[x])b[x]--;
else if(a[x])a[x]--,b[x]++;
}
else mn[x]=min(mn[x],to[x]);
}
}
void Solve(){
scanf("%d%d",&n,&m);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),g[x].push_back(y),g[y].push_back(x);
dfs1(1,0);
for(int i=1,x,y;i<=m;i++){
scanf("%d%d",&x,&y);
if(!to[y]||to[y]>d[x])to[y]=d[x];
}
dfs2(1,0);
cout<<a[1]+b[1]<<'\n';
for(int i=1;i<=n;i++)mn[i]=tag[i]=to[i]=a[i]=b[i]=0,g[i].clear();
}