CF733F Drivers Dissatisfaction 题解

博客解析了CF733F题目的解法,涉及图论和最小生成树问题。当给定一个图和总费用,如何通过降低边的权值来获得最小生成树权值最小的方案。文章介绍了贪心策略,包括降低树边和非树边权值的影响,并使用树链剖分解决复杂情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

CF733F Drivers Dissatisfaction 题解

题目大意:

给定一个 NNN 个点 MMM 条边的无向图,保证图联通。每条边有两个属性 wiw_iwicic_ici ,表示使这条边的权值每降低 111 要花费 cic_ici 的花费。现在你有一个总费用 SSS ,你可以降低某些边的权值(可以降为负数,且总费用不得超过 SSS ) 。问降低权值后,这张图可得到的最小生成树权值是多少,并且输出选为最小生成树中的边的编号及权值。

解析

首先明确一点:题目中说可以降低某些边的权值,其实如果我们只选 cic_ici 最小的那条边,并只降低它的权值,这样一定可以使权值降低的最多(一点贪心的思想)。

我们可以首先找出这张图的最小生成树,并将在树上的边标记。然后,我们考虑降低哪条边的权值。

  1. 如果降低树边的权值

    其实非常好做。由开头所讲,只需要找到树边中 cic_ici 最小的那条边,将其 wiw_iwi 减少 Scmin\frac {S}{c_{min}}cminS 即可。

  2. 如果降低非树边的权值

    这种情况稍微有些麻烦,我们来看下面这张图:

    黑色的边表示树边,而红色的这条边表示我们选择的要降低权值的这条边。

    由树的性质,我们不难发现:给树上两点间加上一条边后,便会生成一个环,由加上的边与两点间简单路径组成。要想其重新变回一棵树,那么必须从两点间的简单路径上去删除一条边。由贪心的思想,我们知道,要是最后答案最小,那么删去的边一定是两点间简单路径上权值最大的边。

    于是,我们可以枚举每一条非树边,找到其连接的两点的树上简单路径上权值最大的边,计算备选答案。

    备选答案就是 sum−wmax+wi−Scisum-w_{max}+w_i-\frac{S}{c_i}sumwmax+wiciS ,其中 sumsumsum 表示原图最小生成树的权值和。

    对于找两点间权值最大的边,我们可以请出我们的树链剖分。码量虽然大,但是很好理解呀!

最后对于路径的输出(真恶心) , 把要选的边标记即可。有很多小细节,在代码里有呈现。本人蒟蒻,不喜勿喷!

#include<iostream>
#include<algorithm>
using namespace std;
#define int long long
const int N=2e5+5;
int n,m,S;
int w[N],rec[N];
int sum,rev;
int he[N],ne[N<<1],go[N<<1],tot;
int fa[N];
struct node{
	int u,v,we,c,id;
	bool operator <(const node W) const{
		return we<W.we;
	}
}a[N];
bool st[N];
inline void add(int a,int b){
	ne[++tot]=he[a];he[a]=tot;go[tot]=b;
}
inline int find(int x){
	if(x!=fa[x]) return fa[x]=find(fa[x]);
	else return x;
}
inline void kruskal(){//最小生成树模板
	int cnt=0;
	sort(a+1,a+m+1); 
	for(int i=1;i<=m;i++){
		int A=find(a[i].u),B=find(a[i].v);
		if(A!=B){
			fa[B]=A;
			add(a[i].u,a[i].v);
			add(a[i].v,a[i].u);
			//建树
			cnt++;st[i]=1;sum+=a[i].we;
			//st[i]=1表示第i条边是一条树边
			//sum存储最小生成树的权值和
		}
		if(cnt==n-1) break;
	}
}
int top[N],dep[N],f[N],son[N],si[N];
int dfn[N],nw[N],cnt;
inline void dfs1(int u,int p){
	f[u]=p;dep[u]=dep[p]+1;
	si[u]=1;
	for(int i=he[u];i;i=ne[i]){
		int v=go[i];
		if(v==p) continue;
		dfs1(v,u);
		si[u]+=si[v];
		if(si[son[u]]<si[v]) son[u]=v;
	}
}
inline void dfs2(int u,int tp){
	top[u]=tp;
	dfn[u]=++cnt;
	nw[cnt]=w[u];
	if(son[u]) dfs2(son[u],tp);
	else return ;
	for(int i=he[u];i;i=ne[i]){
		int v=go[i];
		if(v==f[u]||v==son[u])continue;
		dfs2(v,v);
	}
}
struct Seg_Tree{
	int l,r;
	int val;
	#define lson (p<<1)
	#define rson (p<<1|1)
}t[N<<2];
inline void build(int p,int l,int r){
	t[p].l=l;t[p].r=r;
	if(l==r){
		t[p].val=nw[l];
		return ;
	}
	int mid=l+r>>1;
	build(lson,l,mid);
	build(rson,mid+1,r);
	t[p].val=max(t[lson].val,t[rson].val);
}
inline int query(int p,int l,int r){
	if(t[p].l>=l&&t[p].r<=r)return t[p].val;
	int res=0;	
	if(t[lson].r>=l) res=max(res,query(lson,l,r));
	if(t[rson].l<=r) res=max(res,query(rson,l,r));
	return res; 
} 
inline int query_max(int x,int y){
	int res=0;
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]])
			swap(x,y);
		res=max(res,query(1,dfn[top[x]],dfn[x]));
		x=f[top[x]];
	}
	if(dep[x]>dep[y]) swap(x,y);
	res=max(res,query(1,dfn[son[x]],dfn[y]));
	//边权转点权,最后一部分的上点应取 son[x]
	return res;
}
//---------------以上均为模板---------------//
signed main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1;i<=m;i++) cin>>a[i].we,a[i].id=i; //由于要输出边的序号,将其存进结构体
	for(int i=1;i<=m;i++) cin>>a[i].c;
	for(int i=1;i<=m;i++)
		cin>>a[i].u>>a[i].v;
	cin>>S;
	kruskal(); //最小生成树,同时建树
	rev=sum; //选非树边时方便得到备选答案,将最小生成树的权值拷贝一份
	dfs1(1,0);
	//由于题中给的是边权,我们可以将边权化为点权,用边连接的两点中深度更大的那个点的权值作为这条边的权值。
	for(int i=1;i<=m;i++){
	    if(!st[i]) continue; //只处理树边
	    if(dep[a[i].u]>dep[a[i].v]) w[a[i].u]=a[i].we,rec[a[i].u]=i;
	    else w[a[i].v]=a[i].we,rec[a[i].v]=i;
	    //w记录树中点权。rec表示该点向上连接的边的编号,输出时要用
	}
	dfs2(1,1);
	build(1,1,n);
	int minn=0x3f3f3f3f,id=0;
	//首先处理树边
	for(int i=1;i<=m;i++)
		if(st[i]){
			if(a[i].c<minn){
				minn=a[i].c;
				id=i;
				//minn找到树边中c[i]的最小值,id记录该边的编号
			}
		}
	int r=S/minn;
	sum-=r;//备选答案之一:选择树边
	bool flag=1; //由于两种情况在输出路径时的处理方式不同,因此记录改用哪种处理方式。
	int rd=0,cut=0;
	//如果选择了某条非树边比选择树边更优
	//那么flag标记为0,cut记录选择的那条边的权值,rd记录那条边的编号
	//蒟蒻想不出更多变量名了呜呜呜
	for(int i=1;i<=m;i++)
		if(!st[i]){
			int res=query_max(a[i].u,a[i].v);
			//找到两点简单路径中边权最大的边
			int r=S/a[i].c;
			if(rev-res+a[i].we-r<sum){ //解析中公式有呈现
				sum=rev-res+a[i].we-r;
				flag=0;rd=i;cut=res;
				//备选答案之二:选择非树边
			}
		}
	cout<<sum<<endl;
	if(!flag){ //如果选非树边更优
		int x=a[rd].u,y=a[rd].v;
		//由于query_max只能找到最大的数值而不能找到到底是那条边,因此枚举简单路径上的边即可
		while(1){
		    if(dep[x]<dep[y]) swap(x,y);
			if(w[x]==cut) { //如果当前边边权为要删去的边的边权
			    st[rec[x]]=0; //rec在这里派上用场:边权化为点权,x向上连接的那条边即为要删去的边
				st[rd]=1; //这条非树边标记
				int r=S/a[rd].c;
				a[rd].we-=r;
				break;
			}
			x=f[x]; //树剖的f和并查集的fa别搞混了(掉了几次坑的蒟蒻如是说)
			//每次向上跳一个
		}
		for(int i=1;i<=m;i++)
			if(st[i])
				cout<<a[i].id<<" "<<a[i].we<<endl; //sort会打乱边的顺序,因此输出a[i].id
	}else{
	    //选择树边就很简单了,之间把要选的那条边的 w 减去 S/minn 即可
		int r=S/minn;
		a[id].we-=r;
		for(int i=1;i<=m;i++)
			if(st[i])
				cout<<a[i].id<<" "<<a[i].we<<endl;
	}
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值