最大流問題介紹
圖定義
給定一張有向圖,a->b
邊的權值表示當前情況下,從a到b能夠發送的流量上限是多少,比如 a->b = 10
表示當前能夠從a發10流量給b,當然我們也可以選擇不發那麼多。
例子:
假設a->b
之間未有任何流量的發送,此時我們發8流量,那麼 a->b = 10 變爲 a->b = 2
,a->b
能夠發送的流量上限由10 變爲 2
源點與匯點
源點只發不收,匯點只收不發,其他點作爲中轉站,既可以發也可以收,這像極了快遞物流網絡。。。
源點:賣家
匯點:買家
其他點:快遞物流站點
最大流問題描述
從源點能夠想方設法發送到匯點的最大流量的值,而求解這個問題就是求解這個最大值
你可能會想:這不是直接把源點發送的流量拉滿就行了🐎?
然而事實是這樣的:兩點之間發送流量最大值取決於兩點之間管道最細的地方的大小(木桶原理),前面塞太多後面放不下。。。
Ford-Fulkerson方法
Ford-Fulkerson不是算法,是一種方法,而通過bfs實現這種方法,叫做Edmonds-Karp算法
回退邊與增廣圖
Ford-Fulkerson定義了一個非常具有創造性的“回退邊”
如果我們在a->b
中塞入x流量,表示我們買了x貨物,於是我們可以退貨。
回退邊描述了這種退貨的行爲,回退邊的權值是我們已經塞入的流量的大小(聯繫現實生活中,買了x貨物,最多退貨量爲x)
我們在原圖g的基礎上增加回退邊,構成原圖的增廣圖ga,增廣圖包含的回退邊的權值等於已經塞入的流量的大小
僞代碼
Ford-Fulkerson方法提供了一種求解最大流的思路,是一個很巧妙的貪心思路,這是他的僞代碼:
sum = 0
while True:
構造原圖g的增廣圖ga
在增廣圖ga上找到一條從源點到匯點的最短路徑p,這個最短指的是經過的點最少而不是經過的邊權值之和最小
if 找不到p :
break
記錄p一路走來遇到的"最細"的管道大小f(即經過的所有邊中權值的最小值)
通過f來更新原圖中路徑p上的邊:
for 邊(a->b) in p:
(a->b)能夠通過的流量上限 -= f
(b->a)能夠通過的流量上限 += f
sum += f
sum就是圖最大流
很抽象
圖解
Edmonds-Karp算法
EK算法之所以是算法,在於它確定了在增廣圖上找最短路徑的方法,即BFS,因爲bfs的特性,b到終點一定是最短路
複雜度
bfs用鄰接表,複雜度爲O(e),更新邊複雜度O(e),而每次最少增長1的流量,假設圖最大流答案是f
,則最多需要f
次bfs
,所以總複雜度爲(O(e)+O(e) * f) = O(ef)
代碼實現細節
- 因爲邊具有屬性,鄰接表
adj[x]
不再存儲下一個頂點的信息,而是存儲所有從x出發的邊,在邊集合中的下標 - 一條邊具有兩個屬性:起點,當前允許的最大流量
- 因爲回退邊的存在,總共邊的數量是原圖邊數量的兩倍
- 在bfs的同時記錄路徑使用生成樹
father[]
數組即可做到,father[x]=-1
表示x未被訪問 - bfs的同時記錄最細流量,使用
min_flow[]
數組,假設有邊a->b
,那麼有min_flow[b] = min(該邊允許的流量最大值, min_flow[a])
- bfs返回值爲true表示找到,否則沒找到路徑
- 邊的權值爲0則不可走,因爲每次最小壓入1流量
- bfs的時候,正向邊或回退邊都可以走,只要權值不爲0
代碼
ps:如果您在着手實現深圳大學算法課程的實驗,請手下留情,不要全部copy,否則代碼查重系統將會展現他那殘忍無情的精準
#include <bits/stdc++.h>
using namespace std;
typedef struct edge
{
int st, ed, val; // 起點, 終點, 還能通過多少流量(其中起點不必要,只是方便打印調試信息)
edge(){}
edge(int a, int b, int c){st=a;ed=b;val=c;}
}edge;
int n,e,src,dst,ans; // 頂點數, 初始邊數, 源, 目, 答案
vector<vector<int>> adj; // adj[x][i]表示從x出發的第i條邊在邊集合中的下標
vector<edge> edges; // 邊集合
vector<int> min_flow; // min_flow[x]表示從起點到x的路徑中最細流
vector<int> father; // 生成樹
bool bfs_augment()
{
for(int i=0; i<n; i++) father[i]=-1;
for(int i=0; i<n; i++) min_flow[i]=1145141919; // inf
father[src] = src;
queue<int> q; q.push(src);
while(!q.empty())
{
int x=q.front(),y; q.pop();
if(x==dst) return true;
for(int i=0; i<adj[x].size(); i++)
{
edge e = edges[adj[x][i]];
y = e.ed;
if(father[y]!=-1 || e.val==0) continue;
father[y] = x;
min_flow[y] = min(e.val, min_flow[x]);
q.push(y);
}
}
return false;
}
void graph_update()
{
int x, y=dst, flow=min_flow[dst], i;
cout<<"更新流量: "<<flow<<" 路徑: ";
ans += flow;
vector<int> path;
while(y!=src) // 沿着生成樹找起點並沿途更新邊
{
path.push_back(y);
x = father[y];
for(i=0; i<adj[x].size(); i++) if(edges[adj[x][i]].ed==y) break;
edges[adj[x][i]].val -= flow;
for(i=0; i<adj[y].size(); i++) if(edges[adj[y][i]].ed==x) break;
edges[adj[y][i]].val += flow;
y = x;
}
path.push_back(y);
for(int i=path.size()-1; i>=0; i--) cout<<path[i]<<" "; cout<<endl;
}
int main()
{
cin>>n>>e>>src>>dst;
adj.resize(n);
father.resize(n);
min_flow.resize(n);
edges.resize(e*2);
for(int i=0; i<e; i++)
{
int st, ed, limit; cin>>st>>ed>>limit;
edges[2*i] = edge(st, ed, limit);
edges[2*i+1] = edge(ed, st, 0);
adj[st].push_back(2*i); adj[ed].push_back(2*i+1);
}
while(1)
{
if(!bfs_augment()) break;
graph_update();
// for(int i=0; i<edges.size(); i++) cout<<edges[i].st<<" -> "<<edges[i].ed<<" val="<<edges[i].val<<endl;
}
cout<<"最大流:"<<ans<<endl;
return 0;
}
/*
6 7 0 5
0 2 2
0 1 2
1 4 1
2 4 5
2 3 2
3 5 1
4 5 2
7 11 0 6
0 1 3
0 3 3
1 2 4
2 0 3
2 3 1
2 4 2
3 4 2
3 5 6
4 1 1
4 6 1
5 6 9
*/