题解 #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

上一篇:JavaScript雷区:使用[]引用对象必须加双引号


下一篇:每日一题4.26-4.30