树上最小 k 覆盖问题是一种很典型的树上贪心问题,这里做一下小结。
树上最小 k 覆盖问题的形式一般是:
给定一棵树,边有边权,点 有满足 的点权 ,称所有满足 的 为“关键点”。
你需要选中一些点,令所有的“关键点”被这些点覆盖。一个“关键点” 被“覆盖”了当且仅当存在一个你选中的点 ,满足 和 之间的距离小于等于 ,你需要最小化选中的点的数量。
这个问题有诸多变形,比较常见的是配合二分答案来考。
我们可以使用树上贪心来解决这个问题。
首先设 表示 子树内距离 最远的未被覆盖的关键点距 的距离,显然 。
考虑距离 最远的未被覆盖的关键点 , 显然有可能被 子树里的点覆盖,也有可能被 的父亲覆盖,或者被 的兄弟子树里的点覆盖。那么我们先考虑被 的子树里的点覆盖的情况,显然画出来时这样的:(黄色节点代表 ,绿色节点代表一个已经被选中且覆盖黄色节点的点 )

显然, 和 不可能是 的同一个儿子的子孙,因为那样的话 早就已经被 覆盖了。所以 和 之间的距离就是 和 的距离加上 和 的距离,也就是 加上 和 的距离。那么我们就要让 和 的距离尽可能短,不妨设它为 ,那么显然 。
接下来分类讨论:
-
若 ,那么 的子树能自给自足,这时 ,表示 的字树内没有关键点未被覆盖;
-
若 ,说明 必须被 覆盖了,这时需要选中 ,同时整棵 的子树都会被覆盖,则 ,,;
-
若 且 ,说明 是关键点且不能被它子树内之前选中的点覆盖,那么果断交给自己的父亲和兄弟子树,。
这三种情况有可能同时成立多种,但并不互相干扰,所以需要写成三个 if 并行。
最后注意若 说明还有关键点没被覆盖,等着 号节点的父亲和兄弟子树覆盖它们。但是 号节点没有父亲和兄弟,此时直接选中 号节点即可,。
例题代码如下:
#include <iostream>
#include <cstdio>
using namespace std;
const long long S=1000005,MS=300005;
const int inf=1e8;
int n,m,d[MS];
int esum,to[S],nxt[S],h[MS];
int tot,f[MS],g[MS];
inline void add(int x,int y)
{
to[++esum]=y;
nxt[esum]=h[x];
h[x]=esum;
}
void dfs(int u,int fa,int mid)
{
f[u]=-inf;
g[u]=inf;
for(int i=h[u];i;i=nxt[i])
{
int v=to[i];
if(v==fa)
{
continue;
}
dfs(v,u,mid);
f[u]=max(f[u],f[v]+1);
g[u]=min(g[u],g[v]+1);
}
if(f[u]+g[u]<=mid)
{
f[u]=-inf;
}
if(d[u]==1&&g[u]>mid)
{
f[u]=max(f[u],0);
}
if(f[u]==mid)
{
f[u]=-inf;
g[u]=0;
tot++;
}
}
inline bool check(int mid)
{
tot=0;
dfs(1,0,mid);
if(f[1]>=0)
{
tot++;
}
return tot<=m;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&d[i]);
}
for(int i=1;i<=n-1;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
int l=0,r=n,ans=-1;
while(l<=r)
{
int mid=l+r>>1;
if(check(mid))
{
ans=mid;
r=mid-1;
}
else
{
l=mid+1;
}
}
printf("%d\n",ans);
return 0;
}