圖-網路流-最大流問題
網路流是一種類比水流的解決問題的方法,與線性規劃密切相關。網路流的理論和應用在不斷髮展,具有增益的流、多終端流、多商品流以及網路流的分解與合成等新課題。網路流的應用已遍及通訊、運輸、電力、工程規劃、任務分派、裝置更新以及計算機輔助設計等眾多領域。
網路流是一個適用範圍相當廣的模型,相關的演算法也非常多。儘管如此,網路流中的概念、思想和基本演算法並不難理解。
首先看一下最大流問題。
對於一條邊(u,v),它的物品上限稱為容量(capacity),記為c(u,v)(對於不存在的邊(u,v),c(u,v)=0);實際運送的物品稱為流量(flow),記為f(u,v)。注意,把3個物品從u運送到v,又把5個物品從v運送到u沒什麼意義,因為它等價於把兩個物品從v運送到u。這樣,就可以規定f(u,v)和f(v,u)最多隻有一個正數(可以均為0),並且f(u,v)=-f(v,u)。這樣規定就好比把3個物品從u運送到v等價於把-3個物品從v運送到u一樣。
最大流問題的目標是把最多的物品從s運送到t,而其他結點都只是中轉,因此對於除了結點s和t外的任意結點u,f(u,v)之和=0(這些f中有些是負數)。從s運送出來的物品數目等於到達t的物品數目,而這正是此處最大化的目標。
在最大流問題中,容量c和流量f滿足三個性質:容量限制(f(u,v) <= c(u,v))、斜對稱性(f(u,v)= -f(v,u))和流量平衡(對於除了結點s和t外的任意結點u,.問題的目標是最大化|f| = =,即從s點流出的淨流量(它也等於流入點的淨流量)。
下面介紹求解最大流問題的演算法:增廣路演算法。
演算法思想很簡單,從零流(所有邊的流量均為0)開始不斷增加流量,保持每次增加流量後都滿足容量限制、斜對稱性和流量平衡3個條件。計算出圖中的每條邊上容量與流量之差(稱為殘餘容量,簡稱殘量),得到殘量網路。
殘量網路中任何一條從s到t的有向道路都對應一條原圖中的增廣路-只要求出該道路中所有殘量的最小值d,把對應的所有邊上的流量增加d即可,這個過程稱為增廣。不難驗證,如果增廣前的流量滿足3個條件,增廣後仍然滿足。顯示,只要殘量網路存在增廣路,流量就可以增大。可以證明它的逆命題:如果殘量網路中不存在增廣路,則當前流就是最大流。這就是著名的增廣路定理。
找任意路徑最簡單的辦法無疑是用DFS,但很容易找出讓它很慢的例子,一個稍微好些的方法是使用BFS,這就是Edmonds-Karp演算法。
邊的儲存結構定義:
struct Edge { int from; int to; int flow; //流量 int cap; //容量 Edge(int u, int v, int f, int c):from(u), to(v), flow(f), cap(c){} };
Edmonds-Karp演算法實現:
struct EdmondsKarp
{
int n, m;
vector<Edge> edges; //邊數的兩倍
vector<int> G[maxn]; //鄰接表,G[i][j]表示結點i的第j條邊在e陣列的序號
int a[maxn]; //當起點到i的可改進量
int p[maxn]; //最短路樹上的入弧編號
void init(int n)
{
this->n = n;
edges.clear();
for(int i = 0; i < n; i++)
G[i].clear();
}
void AddEdge(int from, int to, int cap)
{
edges.push_back(Edge(from, to, 0, cap));
edges.push_back(Edge(to, from, 0, 0)); //反向弧
m = edges.size();
G[from].push_back(m-2);
G[to].push_back(m-1);
}
int Maxflow(int s, int t)
{
int flow, x, i, u;
flow = 0;
for(;;)
{
memset(a, 0, sizeof(a));
a[s] = INF;
queue<int> Q;
Q.push(s);
while(!Q.empty())
{
x = Q.front();
Q.pop();
for(i = 0; i < G[x].size(); i++)
{
Edge &e = edges[G[x][i]];
if(!a[e.to] && e.cap > e.flow) //a[i]是正數,代替了原來的vis標誌陣列
{
a[e.to] = min(a[x], e.cap-e.flow); //找小的殘量值
Q.push(e.to);
p[e.to] = G[x][i];
}
}
if(a[t]) //整條s-t道路上的最小殘量 到了t點退出
break;
}
if(!a[t]) //沒有增廣路,可以退出
break;
for(u = t; u != s; u = edges[p[u]].from)
{
edges[p[u]].flow += a[t]; //從t-s修改這條增廣路的流量
edges[p[u]^1].flow -= a[t]; //修改反向弧的流量
}
flow += a[t]; //累加
}
return flow;
}
};
注意上面程式碼的一個技巧:每條弧和對應的反向弧儲存在一起。邊0和1互為反向邊,邊2和3互為反向邊。一般地,邊i的反向邊為i^1,其中^為二進位制異或運算子。
在拓展結點的同時還需遞推出從s到每個結點i的路徑上的最小殘量a[i][,則a[t]就是整條s-t道路上的最小殘量。另外,由於a[i]總是正數,所以用它代替了原來的vis標誌陣列。上面的程式碼把流初始化為零流,但這不是必需的。只要初始流是可行的,就可以用增廣路演算法進行增廣。
關於反向弧:在走反向弧的時候,相當於把正向弧的流量退了回去,退出的流量由另一條路接管,這樣便可以使程式有後悔的機會。