[题解] 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; 
}
posted @ 2021-08-12 16:29  ¶凉笙  阅读(40)  评论(0编辑  收藏  举报