[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;
}

浙公网安备 33010602011771号