[HDU 5263] 平衡大师 题解

[HDU 5263] 平衡大师

这道题是一道上下界网络流最小费用类的题目

这道题十分精妙。

首先,最简单的一步,我们需要发现问题实际上就是删边来最小化每个点出度-入度 \(dif_i\) 的最大值

由于这是一道网络流,我们不妨思考一下流量的意义

我们发现,因为是删边,所以我们可以把流经过的边表示成已经被删除的边

这时,我们发现我们需要一种方式来修改 \(dif_i\)

First,我们当然要建超级源汇点 \(S\)\(T\)

然后,我们神奇地发现,若我们 \(s\) 向点 \(u\) 连一条边,若流量为 \(x\) ,且这是合法的,那么这些流量只能向这条边的出边流动。那么向出边流动代表着什么呢?由于每条边的流量限制当然是上界为1,所以这代表着会有 \(x\)\(u\) 的出边会被删除,所以根据 \(dif_u = 出度-入度\)\(dif_u\) 会减少 \(x\)

同样地,若将一个点 \(u\) 与点 \(t\) 连条边,若流量为 \(x\),且是合法可被满足的,那么必须至少有 \(x\) 条入边向 \(u\) 这个点流了流量,也就是说会有 \(x\) 条入边被删除了。所以这时,\(dif_u\) 会增加 \(x\)

然后,根据合法性,我们便可以分讨来确定 \(x\) 应在哪个范围

当然,在原图 \(u->v\) 边之间需要有1的费用

于是,我们便可以二分这个答案

具体地,引用一下分讨过程:

v为当前二分的答案

1.若dif[u]>=v:

则dif[u]要减去一个数x(x>=0),

因为-v<=dif[u]-x<=v,

所以dif[u]-v<=x<=dif[u]+v,

从s到u连一条上界为dif[u]+v,下界为dif[u]-v,费用为0的边

2.若-v\(<\)dif[u]\(<\)v:

从s到u连一条上界为dif[u]+v,费用为0的边

从u到t连一条上界为v-dif[u],费用为0的边

3.若dif[u]<=-v:

从u到t连一条上界为v-dif[u],下界为-v-dif[u],费用为0的边

于是,就做完了

这道题的精髓在于将操作问题转化为网络流上的流动问题,也就是为每一个流量,每一条连边赋予与题目要求相关的实际意义

Code

#include<bits/stdc++.h>

using namespace std;
// 网络流全局变量:s,t为上下界流超级源汇;s1,t1原图虚拟源汇;k题目允许删边数量;cnt边计数;n,m点数边数
// head邻接表头;dis最短路距离;pid记录到达每个点的前驱边;visSPFA标记数组
int s,t,s1,t1,k,cnt,n,m,head[1010],dis[1010],pid[1010],vis[1010];
// 网络流边结构体:from起点 to终点 nxt下一条邻边 c剩余容量 w单位费用
struct no{
	int from,to,nxt,c,w;
}e[200010];
// 加边函数:正向边u->v容量c费用w,反向边v->u容量0费用-w
void add(int u,int v,int c,int w){
	e[++cnt]={u,v,head[u],c,w};
	head[u]=cnt;
	e[++cnt]={v,u,head[v],0,-w};
	head[v]=cnt;
}
// SPFA求最小费用最短路,返回是否存在从s到t的可行路
int SPFA(){
	memset(dis,0x3f,sizeof dis); // 距离初始化为无穷大
	memset(pid,0,sizeof pid);     // 前驱边清零
	memset(vis,0,sizeof vis);     // 入队标记清零
	queue<int>q;
	q.push(s);
	dis[s]=0;
	vis[s]=1;
	while(q.size()){
		int u=q.front();
		q.pop();
		vis[u]=0; // 出队取消标记
		// 遍历u所有邻边
		for(int i=head[u];i;i=e[i].nxt){
			int v=e[i].to;
			// 边有剩余容量 且可以松弛更短费用路径
			if(e[i].c>0 and dis[v]>dis[u]+e[i].w){
				dis[v]=dis[u]+e[i].w;
				pid[v]=i; // 记录v是通过i号边过来的
				if(!vis[v])q.push(v),vis[v]=1;
			}
		}
	}
	return dis[t]!=0x3f3f3f3f; // 终点距离不是无穷说明有路
}
// EK最小费用流,返回pair{总费用,总流量}
pair<int,int>EK(){
	int flow=0,cost=0;
	while(SPFA()){ // 不断找最短路增广
		int v=t,f=0x3f3f3f3f;
		// 找这条增广路上最小剩余容量(可增广流量)
		while(pid[v]){f=min(f,e[pid[v]].c);
			v=e[pid[v]].from;
		}
		v=t;
		// 沿增广路更新容量、累加费用
		while(pid[v]){cost+=f*e[pid[v]].w;
			e[pid[v]].c-=f;    // 正向边减去流量
			e[pid[v]^1].c+=f;  // 反向边增加流量
			v=e[pid[v]].from;
			
		}
		flow+=f; // 总流量累加本次增广量
		//cost+=f*dis[t];
	}
	return {cost,flow};
}
int dif[2010],aa[2010],bb[2010],sum[2010];
// 二分答案check函数:当前判断最大值v是否合法
int check(int v){
	cnt=1;                          // 重置边计数器,重建图
	memset(head,0,sizeof head);     // 邻接表清空
	memset(sum,0,sizeof sum);       // 上下界平衡量数组清零
	s1=n+1,t1=s1+1,s=t1+1,t=s+1;   // 分配原图虚拟源s1、虚拟汇t1,上下界超级源s、超级汇t
	add(t1,s1,0x3f3f3f3f,0);       // s1-t1环,用于原图无汇源的无源汇流转换
	// 建原图每条边u->v,容量1费用1,代表删这条边花费1代价
	for(int i=1;i<=m;i++)add(aa[i],bb[i],1,1);
	
	// 根据每个点dif[i]分三类建s1/t1与点的约束边,维护度数差限制
	for(int i=1;i<=n;i++){
		if(dif[i]>v){ // 情况1:原始度数差超过v,需要流出x降低差值
			add(s1,i,2*v,0);
			sum[s1]-=dif[i]-v; // 平衡量更新:s1需要流出dif[i]-v
			sum[i]+=dif[i]-v;
		}else if(dif[i]>-v){ // 情况2:度数差在[-v,v]区间内,可增可减
			add(s1,i,dif[i]+v,0);
			add(i,t1,v-dif[i],0);
		}else{ // 情况3:原始度数差小于-v,需要流入x抬高差值
			add(i,t1,2*v,0);
			sum[i]-=-v-dif[i];
			sum[t1]+=-v-dif[i];
		}
	}
	int pd=0; // 记录总需要流出的流量,用于判断是否满流
	// 上下界网络流标准建图:平衡量>0连超级源s,<0连超级汇t
	for(int i=1;i<=t1;i++){
		if(sum[i]>0)add(s,i,sum[i],0),pd+=sum[i]; // 该点需要流入sum[i]流量
		if(sum[i]<0)add(i,t,-sum[i],0);           // 该点需要流出-sum[i]流量
	}
	auto xx=EK(); // 跑最小费用流
	// 满流说明约束全部满足,且总删边代价<=允许数量则v合法
	if(xx.second==pd)return xx.first<=k;
	return 0; 
}
int bh=0; // 字符串映射点编号计数器
map<string,int>mp; // 字符串名称映射到数字点号
void solve(){
	cin>>n>>m>>k;
	bh=0;
	memset(dif,0,sizeof dif); // dif[i] = 出度-入度 清零
	k=m-k; // 最多保留k条边 = 最少删除m-k条边
	mp.clear();
	// 读入m条边,字符串映射点,统计每个点出度入度差值dif
	for(int i=1;i<=m;i++){
		string a,b;
		cin>>a>>b;
		if(!mp[a])mp[a]=++bh;
		if(!mp[b])mp[b]=++bh;
		aa[i]=mp[a];
		bb[i]=mp[b];
		dif[aa[i]]++;  // a是起点,出度+1
		dif[bb[i]]--;  // b是终点,入度+1
	}
	// 二分答案,最小化所有点dif绝对值的最大值
	int l=0,r=n,ans;
	while(l<=r){
		int mid=(l+r)>>1;
		if(check(mid))ans=mid,r=mid-1; // mid可行,尝试更小值
		else l=mid+1; // mid不可行,放大限制
	}
	cout<<ans<<'\n';
}
main(){
	int tt,id=0;
	cin>>tt;
	while(tt--)id++,cout<<"Case #"<<id<<":\n",solve();
	return 0;
}
posted @ 2026-07-05 19:52  zoupiwa666  阅读(14)  评论(0)    收藏  举报