網絡流基礎
網絡流問題
相關概念:
- 源點:有n個點,有m條有向邊,有一個點很特殊,只出不進,叫做源點。
- 匯點:另一個點也很特殊,只進不出,叫做匯點。
- 容量和流量:每條有向邊上有兩個量,容量和流量,從i到j的容量通常用c[i,j]表示,流量則通常是f[i,j]。
- 最大流:通俗點解釋,就好比你有很多貨物要從源點點運到匯點點,有向圖中的一條邊代表一條公路,每條公路有固定的貨物裝載限制(容量),對每條公路你只能運輸一定數量的貨物,問你每一次運輸最多運到匯點點多少貨物。
給定指定的一個有向圖,其中有兩個特殊的源點S和匯點T,每條邊有指定的容量,求滿足條件的從S到T的最大流。
網絡流的性質
- 容量限制:f[u,v]<=c[u,v]
- 反對稱性:f[u,v] = - f[v,u]
- 流量平衡:對於不是源點也不是匯點的任意結點,流入該結點的流量和等於流出該結點的流量和。
殘量網絡,容量網絡,流量網絡
殘量網絡=容量網絡-流量網絡
概念就不講了吧,顧名思義。
增廣路
增廣路: 設 f 是一個容量網絡 G 中的一個可行流, P 是從 Vs 到 Vt 的一條鏈, 若 P 滿足下列條件:
- 在 P 的所有前向弧 <u, v> 上, , 即 P+ 中每一條弧都是非飽和弧;
- 在 P 的所有后向弧 <u, v> 上, , 即 P– 中每一條弧是非零流弧。
則稱 P 為關於可行流 f 的一條增廣路, 簡稱為 增廣路(或稱為增廣鏈、可改進路)。沿着增廣路改進可行流的操作稱為增廣
最小割最大流定理
割,割集
對於一張流量圖G,斷開一些邊后,源點s和匯點t就不在連通,我們將這樣的k條邊的權值(即最大容量)和求和,求和后的值稱為割。顯然,對於一張流量圖G,割有很多個且不盡相同。我們要求的就是所有割中權值最小的那一個(可能不唯一),即花最小的代價使s和t不在同一集合中。
最小割最大流定理
- 任意一個流都小於等於任意一個割
- 構造出一個流等於一個割
- 在一張流量圖G中,最大流=最小割。
網絡流問題解決方法
FF方法(Ford-Fulkerson)
基本思想
根據增廣路定理, 為了得到最大流, 可以從任何一個可行流開始, 沿着增廣路對網絡流進行增廣, 直到網絡中不存在增廣路為止,這樣的算法稱為增廣路算法。問題的關鍵在於如何有效地找到增廣路, 並保證算法在有限次增廣后一定終止。
FF方法的基本流程是 :
- (1) 取一個可行流 f 作為初始流(如果沒有給定初始流,則取零流 f= { 0 }作為初始流);
- (2) 尋找關於 f 的增廣路 P,如果找到,則沿着這條增廣路 P 將 f 改進成一個更大的流, 並建立相應的反向弧;
- (3) 重復第(2)步直到 f 不存在增廣路為止。
反向弧建立的意義:為程序提供反悔的機會
很明顯,上圖最大流應該是2,但我們找到了一條錯誤的路徑,於是我們就應該有返回的機會,即建立反向邊,這樣再次從反向邊流過就相當於抵消了。
算法一:EK算法(EdmondsKarp)
算法思路
在EK算法中, 程序的實現過程與增廣路求最大流的過程基本一致. 即每一次更新都進行一次找增廣路然后更新路徑上的流量的過程。但是我們可以從上圖中發現一個問題, 就是每次找到的增廣路曲曲折折非常長, 此時我們往往走了冤枉路(即:明明我們可以從源點離匯點越走越近的,可是中間的幾條邊卻向離匯點遠的方向走了), 此時更新增廣路的復雜度就會增加。EK 算法為了規避這個問題使用了 bfs 來尋找增廣路, 然后在尋找增廣路的時候總是向離匯點越來越近的方向去尋找下一個結點。
復雜度\(\varTheta(m^{2}n)\)
代碼
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define INF 0x7fffffff
#define N 10010
#define M 100010
using namespace std;
int n,m,ss,tt;
struct Edge{int to;int next;int value;}e[M<<1];
struct Pre{int node;int id;}pre[M<<1];//pre[i].node表示編號為i的點最短路的上一個點,pre[i].id表示最短路上連接i點的邊的編號
int head[N],cnt=-1;//編號從0開始,原因見下
bool vis[N];
queue<int> q;
void add(int from,int to,int value)
{
cnt++;
e[cnt].to=to;
e[cnt].value=value;
e[cnt].next=head[from];
head[from]=cnt;
}
bool bfs(int s,int t)//用來尋找s,t的最短路並記錄,如果s,t不連通則返回0
{
q=queue<int>();//清空隊列
memset(vis,0,sizeof(vis));
memset(pre,-1,sizeof(pre));
pre[s].node=s;
vis[s]=1;
q.push(s);
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];i>-1;i=e[i].next)
{
int now=e[i].to;
if(!vis[now]&&e[i].value)//忽略流量為0的邊
{
pre[now].node=x;//用pre記錄最短路
pre[now].id=i;
vis[now]=1;
if(now==t)return 1;//找到
q.push(now);
}
}
}
return 0;
}
int EK(int s,int t)
{
int ans=0;
while(bfs(s,t))
{
int minv=INF;
for(int i=t;i!=s;i=pre[i].node)
minv=min(minv,e[pre[i].id].value);
for(int i=t;i!=s;i=pre[i].node)
{
e[pre[i].id].value-=minv;
e[pre[i].id^1].value+=minv;//x^1表示x邊的反向邊,此方法僅在邊的編號從0開始時有效
}
ans+=minv;
}
return ans;
}
int main()
{
memset(head,-1,sizeof(head));
scanf("%d%d%d%d",&n,&m,&ss,&tt);
for(int i=1;i<=m;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
add(b,a,0);//建立反向邊
}
printf("%d\n",EK(ss,tt));
return 0;
}
算法二:Dinic算法
其實Dinic算法是EK算法的改進
算法思路
發現在EK算法中,每增廣一次都要先進行bfs尋找最短增廣路,然而bfs后,很可能不止一條路徑可以增廣,如果還是按照EK算法的bfs一次增廣一條路,很顯然浪費了很多時間,這樣,我們讓bfs負責尋找增廣路徑,dfs計算可行的最大流。
下圖1點為s點,6點為t點,紅線代表尋找的路徑,藍線代表回溯的路徑:
-
圖1,bfs計算dis
- 圖2,dfs按最短路找到t點,累加路徑上的最小容量
- 圖3,回溯,順便更新正邊和反向邊的邊權
- 無其他路徑,回溯到源點
-
圖4,再次bfs更新dis
- 圖5,dfs按最短路找到t點,累加路徑上的最小容量
- 圖6,回溯,順便更新正邊和反向邊的邊權
- 無符合要求的其他路徑,回溯到源點
-
再次bfs,發現s和t不連通,結束算法
復雜度:
在普通圖中:\(\varTheta(n^{2}m)\)
在二分圖中:\(\varTheta(m\sqrt{n})\)
代碼
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define N 10010
#define M 100010
#define INF 0x7fffffff
using namespace std;
int n,m,ss,tt;
int dis[N];
queue<int> q;
struct Edge{int to;int value;int next;}e[M<<1];
int head[N],cnt=-1;
void add(int from,int to,int value)
{
cnt++;
e[cnt].to=to;
e[cnt].value=value;
e[cnt].next=head[from];
head[from]=cnt;
}
bool bfs(int s,int t)//bfs功能和EK算法的相似,不同的是Dinic中的bfs要求出所有點到源點s的最短路dis[i]
{
q=queue<int>();//清空隊列
memset(dis,-1,sizeof(dis));
dis[s]=0;
q.push(s);
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];i>-1;i=e[i].next)
{
int now=e[i].to;
if(dis[now]==-1&&e[i].value!=0)
{
dis[now]=dis[x]+1;
q.push(now);
}
}
}
return dis[t]!=-1;
}
int dfs(int x,int t,int maxflow)//表示從x出發尋找到匯點T的增廣路,尋找到maxflow流量為止,並相應的增廣。返回值為實際增廣了多少(因為有可能找不到maxflow流量的增廣路)
{
if(x==t)return maxflow;
int ans=0;
for(int i=head[x];i>-1;i=e[i].next)
{
int now=e[i].to;
if(dis[now]!=dis[x]+1||e[i].value==0||ans>=maxflow)continue;
int f=dfs(now,t,min(e[i].value,maxflow-ans));
e[i].value-=f;
e[i^1].value+=f;
ans+=f;
}
return ans;
}
int Dinic(int s,int t)
{
int ans=0;
while(bfs(s,t))
ans+=dfs(s,t,INF);
return ans;
}
int main()
{
memset(head,-1,sizeof(head));
scanf("%d%d%d%d",&n,&m,&ss,&tt);
for(int i=1;i<=m;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
add(b,a,0);
}
printf("%d\n",Dinic(ss,tt));
return 0;
}
當前弧優化
我們知道Dinic算法中的dfs是為了在可行增廣路中找到最小容量並進行增廣。而找增廣路需要遍歷每個點所連接的邊,直至找到一條可到達終點的路。如果這一次找到了增廣路,下一次在訪問到這個點時,上一次已經檢查過的邊就不用再走一遍了,因為遍歷一個點連接的邊都是有一定順序的,上一次訪問到這個點已經確定那幾條邊是不可行的。於是,我們用cur[i]
來表示下一次遍歷邊時應該從那一條開始。
雖然漸進時間復雜度沒有發生變化,但實際應用中的確大大降低了Dinic的常數
優化代碼(其他代碼不發生變化)
int cur[N];
int dfs(int x,int t,int maxflow)
{
if(x==t)return maxflow;
int ans=0;
for(int i=cur[x];i>-1;i=e[i].next)
{
int now=e[i].to;
if(dis[now]!=dis[x]+1||e[i].value==0||ans>=maxflow)continue;
cur[x]=i;//此路可行,記錄此路
int f=dfs(now,t,min(e[i].value,maxflow-ans));
e[i].value-=f;
e[i^1].value+=f;
ans+=f;
}
return ans;
}
int Dinic(int s,int t)
{
int ans=0;
while(bfs(s,t))
{
memcpy(cur,head,sizeof(head));//初始化
ans+=dfs(s,t,INF);
}
return ans;
}
網絡流的優化算法還有ISAP(Improved Shortest Augumenting Path),最高標號預流推進(HLPP)等等,Dinic在一般情況下已經夠用了,其他算法自學請移步其他大佬博客嘍。