P1084 疫情控制(二分答案,倍增,贪心)

P1084 疫情控制

题目描述

给定一棵树,一些点(不包括树根)上有军队,每条边有一个边权(移动时间),各个军队可以同时移动,求最小的移动时间,使得每条通往任意叶子结点的路径上至少都有一支军队.

题目分析

二分答案:

通过观察题意可知,答案具有单调性(如果pp小时时能控制所有的边境城市,那么q(q>p)q(q>p)小时时同样可以控制所有的边境城市),所以我们可以二分答案.


贪心:

通过以上的二分答案,我们把一个求值问题转化成了判定性问题,接下来我们要做的就是考虑如何判断我们当前二分出来的时间是否能够成立.

我们可以发现,离根节点越近的节点,能够控制的叶子越多. 所以我们将所有的军队在不超过当前二分出来的时间的前提下尽量往根节点跳. 如果不能跳了,就“就地驻扎”,控制该节点. 显然这样能控制到的叶子是最多的.

这里我们要明确一下,如果一支军队能到达一个根的儿子并且够越过根节点,那我们只是把他看成“暂时驻留”在该儿子上,而非“控制了该节点”. 原因后面会加以说明.

第一步:将每支军队尽量向树根移动

然后我们就会碰到一个问题:有一些叶子是不能被控制到的,而另外一些军队却可以通过根节点跳到其他的子树上,从而控制其他子树.

而且显然只要控制各个当前未被控制的叶子所在子树的根就行了. 控制这些根,所有的还未被控制的叶子就被控制了.

所以我们现在要做的就是:尝试将那些能够越过根节点到达其他子树的军队自身子树 还存在未被控制的叶子的 根的儿子一一配对,如果所有的根的儿子都能够与至少一支军队配对,那么当前二分出来的时间显然成立.

第二步:找到所有子树中包含未被控制的叶子的根的儿子(这一步可以通过dfs,O(n)O(n)实现)
第三步:尝试将这些根的儿子和可以越过根的军队相对应.

接下来我们要考虑第三步中如何对应的问题.

考虑对于一个到根所需的时间为timetime的根的儿子,如果它要被从其他子树来的军队控制,那么这个军队的剩余时间需要大于等于timetime.

这启示我们可以分别对根到儿子的时间军队越过根之后剩余的时间从大到小排序,然后将两者扫一遍,如果出现了根到儿子的时间大于军队越过根之后剩余的时间的情况,说明无法匹配,二分出来的时间不成立;否则成立.

为什么呢?比方说扫到kk位置时,k1k-1位置及以前的所有位置都已经匹配完成. 到这个位置时的 军队越过根之后剩余时间 已经是所能提供给这个 根的儿子 的最大值了,如果无法满足,说明其他所有的匹配方案都无法满足.


但是!

有没有考虑过一种情况:某一个节点上能够越过根节点的军队全部越过了根节点,但是这些军队跳走了之后,这个子树却不能被完全控制了.

回收悬念:这就是之前需要加以说明的原因

那我们怎么办?

我们发现,对于一个根的儿子,有两种情况:一是它的所有军队都跳走了,然后由其他子树的军队来控制它;二是它自身保留一支军队控制自身.

所幸我们以上的贪心总体思路仍然正确,排序仍然对需要跨子树的匹配成立. 也就是说,我们只需要多加考虑这种特殊情况即可.

先讲结论:如果扫到一个根的儿子,而且这个儿子上仍然有暂时驻扎的军队,那么我们就让这些军队中越过根节点后剩余时间最小的那支军队留下;否则没有暂时驻扎的军队的话,就另外找军队控制它.

我们考虑这个补丁的正确性. 首先,如果当前这个儿子上有暂时驻扎的军队,那么显然这些军队越过子树后剩余的时间都是小于我们从其他子树调来军队剩余的时间的.(由之前的由大到小排序显然)那么我们不如把这个其他子树调来的军队调到下一个根的儿子,这样可以使下一个根的儿子可以成功匹配的可能性更大. 另外一种情况:暂时驻扎的军队全部离开的情况,正确性实际上和一开始的贪心一样,在这里就不加以赘述了.

贪心总体做法:离根节点远的儿子由剩余路程大的军队来管辖,但是要优先由本来就在这棵子树上的军队来管辖. 所以我们先查看在子树x中,可以到达根节点,且到根节点后剩余路程最小的军队是否被使用(可以开一个桶记录这个儿子是否仍有未被使用的军队,因为是桶,由于排序后有单调性,就不需要记录剩余路程最小的军队了);如果被使用,再看当前没有被使用的军队里剩余路程最大的可否到达这棵子树.


倍增:

这东西很简单,就是在各个军队尽量向上跳时减少跳的步数,从而降低时间复杂度.

可能以上我对子树的说明不够清楚,语言表达能力还是欠缺啊。。。

程序实现

#include<bits/stdc++.h>
#define maxn 50010
#define ll long long
using namespace std;
struct edge{
	int v,next;ll w;
}e[maxn<<1];
int head[maxn],tot;
void add(int u,int v,ll w){
	e[++tot].v =v;e[tot].w =w;
	e[tot].next =head[u];head[u]=tot;
}
int fa[maxn][30];
ll dis[maxn][30];
void dfs1(int u,int pre,ll dist){
	fa[u][0]=pre;
	dis[u][0]=dist;
	for(int i=head[u];i;i=e[i].next ){
		int v=e[i].v ;
		if(v==pre)continue;
		dfs1(v,u,e[i].w );
	}
}//倍增预处理
ll l,r,ans;
int n,m,army[maxn];
struct node{
	int pos;ll len;
	bool operator <(node nd)const{
		return len<nd.len ;
	}
};
bool ck[maxn],vis[maxn];
int sum[maxn];
bool dfs(int u,int pre){
	if(vis[u]){
		ck[u]=true;
		return true;
	}
	ck[u]=true;bool flag=false;
	for(int i=head[u];i;i=e[i].next ){
		int v=e[i].v ;
		if(v==pre)continue;
		flag=true;
		if(!dfs(v,u))ck[u]=false;	
	}
	if(!flag){ck[u]=false;return false;}
	else return ck[u];
}//找到那些没有被完全控制的叶子所在的子树
bool check(ll x){
	memset(vis,false,sizeof vis);
	memset(sum,0,sizeof sum);
	//sum可以看成一个存各个根的儿子的剩余驻扎军队数量的桶
	memset(ck,false,sizeof ck);
	vector<node>g,f;g.clear();f.clear();
	//g,f分别存能够越过根的军队和需要匹配的根的儿子
	for(int i=1;i<=m;i++){
		int u=army[i];ll len=0;
		for(int j=20;j>=0;j--){
			if(fa[u][j]==1||len+dis[u][j]>x)continue;
			len+=dis[u][j];u=fa[u][j];
		}
		if(fa[u][0]==1&&len+dis[u][0]<x){
			g.push_back((node){u,x-len-dis[u][0]});
			sum[u]++;
		}
		else vis[u]=true;
	}
	if(dfs(1,1))return true;
	for(int i=head[1];i;i=e[i].next ){
		int v=e[i].v ;
		if(ck[v])continue;
		f.push_back((node){v,e[i].w });
	}//找到没有被封死的子树
	if(g.empty()||f.empty())return false;
	sort(f.rbegin(),f.rend());
	sort(g.rbegin(),g.rend());//由大到小排序
	int k=0;
	for(int i=0;i<f.size();i++){
		node now=f[i];
		if(sum[now.pos ]>0){
		//如果这个根的儿子仍然有军队暂时驻扎
			sum[now.pos ]--;
		}
		else {
		//如果没有军队驻扎
			while(sum[g[k].pos]==0){
				k++;
				if(k>=g.size())return false;
			}//跳过那些军队都被用完的根的儿子
			sum[g[k].pos ]--;
			if(g[k].len<f[i].len)return false;
			k++;if(k>=g.size()&&i<f.size()-1)return false;
		}
	}
	return true;
}
int main(){
	scanf("%d",&n);
	for(register int i=1,u,v;i<n;i++){
		ll w;
		scanf("%d%d%lld",&u,&v,&w);
		add(u,v,w);add(v,u,w);r+=w;
	}
	dfs1(1,1,0);
	for(int i=1;i<=20;i++){
		for(int j=1;j<=n;j++){
			fa[j][i]=fa[fa[j][i-1]][i-1];
			dis[j][i]=dis[j][i-1]+dis[fa[j][i-1]][i-1];
		}//dis表示到第2^i辈祖先所需的时间
	}
	scanf("%d",&m);
	for(register int i=1;i<=m;i++)scanf("%d",&army[i]);
	//哪些节点上有军队
	while(l<=r){
		ll mid=(l+r)>>1;
		if(check(mid))ans=mid,r=mid-1;
		else l=mid+1;
	}//二分答案
	printf("%lld\n",ans);
	return 0;
} 

特别鸣谢:思路提供 @litble

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章