1. 程式人生 > 其它 >題解 #10069.【國家集訓隊Tree】

題解 #10069.【國家集訓隊Tree】

[國家集訓隊]Tree

題目大意:

給你一個無向帶權連通圖,每條邊是黑色或白色。讓你求一棵最小權的恰好有 \(need\) 條白色邊的生成樹。

solution:

考慮 \(\text{Kruskal}\) 的演算法流程:每次取邊權最小的邊添到生成樹裡,所以為了讓白色邊進入生成樹,我們可以將白色邊的邊權改變一下,但是改變多少呢?這個就可以用二分答案,再驗證。單調性後面證。

具體操作如下:

  1. 二分出當前 \(mid\) ,把所有白色邊的邊權都減去 \(mid\)
  2. 構建最小生成樹,記錄被加入到生成樹的白邊的個數,同時記錄答案。
  3. 若白邊個數 \(\geq need\) 則統計答案:\(ans=res+need \times mid\)
    (把減去的 \(mid\) 加回來,共 \(need\) 條)。
  4. 把所有邊權恢復,準備進行下一次二分。

單調性證明:

貪心的想,白色邊的邊權越小,則被加入到生成樹的邊數越多(顯然),這樣就滿足了單調性。

證畢[滑稽]。

細節處理:

  • 這裡的白邊權有可能小導致生成樹中白邊過多,所以我們要增加權值,所以二分的左邊界 \(l\) 要從 \(-101\)\(< -100\) 的數),右邊界 \(r\) 要取一個大於 \(100\) 的數。
  • 答案 \(ans\) 一定要用最小生成樹的 \(res+\) 多減去的邊權,不可直接用 \(res+=need \times mid\)

感性理解下:

若倒數第二次的二分答案驗證成功,\(ans\)

被更新。而最後一次的驗證失敗,以後一種寫法就會出問題:在求最小生成樹時 \(ans\) 被清零,沒法儲存上次成功的答案,且此次答案不合法,從而丟了最優解。

程式碼
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=5e4+5,M=1e5+5;
int n,m,need; 
struct B{
	int x,y,z,c;
}e[M];
inline bool cmp(B x,B y){
	if(x.z==y.z) return x.c<y.c;//這也要注意:若此時邊權相等,優先選擇白色的邊
	else		 return x.z<y.z;
}
int fa[N];
inline int find(int s){
	return fa[s]==s?s:fa[s]=find(fa[s]);
}
int ans,res,num;
inline void kru(){
	sort(e+1,e+m+1,cmp);
	res=0,num=0;
	int cnt=0;
	for(int i=1;i<=m;i++){
		int x=e[i].x,y=e[i].y,z=e[i].z;
		int col=e[i].c;
		x=find(x),y=find(y);
		if(x!=y){
			fa[x]=y;
			if(!col) num++;//白色邊數++
			res+=z;
			cnt++;
		}
		if(cnt==n-1) return ;
	}
}
int main(){
	scanf("%d%d%d",&n,&m,&need);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d%d",&e[i].x,&e[i].y,&e[i].z,&e[i].c);
		e[i].x++,e[i].y++;
	}
	int l=-101,r=110;
	while(l<=r){
		int mid=l+r>>1;
		for(int i=1;i<=n+1;i++) fa[i]=i;
		for(int i=1;i<=m;i++) 
			if(!e[i].c) e[i].z-=mid;
		kru();
		if(num>=need) {//答案可行,減少白邊數量
			r=mid-1;
			ans=res+need*mid;//注
		}
		else 		  l=mid+1;
		for(int i=1;i<=m;i++)
			if(!e[i].c) e[i].z+=mid;//恢復邊權
	}
	printf("%d",ans);
	return 0;
}

End