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

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