[国家集训队]Tree
题目大意:
给你一个无向带权连通图,每条边是黑色或白色。让你求一棵最小权的恰好有 \(need\) 条白色边的生成树。
solution:
考虑 \(\text{Kruskal}\) 的算法流程:每次取边权最小的边添到生成树里,所以为了让白色边进入生成树,我们可以将白色边的边权改变一下,但是改变多少呢?这个就可以用二分答案,再验证。单调性后面证。
具体操作如下:
- 二分出当前 \(mid\) ,把所有白色边的边权都减去 \(mid\) 。
- 构建最小生成树,记录被加入到生成树的白边的个数,同时记录答案。
- 若白边个数 \(\geq need\) 则统计答案:\(ans=res+need \times mid\)(把减去的 \(mid\) 加回来,共 \(need\) 条)。
- 把所有边权恢复,准备进行下一次二分。
单调性证明:
贪心的想,白色边的边权越小,则被加入到生成树的边数越多(显然),这样就满足了单调性。
证毕[滑稽]。
细节处理:
- 这里的白边权有可能小导致生成树中白边过多,所以我们要增加权值,所以二分的左边界 \(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;
}