网络流相关技术
基础部分
最大流
Dinic
板子
#include<bits/stdc++.h>
using namespace std;
using i64=long long;
constexpr int maxn=210,maxm=5e3+10,inf=0x7fffffff;
int n,m,S,T;
int tot,head[maxn];
struct EDGE{
int v,w,nxt;
}e[maxm<<1];
inline void addedge(int u,int v,int w){
e[++tot].v=v;e[tot].w=w;e[tot].nxt=head[u];head[u]=tot;
}
int cur[maxn],dep[maxn];
queue<int> q;
bool bfs(){
fill(dep+1,dep+n+1,0);
for(int i=1;i<=n;++i) cur[i]=head[i];
q.push(S);dep[S]=1;
while(!q.empty()){
int u=q.front();q.pop();
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v,w=e[i].w;if(dep[v]||!w) continue;
dep[v]=dep[u]+1;q.push(v);
}
}
return dep[T];
}
int dfs(int u,int f){
if(u==T||!f) return f;
int rs=0;
for(int i=cur[u];i;i=e[i].nxt){
cur[u]=i;
int v=e[i].v,w=e[i].w;if(dep[v]!=dep[u]+1) continue;
int r=dfs(v,min(w,f-rs));if(!r) continue;
e[i].w-=r;e[i^1].w+=r;rs+=r;
if(rs==f) break;
}
return rs;
}
i64 ans;
void dinic(){
while(bfs()) ans+=dfs(S,inf);
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m>>S>>T;
tot=1;
for(int i=1;i<=m;++i){
int u,v,w;cin>>u>>v>>w;
addedge(u,v,w);addedge(v,u,0);
}
dinic();
cout<<ans<<'\n';
return 0;
}
是 BFS 一次之后进行 DFS 同时多路增广。当前弧优化是复杂度正确性的保障。
时间复杂度为\(O(n^2m)\),但大多数情况跑不满,不会被卡。但要是\(n=10^6\)了还是想想其他办法吧。
HLPP
板子
#include<bits/stdc++.h>
using namespace std;
const int maxn=1210,maxm=120010;
int s,t,n,m,tot=1,maxh,head[maxn],h[maxn],gap[maxn];
long long ex[maxn];
stack<int> b[maxn];
struct edge{
int v,nxt;
long long w;
}e[maxm<<1];
void add(int u,int v,long long w){
e[++tot].v=v;
e[tot].w=w;
e[tot].nxt=head[u];
head[u]=tot;
}
int getmxh(){
while(b[maxh].empty()&&maxh>-1) maxh--;
return maxh==-1?0:b[maxh].top();
}
bool bfs(){
memset(h,0x3f3f3f3f,sizeof(h));
h[t]=0;
queue<int> q;
q.push(t);
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v;
if(e[i^1].w&&h[v]>h[u]+1){
h[v]=h[u]+1;
q.push(v);
}
}
}
return h[s]!=0x3f3f3f3f;
}
bool push(int u){
bool p=(u==s);
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v,w=p?e[i].w:min(ex[u],e[i].w);
if(!w||(!p&&h[u]!=h[v]+1)||h[v]==0x3f3f3f3f) continue;
if(v!=s&&v!=t&&!ex[v]) b[h[v]].push(v),maxh=max(maxh,h[v]);
ex[u]-=w,ex[v]+=w;
e[i].w-=w,e[i^1].w+=w;
if(!ex[u]) return 0;
}
return 1;
}
void relabel(int u){
h[u]=0x3f3f3f3f;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v,w=e[i].w;
if(!w) continue;
h[u]=min(h[u],h[v]+1);
}
if(h[u]<n){
b[h[u]].push(u);
gap[h[u]]++;
maxh=max(maxh,h[u]);
}
}
long long hlpp(){
if(!bfs()) return 0;
memset(gap,0,sizeof(gap));
for(int i=1;i<=n;++i){
if(h[i]!=0x3f3f3f3f) gap[h[i]]++;
}
h[s]=n,push(s);
int u;
while((u=getmxh())){
b[maxh].pop();
if(push(u)){
if(!--gap[h[u]]){
for(int i=1;i<=n;++i){
if(i!=s&&i!=t&&h[i]>h[u]&&h[i]<n+1) h[i]=n+1;
}
}
relabel(u);
}
}
return ex[t];
}
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1;i<=m;++i){
int u,v;
long long w;
scanf("%d%d%lld",&u,&v,&w);
add(u,v,w);
add(v,u,0);
}
printf("%lld\n",hlpp());
return 0;
}
时间复杂度\(O(n^2\sqrt m)\),理论上比Dinic优秀。
一般来说不会卡Dinic,但看情况吧(
最小费用最大流
都是 SSP 算法,时间复杂度是伪多项式 \(O(nmf)\)。
Dinic
对最大流的Dinic做一些修改就好了。
把 BFS 换成 SPFA。DFS 中的 vis 标记用于防止在 \(u,v\) 之间两条边都有流量时反复横跳。
这是保证流量最大时的最小费用。
板子
#include<bits/stdc++.h>
using namespace std;
constexpr int maxn=5e3+10,maxm=5e4+10,inf=0x7fffffff;
int n,m,S,T;
int tot,head[maxn];
struct EDGE{
int v,w,c,nxt;
}e[maxm<<1];
inline void addedge(int u,int v,int w,int c){
e[++tot].v=v;e[tot].w=w;e[tot].c=c;e[tot].nxt=head[u];head[u]=tot;
}
int cur[maxn],dis[maxn];
bool inq[maxn];
queue<int> q;
bool spfa(){
fill(dis+1,dis+n+1,inf);
for(int i=1;i<=n;++i) cur[i]=head[i];
q.push(S);dis[S]=0;inq[S]=true;
while(!q.empty()){
int u=q.front();q.pop();inq[u]=false;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v,w=e[i].w,c=e[i].c;if(dis[v]<=dis[u]+c||!w) continue;
dis[v]=dis[u]+c;if(!inq[v]) inq[v]=true,q.push(v);
}
}
return dis[T]!=inf;
}
int ansf,ansc;
bool vis[maxn];
int dfs(int u,int f){
if(u==T||!f) return f;
vis[u]=true;
int rs=0;
for(int i=cur[u];i;i=e[i].nxt){
cur[u]=i;
int v=e[i].v,w=e[i].w,c=e[i].c;if(vis[v]||dis[v]!=dis[u]+c) continue;
int r=dfs(v,min(f-rs,w));if(!r) continue;
rs+=r;e[i].w-=r;e[i^1].w+=r;ansc+=r*c;
if(rs==f) break;
}
vis[u]=false;
return rs;
}
void dinic(){
while(spfa()){
int x=0;while((x=dfs(S,inf))) ansf+=x;
}
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m>>S>>T;
tot=1;
for(int i=1;i<=m;++i){
int u,v,w,c;cin>>u>>v>>w>>c;
addedge(u,v,w,c);addedge(v,u,0,-c);
}
dinic();
cout<<ansf<<' '<<ansc<<'\n';
return 0;
}
Primal-Dual原始对偶算法
把SPFA换成Johnson,于是跑Dijkstra即可。
上下界网络流
无源汇上下界可行流
没有源点和汇点,每条边容量有上下界,问是否有一种给每条边标定流量的方式使得每个点流量平衡。
首先在原图上让每条边流到下界,然后给每个点计算流入的流量和流出的流量,并建立差网络(即让每条边的流量设为上界与下界之差,当做普通网络流的边)。
我们希望调整每个点的流入量或者流出量使之平衡。在差网络上建立汇源\(S\)和\(T\),若对于点\(u\),流入量大于流出量且差为\(x\),就从\(S\)向\(u\)连容量为\(x\)的边,这是为了让\(u\)在差网络上继续流出,使得其平衡。同理,若流出量大于流入量且差为\(x\),就从\(u\)向\(T\)连容量为\(x\)的边。我们称这里以\(S\)或\(T\)为端点的边为附加边。
在差网络上跑最大流后,将每条非附加边的流加上原图中的下界就是一个可行流。如果跑完最大流后发现没有满流,就说明有结点没有平衡,则不存在可行流。
判是否满流只需判断与\(S\)相连的边是否满流或者与\(T\)相连的边是否满流。根据网络流的性质,此二者要么都满流,要么都不满流。
有源汇上下界可行流
有源汇只是多了可以凭空拿出流的\(S\)和容纳所有流的\(T\)。可以简单转化成无源汇上下界可行流。只需从\(T\)向\(S\)连一条下界为\(0\),上界为\(+\infty\)的边即可。其流量就是\(T\)到\(S\)这条边的流量。
注意:转无源汇后仍需新建源汇\(S'\)和\(T'\)来跑,原来的\(S\)和\(T\)要当做普通的点。
有源汇上下界最大流
以上,我们就已经可以求出一条可行流,现在来求最大流。
我们可以在差网络中把所有附加边删去,然后以原本的\(S\)和\(T\)作为源汇从\(S\)到\(T\)在残量网络上跑最大流,这次的最大流加上可行流即为原问题的最大流。
可行流已经保证流量平衡,这样删去附加边后不会再不平衡,然后再跑最大流相当于将原网络中还能用的流用完。
P5192 Shoot the Bullet|东方文花帖|【模板】有源汇上下界最大流
板子
#include<bits/stdc++.h>
using namespace std;
#define gc getchar
int rd(){
int f=1,r=0;
char ch=gc();
while(!isdigit(ch)){ if(ch=='-') f=-1;ch=gc();}
while(isdigit(ch)){ r=(r<<3)+(r<<1)+(ch^48);ch=gc();}
return f*r;
}
const int maxn=3e3+10,maxm=3e5+10,inf=0x7fffffff;
int n,m,s,t,s1,t1,tot=1,flow,ind[maxn],otd[maxn],head[maxn],dep[maxn],cur[maxn];
struct edge{
int v,f,nxt;
}e[maxm<<1];
inline void add(int u,int v,int f){
e[++tot].v=v;
e[tot].nxt=head[u];
e[tot].f=f;
head[u]=tot;
}
bool bfs(){
for(int i=1;i<=t;++i) dep[i]=0,cur[i]=head[i];
queue<int> q;
q.push(s);
dep[s]=1;
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v,f=e[i].f;
if(f&&!dep[v]){
dep[v]=dep[u]+1;
q.push(v);
}
}
}
return dep[t];
}
int dfs(int u,int fl){
if(u==t||!fl) return fl;
int used=0;
for(int i=cur[u];i&&fl;i=e[i].nxt){
cur[u]=i;
int v=e[i].v,f=min(e[i].f,fl);
if(f&&dep[v]==dep[u]+1){
int k=dfs(v,f);
e[i].f-=k,e[i^1].f+=k;
fl-=k,used+=k;
}
}
if(!used) dep[u]=-1;
return used;
}
int dinic(){
int ans=0;
while(bfs()) ans+=dfs(s,inf);
return ans;
}
void clean(){
tot=1;
flow=0;
s=s1=t=t1=0;
memset(head,0,sizeof(head));
memset(ind,0,sizeof(ind));
memset(otd,0,sizeof(otd));
return;
}
void solve(){
clean();
s1=n+m+1,t1=s1+1;
add(t1,s1,inf);
add(s1,t1,0);
for(int i=1;i<=m;++i){
int x=rd();
add(i,t1,inf);
add(t1,i,0);
ind[t1]+=x;
otd[i]+=x;
}
for(int i=1;i<=n;++i){
int c=rd(),d=rd();
add(s1,i+m,d);
add(i+m,s1,0);
for(int j=1;j<=c;++j){
int id=rd()+1,l=rd(),r=rd();
add(i+m,id,r-l);
add(id,i+m,0);
ind[id]+=l;
otd[i+m]+=l;
}
}
s=t1+1,t=s+1;
for(int i=1;i<=t1;++i){
if(ind[i]==otd[i]) continue;
else if(ind[i]>otd[i]) add(s,i,ind[i]-otd[i]),add(i,s,0);
else add(i,t,otd[i]-ind[i]),add(t,i,0);
}
dinic();
for(int i=head[s];i;i=e[i].nxt){
if(e[i].f){
puts("-1");
puts("");
return;
}
}
s=s1,t=t1;
for(int i=head[t];i;i=e[i].nxt){
if(e[i].v==s){
flow=e[i^1].f;
e[i].f=e[i^1].f=0;
}
}
printf("%d\n\n",dinic()+flow);
return;
}
int main(){
while(scanf("%d%d",&n,&m)==2) solve();
return 0;
}
有源汇上下界最小流
与上面几乎相同,只是改为从\(T\)到\(S\)跑一遍最大流,然后用可行流减去这次的流量。考虑到建图时建的反边此时当做正向的边,于是这样就相当于在不破坏平衡的前提下反悔最多的流,这样回退流量就得到最小流。
有源汇上下界最小费用可行流
注意:只是可行流,而非最大流。
和前面几乎相同。还是拆成差网络,非附加边的费用不变,附加边的费用为\(0\)。最后的最小费用即原图中流量下界与对应费用的乘积加上在差网络上用最小费用最大流跑可行流的费用。
最小割树Gomory-Hu Tree
构造一棵树,使得原图中\(u\)和\(v\)之间的最小割为树上\(u\)到\(v\)路径上的最小值。
构造是简单的,先在原图上随便选两个点\(u\)和\(v\),求它们的最小割,然后在树上连边。再把最小割都割掉,对于\(u\)和\(v\)分属的两个点集递归进行操作。
正确性待补。
时间复杂度\(O(n^3m)\),很难卡满。
模拟费用流
待补。
网络流中的转化关系
最大流等于最小割
有一类经典题目,用最大流等于最小割转化到网络流上。
-
有若干元素,每个元素有两个状态,要求给每个元素决定一种,每个元素选一种状态都有相应的收益。
-
有若干限制或者额外收益,表现为\(A\)和\(B\)不能同种状态或者\(A\)和\(B\)同种状态会有多少额外收益/etc.
-
要求收益最大。
可以先获得全部收益,然后构造一张图,使得这张图的最小割对应着不得不放弃的最小代价。割掉最小割后与\(S\)连着的是一种状态,与\(T\)连着的是另一种状态。
平面图最小割等于对偶图最短路
网络流的复杂度显然高于最短路的复杂度,这样转化自然更好。
首先介绍一下概念,平面图就是可以画在一张平面上的图,其对偶图就是将一个划分出来的区域视作点,每条边跨过原图中的一条边将各个区域连起来。注意最外面的无界区域需要特殊处理,可以视情况将其分成几个区域。
注意到在对偶图中,最短路恰好将原来的平面图分成两半,于是对应着原来的平面图的一个最小割。
P4001 [ICPC-Beijing 2006] 狼抓兔子
这里要求一个网格图中左上角到右下角的最小割,但是点数\(n\le 10^6\),直接Dinic跑不了。
考虑转对偶图最短路,这是好转的,因为是网格图。对最外面的无界区域,我们特殊处理:沿矩形的主对角线将外面的区域分成两半,左下角作为\(s\),右上角作为\(t\),然后就好了。
最大权闭合子图
用最小割做。
对于原图中的边,不动,设置容量为\(+\infty\)。这表示原图中的边一定不会被割。
对于正权点\(u\),连边\(S\to u\),设置容量为\(a_u\)。
对于负权点\(u\),连边\(u\to T\),设置容量为\(|a_u|\)。
割掉\((S,u)\),表示不选这个点。割掉\((u,T)\),表示选这个点。
设所有正权点的权值和为\(Sum\),那么最大权闭合子图的权值为\(Sum-\texttt{最小割}\)。

浙公网安备 33010602011771号