網路流
網路流
網路流概念
在一個有向圖上選擇一個源點,一個匯點,每一條邊上都有一個流量上限(以下稱為容量),
即經過這條邊的流量不能超過這個上界,同時,除源點和匯點外,所有點的入流和出流都相等,
而源點只有流出的流,匯點只有匯入的流。這樣的圖叫做網路流。
相關定義
1. 源點:有n個點,有m條有向邊,有一個點很特殊,只出不進,叫做源點。
-
匯點:另一個點也很特殊,只進不出,叫做匯點。
-
容量和流量:每條有向邊上有兩個量,容量和流量,從i到j的容量通常用c[i,j]表示,流量則通常是f[i,j].
通常可以把這些邊想象成道路,流量就是這條道路的車流量,容量就是道路可承受的最大的車流量。
很顯然的,流量<=容量。而對於每個不是源點和匯點的點來說,可以類比的想象成沒有儲存功能的貨物的中轉站,
所有“進入”他們的流量和等於所有從他本身“出去”的流量。
4. 最大流:把源點比作工廠的話,問題就是求從工廠最大可以發出多少貨物,是不至於超過道路的容量限制,也就是,最大流。
增廣路演算法
該方法通過尋找增廣路來更新最大流,有 EK,dinic,SAP,ISAP 主流演算法。
求解最大流之前,我們先認識一些概念。
最常用的就是dinic 但是費用流需要用到EK演算法,so,要學會EK演算法,dinic;
增廣路:在圖中若一條從源點到匯點的路徑上所有邊的 剩餘容量都大於 0 (注意是大於不是大於等於),這條路被稱為增廣路。
求解思路
從圖中找一條增廣路,然後增廣,怎麼找?
1.從源點開始bfs,找到到匯點的一條路徑,並記錄這條路徑上所有邊剩餘流量最小值,因為要找增廣
路,所以我們在bfs時要判斷一下邊的剩餘容量是否為0,記得用一個pre陣列記錄下路徑。
2.找到路徑後,對其進行增廣(程式碼裡的up函式),增廣就是把這條路徑的每條邊都減去這
些邊中剩餘流量的最小值(bfs時記錄),反向邊加上這個最小值(關於方向邊下面再解釋)。
3.一直找增廣路增廣,直到不能增廣為止(找不到增廣路)。
可以看下面這張圖。
上面我們提到了反向邊,下面我們解釋下為什麼要建反向邊。(放圖,簡單圖我還是可以的現學的)
像我們上面這張圖,因為我們bfs時不能確定第一次走哪條邊,要是你像bmf一樣運氣不好,
如果bfs第一次找到的增廣路是1→3→2→1的話,我們最後求得的最大流就是1.
但是很明顯這張圖最大流是2,所以我們發現第一次找的增廣路不是最優的,這時候你就涼了。
那我們怎麼解決呢反向邊,反向邊其實就是一個反悔的機會,也就是讓流過的流量流回去。
如果還不明白的話還學什麼網路流,下面模擬一下這個過程。
先說一下反向邊的一些東西,反向邊初始化為0,當正向邊剩餘流量減少的是時候,
反向邊流量增加同樣的值,因為我們可以反悔的流量等於已經從這條邊正向流過的流量。
下面看一下我們是如何通過反向邊反悔的又要做圖qaq。
因為我不會畫反向邊,所以我們假設‘1/0’左邊那個數字表示正向變邊權,右邊是反向邊。
下面這張圖就是建完邊後的圖。
如果我們第一次找到的增廣路是1→3→2→1的話,總流量+1=1,圖就變成了
我們發現我們還可以找到增廣路1→2→3→4,總流量+1=2,圖變成
然後發現,找不到增廣路了,程式結束不用作圖了,我們發現
再找增廣路的過程中3→2這條邊正向一次,反向一次,相當於流量為0.
這就是反向邊的作用。
另外提供一種小技巧,使用鄰接表建圖的話,可以邊的編號從2開始建,我們知道
2^1=3,3^1=2……
這樣的話我們可以通過異或得到反向邊的編號(記得建完正向邊,緊接著就建反向邊),具體看程式碼
時間複雜度為O(nm2)至於為什麼,本人很菜不會,另外,一般時間複雜度是遠遠達不到這個值的。
程式碼(由於本人沒寫過EK,所以從同學那扒了一份)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define LL long long
using namespace std;
const int inf=1<<29;
const int N=207;
const int M=5e3+7;
int n,m,s,t,cnt=1;//從編號2開始建邊
LL maxf;//最大流
int head[N],vis[N],pre[N];
LL incf[N];
LL v[N][N];
struct edge{
int v,nxt;
LL w;
}e[M<<1];//因為要建反向邊,所以開二倍空間
void add_edge(int u,int v,LL w){//存邊
cnt++;
e[cnt].v=v;
e[cnt].w=w;
e[cnt].nxt=head[u];
head[u]=cnt;
}
bool bfs(){
memset(vis,0,sizeof(vis));
queue<int>q;
q.push(s);
vis[s]=1;
incf[s]=inf;
while(q.size()){
int now=q.front();q.pop();
for(int i=head[now];i;i=e[i].nxt){
if(e[i].w==0||vis[e[i].v])continue;
int to=e[i].v;
incf[to]=min(incf[now],e[i].w);//記錄路徑最小邊的流量
pre[to]=i;//記錄路徑邊的編號
q.push(to);
vis[to]=1;
if(to==t)return 1;
}
}
return 0;
}
void up(){
int x=t;
while(x!=s){
int i=pre[x];
e[i].w-=incf[t];
e[i^1].w+=incf[t];//反向邊加上正向邊減少的流量
x=e[i^1].v;
}
maxf+=incf[t];
}
inline int read()
{
int x = 0 , f = 1; char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
return x * f;
}
int main(){
n = read(); m = read(); s = read(); t = read();
for(int i=1;i<=m;i++){
int x,y,z;
x = read(); y = read(); z = read();
v[x][y] += z;
}
for(int i = 1;i <= n;i ++) for(int j = 1;j <= n;j ++) if(v[i][j]) add_edge(i,j,v[i][j]),add_edge(j,i,0);
while(bfs())up();
cout<<maxf<<endl;
}
//碼風過醜,請聯絡Aswert 這鍋我不背
Dinic 演算法
學了EK還學dinic有什麼用呢,有用,我們來分析下下面這張圖不小心把顏色改了下,懶得再做一張。
如果你運氣不好的話像bmf一樣,若你每次找到的增廣路都經過了2→3或3→2這條邊的話你又涼了
所以這時候就用到了我們的Dinic演算法。
Dinic 演算法 的過程是這樣的:每次增廣前,我們先用 BFS 來將圖分層。設源點的層數為1 ,
那麼一個點的層數便是它離源點的最近距離。
層次用陣列dep表示。分層圖對於每條邊滿足dep[v]=dep[u]+1。
我們思考,EK演算法每輪可能會遍歷整個圖,但只找出一條增廣路,屬於單路增廣。
而Dinic屬於多路增廣,時間複雜度O(n2m)
求解思路
1.bfs求出節點的層次,構建分層圖
2.對於求出的分層圖,用dfs進行多路增廣,由於本人菜,講的不是很明白,我們可以看程式碼
3.當前弧優化 :cur[]陣列的應用,如果一條邊已經被增廣過,那麼它就沒有可能被增廣第二次。那麼,我們下一次進行增廣的時候,
就可以不必再走那些已經被增廣過的邊。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
#define LL long long
const int INF = 2333333;
const int N = 210;
int n,m,x,y,w,s,t,tot = 1;
LL maxflow;
int dep[N],head[N];
struct node{
int to , net , w;
}e[1000010];
inline int read()
{
int s = 0 , w = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-')w = -1; ch = getchar();}
while(ch >= '0' && ch <= '9'){s = s * 10+ch-'0'; ch = getchar();}
return s * w;
}
void add(int x, int y , int w)
{
e[++tot].w = w;
e[tot].to = y;
e[tot].net = head[x];
head[x] = tot;
}
bool bfs()//構建分層圖
{
memset(dep , 0 , sizeof(dep));//每個節點層次初始化
queue<int> q;
q.push(s); dep[s] = 1;//源點初始化為1;
while(!q.empty())
{
int x = q.front(); q.pop();
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(e[i].w && !dep[to])//構建分層圖的時候要保證邊不為0,如果dep[]已經被更新就不用更新了
{
q.push(to);
dep[to] = dep[x] + 1;
if(to == t) return 1;//如果到達匯點,進行dfs
}
}
}
return 0;
}
int dinic(int x , int flow)
{
if(x == t) return flow;
int rest = flow , k;//rest表示當前這個節點最大允許通過流量
for(int i = head[x]; i && rest; i = e[i].net)
{
int to = e[i].to; int val = e[i].w;
if(val && (dep[to] == dep[x] + 1))//尋找增廣路
{
k = dinic(to , min(rest , val));
if(!k) dep[to] = 0;//如果在to之後的路徑找不到增廣路,踢出分層圖
e[i].w -= k; e[i^1].w += k;
rest -= k;//當前節點最大允許通過流量減去這次通過的流量
}
}
return flow - rest;
}
int main()
{
n = read(); m = read(); s = read(); t = read();
for(int i = 1; i <= m; i++)
{
x = read(); y = read(); w = read();
add(x,y,w); add(y,x,0);
}
int flow = 0;
while(bfs())
{
while(flow = dinic(s,INF)) maxflow += 1LL*flow;
}
printf("%lld\n",maxflow);
return 0;
}
最小割
最小割問題是指:給出一種有向圖(無向圖)和兩個點s,t以及圖中的邊的邊權,
求一個權值和最小的邊集,使得刪除這些邊之後是s,t不連通。這類問題,一般運用最
大流等於最小流定理,求出最大流來解決。證明自行百度百科
程式碼同上。
ENDing!