[题解] POI2011DYN-Dynamite
特点
-
二分法适用于解决最小值最大化 or 最大值最小化的问题。
-
在难题里,二分可能就是一个优化时间的工具,然而,二分的思想是非常重要的。
适用于二分的题目:
- 二分通常是指二分答案,难点变成判断当前假定的答案能否满足题意。可以二分的题目答案必须有单调性。
例题
1.让我们来写一写POI的 DYN-Dynamite:传送门
题目大意:
给一棵树,书上有一些关键节点,要求你选m个点,使得关键节点到这些点中距离的最小值的最大值最小,求这个值。
题目分析:
这道题有一点就是树上dp的意思,借助 zky 学长的思路:
- 树上dp一般都是维护以某个结点为根的子树的状态
采用二分思想,将问题转化成:
一棵树上选 tot 个点,使得这 tot 个点与关键节点的最大距离不超过mid,最小化 tot。
也就是用最少的点覆盖所有关键点,是一个最小点覆盖问题。
-
接下来怎么解决呢?我们发现,如果一棵子树的根 root 到最远的关键节点的距离小于 mid ,那意味着这整棵子树是可以被这个 root 覆盖的.并且如果以这种方式自底而上递归,一定能够得出一种方案来解决这个问题,但这个方案很明显不是最优解。
-
为什么呢?经过观察,我们可以发现,如果每次贪心的采取这种方式,我们每次会重复覆盖一些点,这样就会得不到最优解,那怎么办呢?我们可以记录下每次未被之前覆盖的关键节点到 root 的最远距离,这样就可以减少掉多余的覆盖。
- 为什么说叫做减少多余的覆盖,而不是消除所有多余的覆盖呢?经过思考,我们可以发现,我们覆盖整颗子树 root 时,不一定会选定 root 作为用来覆盖这棵子树的点,我们可能会使用 root 下的子孙 y 来覆盖整颗子树,从而得到更优解。i
因此无非三种情况,对于某个结点 i 和以他为根的子树 t :
1.t被 i 的儿子覆盖。
2.t被 i的兄弟子树中的结点覆盖。(这个可能在 1 这种情况下就已经解决了)
3.t被它的祖先所覆盖。(同上)
因此,1才是本题的唯一情况,不需要想太复杂(我也是想了半天)
- 根据上面的思路,我们可以定义f[i]表示距 i 最远的未被覆盖的关键结点到 i 的距离,g[ i ]表示 i 到该子树下的关键点的最小距离
DP过程即:f[ x ] = max(f[ y ] + 1),g[ x ] = min(g[ y ] + 1)。
难道这就完了么?那d[ i ]有什么卵用?
-
当f[ x ]+g[ x ]<=mid,说明只用已选定节点中到 x 距离最小的点就能够覆盖到最远的结点,整棵子树自然可以被完全覆盖。就有f[ x ] = -INF(想想上面第1种情况,有可能是从x的一棵子树跨到另一棵子树)
-
当f[ x ] = mid,说明最远的关键结点到根的距离恰好为 mid ,如果我们不选用 x 结点为关键节点显然是错误的,所以f[ x ] = -INF,g[ x ] = 0,++tot。
-
当g[ x ] > mid && d[ i ] = 1 时,说明了什么?说明他的儿子们已经无法覆盖整棵子树,它的下面没有关键节点,留给他的父亲来帮忙,或者仍然保留最大距离,此时f[ x ] = max(f[ x ] , 0)(刘汝佳神问:想一想,为什么?)
伪代码如下:
++tot;//from zky
void dfs(int x){
for(son : y){
dfs(y);
}
for(son : y){
deep1=min(g[y])+1;
deep2=max(f[y])+1;
}
if(deep1+deep2<=mid) f[x]=0;
else f[x]=deep2;
//f[x] g[x]
if(f[x]==mid) {
++tot;
f[x]=0;g[x]=0;
}else{
g[x]=deep1;
}
}
完整代码如下:
//Shiyan Wang
//POI 2011
#include <iostream>
#include <cstdio>
#include <algorithm>
#define re register
#define ll long long
using namespace std;
const int INF = 1e8,maxn=3e5+5;
struct tree{
int to,nxt;
}e[maxn<<1];
int n,m,cnt=0,ans=0,tot=0,d[maxn],head[maxn];
ll f[maxn],g[maxn];
inline void link(int u,int v){
e[++cnt].to=v;e[cnt].nxt=head[u];head[u]=cnt;
}
inline void dfs(int u,int fa,int mid){
f[u]=-INF;g[u]=INF;//别忘了初始化
for(re int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
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(g[u]>mid&&d[u]==1){f[u]=max(f[u],(ll)0);}
if(f[u]==mid){f[u]=-INF;++tot;g[u]=0;}
}
inline bool check(int mid){
tot=0;
dfs(1,-1,mid);
if(f[1]>=0)tot++;//特判根,如果根的f值大于等于一,说明他可能也是关键节点还在等待覆盖,直接计数器加一
return tot<=m;
}
int main(){
scanf("%d%d",&n,&m);
for(re int i=1;i<=n;i++)scanf("%d",d+i);
for(re int i=1;i<n;i++){
int u,v;
scanf("%d%d",&u,&v);
link(u,v);link(v,u);
}
int l=0,r=n;//二分长度mid
while(l<=r){
int mid=l+r>>1;
if(check(mid))r=mid-1,ans=mid;
else l=mid+1;
}
cout<<ans<<endl;
return 0;
}