网络流
最大流
二分图匹配
对增广路的深入思考
最小费用流
转运问题
运输问题
任务分配问题
线性规划
很多问题都可以抽象为包含顶点和边有容量限制的网络。本章从实际需求出发,介绍解决这些特定问题的算法。
任务分配——有一堆任务待分配。对不同任务,不同员工所需费用不同。现在要求找到一种分配方式使得总费用最小。
二分图匹配——一下求职者要面试一系列工作岗位,现在要求找到一种合理的分配方式,使得可能多的人找到他们能胜任的工作。
最大流——给定一个网络,网络中每条边显示两地间潜在货运量,现在要求计算网络能支持的最大流量。
运输问题——确定从工厂向零售商店运输商品性价比最高的方法。
转运问题——确定从工厂向零售商店运输商品性价比最高的方法。但在这类问题中,我们可以使用一些中转站作为临时仓库。
下图展示了将以上问题转化为网络流问题的过程,他们都是从一或多个源点流向一或多个汇点。
![3c5d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/3c5d5dfc-d517-eb11-8da9-e4434bdf6706.png)
在本章,我们将阐述Ford-fulkerson算法,它用于解最大流问题,当然也可直接用于二分图匹配问题(如上图)。此外,一旦理解了该算法,最小费用流问题如转运问题、运输问题、任务分配问题也能相应解出。
1网络流
流网络可抽象为有向图G=(V,E)。V:顶点集,E:边集。
图是连通的。有一个特殊的源点s,负责生产商品,商品通过图的边运输到汇点t进行消费。
每条边(u,v)有一个流量f(u,v),表示从u运输到v的商品单位数目,一个容量c(u,v),表示能从u运输到v的最多的商品单元数目。
![3d5d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/3d5d5dfc-d517-eb11-8da9-e4434bdf6706.png)
可行流需满足三大条件:容量限制 流量守恒 反对称性
![3e5d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/3e5d5dfc-d517-eb11-8da9-e4434bdf6706.png)
接下来的算法中我们提到的网络路径指的是没有环的路径,有不同顶点<v1,v2,...,vn>连接的n-1条边构成。
2最大流
在一个流网络中,如果给定了边集E中所有有向边e=(u,v)的容量限制c(u,v),我们可以计算出顶点s和t之间的最大流。也就是说,在每条边都有容量限制的情况下,从源顶点s输出的,通过这个网络达到汇点的最大流量是可以计算出来的,从一个可行的最小流(例如每条边的流量为0的流)开始,Ford- Fulkerson算法会持续寻找从s到t的增广路径,继而增加更多的流。如果再也无法找到增广路径,那么算法终止。最大流最小割定理(Ford- Fulkerson,1962)保证了,在没有非负流量和非负客量限制的前提下,Ford- Fulkerson算法总是能够终止并找出网络中的最大流。
流网络可以定义为指定源顶点和终点的图G=(V,E),其中E中的每条有向边都有一个整数容量c(u,v)和实际流量f(u,v)。路径前向边和后向边组成,前向边是指由连续顶点
2.1输入/输出
对于每条边(u,v),Ford- Fulkerson算法会计算出整数流量f(u,v)。该算法结束时还会顺带输出网络的最小割,即由边集组成的一个瓶颈,来防止更多的单元在网络中从s流向t。
2.1解决方案
处理最大流问题最著名的就是Ford- Fulkerson算法,它包含了几种运行时间不同的具体实现。Ford- Fulkerson算法主要包含3个子算法思想:残差网络思想、增广路径思想、最大流最小割思想。
整体上,Ford- Fulkerson算法思想是一个迭代算法。初始时假定所有路径流量为0.每次迭代时通过增广路经扩大流量,利用最大流最小割定理判定算法运行结束。为了更好地理解算法,我们先分别介绍Ford- Fulkerson算法的3个组成部分。
1)残差网络
它是流网络的一部分。我们定义在不违反流网络G中容量限制的条件下,从u到v可以压入的额外流量就是流网络G中u、v的残留流量,容纳这些残留流量的边组成的子网络就是残留网络。残留网络的流量满足
注意:
2)增广路经
残留网络
增广路经的流量满足
![405d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/405d5dfc-d517-eb11-8da9-e4434bdf6706.png)
![415d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/415d5dfc-d517-eb11-8da9-e4434bdf6706.png)
3)流网络的割
![425d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/425d5dfc-d517-eb11-8da9-e4434bdf6706.png)
![435d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/435d5dfc-d517-eb11-8da9-e4434bdf6706.png)
最小割:一个流网络中净流量最小的割。
![445d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/445d5dfc-d517-eb11-8da9-e4434bdf6706.png)
割的容量
![455d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/455d5dfc-d517-eb11-8da9-e4434bdf6706.png)
最大流最小割定理:1)2)3)等价1)流f是流网络的最大流 2)残留网络G_{f}不含增广路经3 )对G的某割,存在关系|f|=c(S,T)
下面开始Ford-folkerson算法的正式表演:
迭代算法。开始时任意找出从s到t的一条增广路p,往p上每条边的流量f加上残留容量,迭代此过程,更新每一对顶点间网络流直到不存在增广路。
伪代码:
Ford-F(G,s,t)
for each edge (u,v) in E[G]
do f(u,v)=0
while exist a path p from s to t in
do :min( |(u,v) in p}
for each edge (u,v) in p:
do f(u,v)+= ,f(v,u)=-f(u,v)
正式c++代码:(广度优先搜素增广路经)
// C++ program for implementation of Ford Fulkerson algorithm
#include <iostream>
#include <limits.h>
#include <string.h>
#include <queue>
using namespace std;
// Number of vertices in given graph
#define V 6
/* Returns true if there is a path from source 's' to sink 't' in
residual graph. Also fills parent[] to store the path */
bool bfs(int rGraph[V][V], int s, int t, int parent[])
{
// Create a visited array and mark all vertices as not visited
bool visited[V];
memset(visited, 0, sizeof(visited));
// Create a queue, enqueue source vertex and mark source vertex
// as visited
queue <int> q;
q.push(s);
visited[s] = true;
parent[s] = -1;
// Standard BFS Loop
while (!q.empty())
{
int u = q.front();
q.pop();
for (int v=0; v<V; v++)
{
if (visited[v]==false && rGraph[u][v] > 0)
{
q.push(v);
parent[v] = u;
visited[v] = true;
}
}
}
// If we reached sink in BFS starting from source, then return
// true, else false
return (visited[t] == true);
}
// Returns the maximum flow from s to t in the given graph
int fordFulkerson(int graph[V][V], int s, int t)
{
int u, v;
// Create a residual graph and fill the residual graph with
// given capacities in the original graph as residual capacities
// in residual graph
int rGraph[V][V]; // Residual graph where rGraph[i][j] indicates
// residual capacity of edge from i to j (if there
// is an edge. If rGraph[i][j] is 0, then there is not)
for (u = 0; u < V; u++)
for (v = 0; v < V; v++)
rGraph[u][v] = graph[u][v];
int parent[V]; // This array is filled by BFS and to store path
int max_flow = 0; // There is no flow initially
// Augment the flow while tere is path from source to sink
while (bfs(rGraph, s, t, parent))
{
// Find minimum residual capacity of the edges along the
// path filled by BFS. Or we can say find the maximum flow
// through the path found.
int path_flow = INT_MAX;
for (v=t; v!=s; v=parent[v])
{
u = parent[v];
path_flow = min(path_flow, rGraph[u][v]);
}
// update residual capacities of the edges and reverse edges
// along the path
for (v=t; v != s; v=parent[v])
{
u = parent[v];
rGraph[u][v] -= path_flow;
rGraph[v][u] += path_flow;
}
// Add path flow to overall flow
max_flow += path_flow;
}
// Return the overall flow
return max_flow;
}
// Driver program to test above functions
int main()
{
// Let us create a graph shown in the above example
int graph[V][V] = { {0, 16, 13, 0, 0, 0},
{0, 0, 10, 12, 0, 0},
{0, 4, 0, 0, 14, 0},
{0, 0, 9, 0, 0, 20},
{0, 0, 0, 7, 0, 4},
{0, 0, 0, 0, 0, 0}
};
cout << "The maximum possible flow is " << fordFulkerson(graph, 0, 5);
return 0;
}
Output:
The maximum possible flow is 23
Ford-Fulkerson算法的上述实现称为Edmonds-Karp算法。Edmonds-Karp的想法是在Ford Fulkerson实现中使用BFS,因为BFS总是选择一条边数最少的路径(由于DFS)。设计优良的广度优先搜索可以在O(V+E)时间内找到增广路径(注意:上述代码BFS花费的则是O(E^2)时间),实际上就是O(E),因为连通的流网络中V比E少得多。cormen2009证明了Edmonds-Karp算法的性能是O(VE^2)。
Dinic最大流算法
Edmond Karp实现的时间复杂度为O(VE^2),而Dinic算法更快,时间复杂度为O(EV^2)。
与Edmond Karp的算法一样,Dinic的算法使用以下概念:
- 如果残差图中没有s-t路径,则流量最大。
- BFS循环使用。虽然在两种算法中使用BFS的方式有所不同。
在Edmond-Karp算法中,我们使用BFS查找增广路经并通过该路径发送流。在Dinic算法中,我们使用BFS来检查是否有可能有更多的流量,并构造level graph。在level graph中,我们为所有节点分配level ,一个节点的level 是该节点到源的最短距离(以边的数量表示)。一旦构建了level 图,我们就可以使用该level 图发送多个流。这就是它比Edmond Karp更好的原因:在Edmond Karp中,我们仅发送通过BFS所找路径发送的流。
Dinic算法概述:
1)将残差图G初始化为给定图。
1)进行G的BFS构造一个level 图(或为顶点分配level ),并检查是否更多的流量是可能的。
a)如果不可能有更多的流量,则返回。
b)使用level 图在G中发送多个流 直到达到阻塞流量。在这里使用level 图表示在每个flow中从s到t路径节点的level 应为0、1、2 ...(按顺序)。
如果无法使用level 图发送更多的流,则该流为“阻塞流”,即,不再存在任何s-t路径,从而路径顶点的level依次为0、1、2…。阻塞流可以视为与此处讨论的贪婪算法中的最大流路相同。
初始残差图(与给定图相同)
![465d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/465d5dfc-d517-eb11-8da9-e4434bdf6706.png)
总流量= 0
第一次迭代:我们使用BFS为所有节点分配level。我们还会检查是否有更多流量(或残差图中有s-t路径)。
![485d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/485d5dfc-d517-eb11-8da9-e4434bdf6706.png)
现在,我们使用level来发现阻塞流(这意味着每个流路径的level都应为0、1、2、3)。我们一起发送三个流。与我们一次发送一个流的Edmond Karp相比,这是优化的地方。
路径s – 1 – 3 – t上有4个流量单位。
路径s – 1 – 4 – t上有6个流量单位。
路径s – 2 – 4 – t上有4个流量单位。
总流量=总流量+ 4 + 6 + 4 = 14
一轮迭代后,残差图变为以下。
![4a5d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/4a5d5dfc-d517-eb11-8da9-e4434bdf6706.png)
第二次迭代:我们使用上述修改后的残差图的BFS为所有节点分配新level。我们还会检查是否有更多流量(或残差图中有st路径)。
![4c5d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/4c5d5dfc-d517-eb11-8da9-e4434bdf6706.png)
现在,我们发现使用level来阻塞流(意味着每个流路径的level都应为0、1、2、3、4)。这次我们只能发送一个流。
路径s上5个流量单位s– 2 – 4 – 3 – t
总流量=总流量+ 5 = 19
新的残差图是
![4d5d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p05.5ceimg.com/content/4d5d5dfc-d517-eb11-8da9-e4434bdf6706.png)
第三次迭代:我们运行BFS并创建一个level图。我们还会检查是否有更多流量,并仅在可能的情况下进行。这次残差图中没有st路径,因此我们终止了算法。
以下是Dinic算法的c ++实现:
// C++ implementation of Dinic's Algorithm
#include<bits/stdc++.h>
using namespace std;
// A structure to represent a edge between
// two vertex
struct Edge
{
int v ; // Vertex v (or "to" vertex)
// of a directed edge u-v. "From"
// vertex u can be obtained using
// index in adjacent array.
int flow ; // flow of data in edge
int C; // capacity
int rev ; // To store index of reverse
// edge in adjacency list so that
// we can quickly find it.
};
// Residual Graph
class Graph
{
int V; // number of vertex
int *level ; // stores level of a node
vector< Edge > *adj;
public :
Graph(int V)
{
adj = new vector<Edge>[V];
this->V = V;
level = new int[V];
}
// add edge to the graph
void addEdge(int u, int v, int C)
{
// Forward edge : 0 flow and C capacity
Edge a{v, 0, C, adj[v].size()};
// Back edge : 0 flow and 0 capacity
Edge b{u, 0, 0, adj[u].size()};
adj[u].push_back(a);
adj[v].push_back(b); // reverse edge
}
bool BFS(int s, int t);
int sendFlow(int s, int flow, int t, int ptr[]);
int DinicMaxflow(int s, int t);
};
// Finds if more flow can be sent from s to t.
// Also assigns levels to nodes.
bool Graph::BFS(int s, int t)
{
for (int i = 0 ; i < V ; i++)
level[i] = -1;
level[s] = 0; // Level of source vertex
// Create a queue, enqueue source vertex
// and mark source vertex as visited here
// level[] array works as visited array also.
list< int > q;
q.push_back(s);
vector<Edge>::iterator i ;
while (!q.empty())
{
int u = q.front();
q.pop_front();
for (i = adj[u].begin(); i != adj[u].end(); i++)
{
Edge &e = *i;
if (level[e.v] < 0 && e.flow < e.C)
{
// Level of current vertex is,
// level of parent + 1
level[e.v] = level[u] + 1;
q.push_back(e.v);
}
}
}
// IF we can not reach to the sink we
// return false else true
return level[t] < 0 ? false : true ;
}
// A DFS based function to send flow after BFS has
// figured out that there is a possible flow and
// constructed levels. This function called multiple
// times for a single call of BFS.
// flow : Current flow send by parent function call
// start[] : To keep track of next edge to be explored.
// start[i] stores count of edges explored
// from i.
// u : Current vertex
// t : Sink
int Graph::sendFlow(int u, int flow, int t, int start[])
{
// Sink reached
if (u == t)
return flow;
// Traverse all adjacent edges one -by - one.
for ( ; start[u] < adj[u].size(); start[u]++)
{
// Pick next edge from adjacency list of u
Edge &e = adj[u][start[u]];
if (level[e.v] == level[u]+1 && e.flow < e.C)
{
// find minimum flow from u to t
int curr_flow = min(flow, e.C - e.flow);
int temp_flow = sendFlow(e.v, curr_flow, t, start);
// flow is greater than zero
if (temp_flow > 0)
{
// add flow to current edge
e.flow += temp_flow;
// subtract flow from reverse edge
// of current edge
adj[e.v][e.rev].flow -= temp_flow;
return temp_flow;
}
}
}
return 0;
}
// Returns maximum flow in graph
int Graph::DinicMaxflow(int s, int t)
{
// Corner case
if (s == t)
return -1;
int total = 0; // Initialize result
// Augment the flow while there is path
// from source to sink
while (BFS(s, t) == true)
{
// store how many edges are visited
// from V { 0 to V }
int *start = new int[V+1];
// while flow is not zero in graph from S to D
while (int flow = sendFlow(s, INT_MAX, t, start))
// Add path flow to overall flow
total += flow;
}
// return maximum flow
return total;
}
// Driver program to test above functions
int main()
{
Graph g(6);
g.addEdge(0, 1, 16 );
g.addEdge(0, 2, 13 );
g.addEdge(1, 2, 10 );
g.addEdge(1, 3, 12 );
g.addEdge(2, 1, 4 );
g.addEdge(2, 4, 14);
g.addEdge(3, 2, 9 );
g.addEdge(3, 5, 20 );
g.addEdge(4, 3, 7 );
g.addEdge(4, 5, 4);
// next exmp
/*g.addEdge(0, 1, 3 );
g.addEdge(0, 2, 7 ) ;
g.addEdge(1, 3, 9);
g.addEdge(1, 4, 9 );
g.addEdge(2, 1, 9 );
g.addEdge(2, 4, 9);
g.addEdge(2, 5, 4);
g.addEdge(3, 5, 3);
g.addEdge(4, 5, 7 );
g.addEdge(0, 4, 10);
// next exp
g.addEdge(0, 1, 10);
g.addEdge(0, 2, 10);
g.addEdge(1, 3, 4 );
g.addEdge(1, 4, 8 );
g.addEdge(1, 2, 2 );
g.addEdge(2, 4, 9 );
g.addEdge(3, 5, 10 );
g.addEdge(4, 3, 6 );
g.addEdge(4, 5, 10 ); */
cout << "Maximum flow " << g.DinicMaxflow(0, 5);
return 0;
}
Output:
Maximum flow 23
时间复杂度:O(EV2)。进行BFS构造level图需要O(E)时间。发送更多流量直到达到阻塞流量需要O(VE)时间。外循环最多运行O(V)时间。在每次迭代中,我们构造新的level图并查找阻塞流。可以证明,level的数量在每次迭代中至少增加了一个(请参见https://www.youtube.com/watch?v=uM06jHdIC70作为证明)。因此,外循环最多运行O(V)次。因此,总体时间复杂度为O(EV^2)。
最大流算法变体(添加了顶点容量或无向边该咋办呐)
![4f5d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/4f5d5dfc-d517-eb11-8da9-e4434bdf6706.png)
![505d5dfc-d517-eb11-8da9-e4434bdf6706.png](http://p03.5ceimg.com/content/505d5dfc-d517-eb11-8da9-e4434bdf6706.png)