网络流
- 网络流模型与最大流问题
- Ford Fulkerson 框架
- Edmonds-Kap 算法
- Dinic 算法
- MPM 多源点多汇点最大流算法
- MCF 最小费用流
- PushRelabel 压入与重标记算法
网络流模型与最大流问题
生活中的电网、水管网、交通运输网都有一个共同点:在网络传输中有方向和容量。
没错,给一张有向图增加一些限定,就是一个网络流模型。
限定:
- 一个源点 s:入度为 0,所有流量的起点
- 一个汇点 t:出度为 0,所有流量的终点
- 容量限制:任何一条边的流量都不能大于容量
- 流量限制:除了源点、汇点,其他节点,流入量 = 流出量
网络流中最常见的问题就是网络最大流,即在满足容量限制、流量限制的条件下,从源点 t 最多能发出多少流量,这些流量可以存在整个网络中,最终汇聚到汇点 s。
Ford Fulkerson 框架
一个网络流模型:
最大流路径:
要求出这样一条路径,需要引入反向边,把网络流模型变成残量图模型:
- 原图(网络流模型):节点 v -> 节点 w 的流量为 f,容量为 c
- 新图(残量图模型):节点 v -> 节点 w 的权值为 c - f,节点 w -> 节点 v 的权值为 f
在残量图中,我们只要找到一条所有边的权值都大于 0 的路径:
- 从源点 s 到汇点 t 的路径,沿途的边都大于 0
这样一条路,就称为 — 增广路径。
这样的路径一定能注入新的流量。
Ford Fulkerson 思想:
- 从原始图出发,创建残量图
- 在残量图中不断寻找增广路径(源点->汇点,所有边的权值都大于 0 的路径)
- 每找到一条增广路径,意味着流量增加了,这时候残量图需要更新权值
- 再继续找增广路径,直到没有增广路径为止
Ford Fulkerson 思想在于,寻找增广路径,但没有说怎么寻找增广路径,所以找增广路径的方法不同,算法名字也就不同,但其实都是在找增广路径。
Ford Fulkerson 是一个算法框架,有不同的实现:
- BFS(Edmond-Karp 算法)
- DFS(Ford-Fulkerson 算法)
- 先用 BFS 分层,再用 DFS 寻找(Dinic 算法)
搜索具体过程,请看 Edmond-Karp 算法。
Ford-Fulkerson 算法,代码实现:
#define INF 0xfffffff
// 无穷大
int n, m, s, t;
// s是源点,t是汇点
bool vis[MAXN];
/* 链式前向星,使用数组模拟邻接表的方法 */
struct Edge {
int to; // 目标顶点编号
int w; // 边的权值
int next; // 下一条边的编号
} edges[MAXM];
int head[MAXM]; // head[i]表示从顶点i出发的第一条边在edge[]中的位置
/* 基本思想:在网络中寻找增广路,沿增广路增加流量,直到不存在增广路 */
int dfs_get_augmenting_path(int p, int flow) {
if (p == t) // 到达终点
return flow; // 返回这条增广路的流量
vis[p] = true;
for (int eg = head[p]; eg; eg = edges[eg].next) {
int to = edges[eg].to, vol = edges[eg].w, c;
// 返回的条件是残余容量大于0、未访问过该点且接下来可以达到终点(递归地实现)
// 传递下去的流量是边的容量与当前流量中的较小值
if (vol > 0 && !vis[to] && (c = dfs_get_augmenting_path(to, min(vol, flow))) != -1) {
edges[eg].w -= c;
edges[eg ^ 1].w += c;
// 这是链式前向星取反向边的一种简易的方法
// 建图时要把cnt置为1,且要保证反向边紧接着正向边建立
return c;
}
}
return -1;
// 无法到达终点
}
int Ford_Fulkerson() {
int max_flow = 0, c;
while ( (c = dfs_get_augmenting_path(s, INF)) != -1 ) {
memset(vis, 0, sizeof(vis));
max_flow += c;
}
return max_flow;
}
时间复杂度:,
边数、
因为不断寻找增广路径,每寻找到一条,图的最大流都会增加 E,最多情况下需要找 。
Edmonds-Kap 算法
算法一开始,就把网络流模型变成残量图模型:
Edmonds-Kap 就是用 BFS 寻找增广路径(源点->汇点,所有边的权值都大于 0 的路径)。
- 在上图基础上,找到一条增广路径:
0 -> 1 -> 3
,并更新残量图权重: - 在上图基础上,找到一条增广路径:
0 -> 2 -> 3
,并更新残量图权重:
- 在上图基础上,找到一条增广路径:
0 -> 1 -> 2 -> 3
,并更新残量图权重:
现在已经搜索不到增广路径,所以算法结束,当前流是最大流。
代码实现:
#define INF 0xfffffff
// 无穷大
int n, m, s, t, last[MAXN], flow[MAXN];
// s是源点,t是汇点
/* 链式前向星,使用数组模拟邻接表的方法 */
struct Edge {
int to; // 目标顶点编号
int w; // 边的权值
int next; // 下一条边的编号
} edges[MAXM];
int head[MAXM]; // head[i]表示从顶点i出发的第一条边在edge[]中的位置
int bfs_get_augmenting_path() {
memset(last, -1, sizeof(last));
queue<int> q;
q.push(s);
flow[s] = INF;
while (!q.empty()) {
int p = q.front();
q.pop();
if (p == t) // 到达汇点,结束搜索
break;
for (int eg = head[p]; eg; eg = edges[eg].next) {
int to = edges[eg].to, vol = edges[eg].w;
if (vol > 0 && last[to] == -1) { // 如果残余容量大于0且未访问过(所以last保持在-1)
last[to] = eg;
flow[to] = min(flow[p], vol);
q.push(to);
}
}
}
return last[t] != -1;
}
int Edmonds_Kap() {
int maxflow = 0;
while ( bfs_get_augmenting_path() ) {
maxflow += flow[t]; // 最大流更新
/* 从汇点原路返回更新残余容量 */
for (int i = t; i != s; i = edges[last[i] ^ 1].to) {
edges[last[i]].w -= flow[t];
edges[last[i] ^ 1].w += flow[t];
}
}
return maxflow;
}
时间复杂度:,
边数、
最多通过 时间找不到增广路径了,而找一次增广路径需要
,所以是
。
Edmonds-Kap 算法,图论库(图论文章系列)实现:
- 前置代码来源:adjMatrix * G 的 create_graph( *G, graph_kind )。
/* 深拷贝:保护数据,防止原数据因为当前算法被篡改 */
void deep_copy(adjMatrix *G, adjMatrix *nG){
nG->n = G->n;
nG->m = G->m;
nG->kind = G->kind;
nG->list = (T *) malloc(sizeof(T) * MAX_vertex);
for(int i=0; i<G->n; i++)
nG->list[i] = G->list[i];
assert(nG->list != NULL);
nG->matrix = (int **)malloc(sizeof(int) * MAX_vertex);
for (int i = 0; i < MAX_vertex; i++)
nG->matrix[i] = (int *)malloc(sizeof(int) * MAX_edge);
for(int i=0; i<G->n; i++)
for(int j=0; j<G->m; j++){
nG->matrix[i][j] = G->matrix[i][j];
}
}
/* 基本思想:在网络中寻找增广路,沿增广路增加流量,直到不存在增广路 */
bool get_augmenting_path(adjMatrix *nG, int s, int t, int *pre){
bool visited[MAX_edge];
memset(visited, false, sizeof(visited));
std::queue <int> q;
q.push(s);
visited[s] = true;
pre[s] = -1;
while (!q.empty()) {
int j = q.front();
q.pop();
for (int i=0; i<nG->n; i++) {
if (visited[i]==false && nG->matrix[j][i] > 0) {
q.push(i);
pre[i] = j;
visited[i] = true;
}
}
}
return (visited[t] == true);
}
int Edmonds_Kap(adjMatrix *G, adjMatrix *nG, int s, int t){
deep_copy(G, nG);
int max_flow = 0;
int pre[MAX_edge];
while( get_augmenting_path(nG, s, t, pre) ) {
int path_flow = INT_MAX;
/* 计算增广路径上的最小值 */
for(int i=t; i!=s; i=pre[i]){
int j = pre[i];
path_flow = std::min(path_flow, nG->matrix[j][i]);
}
max_flow += path_flow; // 最大流更新
/* 根据增广路径更新残差图 */
for(int i=t; i!=s; i=pre[i]) {
int j = pre[i];
nG->matrix[j][i] -= path_flow;
nG->matrix[i][j] += path_flow;
}
}
return max_flow;
}
Dinic 算法
最常用的网络流算法是 Dinic 算法。作为前俩者算法的优化,它选择了先用BFS分层,再用DFS寻找。
时间复杂度上界是 。
所谓分层,其实就是预处理出源点到每个点的距离(注意每次循环都要预处理一次,因为有些边可能容量变为0不能再走)。我们只往层数高的方向增广,可以保证不走回头路也不绕圈子。
我们可以使用多路增广节省很多花在重复路线上的时间:在某点DFS找到一条增广路后,如果还剩下多余的流量未用,继续在该点DFS尝试找到更多增广路。
此外还有当前弧优化。因为一条边增广一次后,就不会再次增广了,所以下次增广时不需要再考虑这条边。
我们把 head 数组复制一份,但不断更新增广的起点。
/* 链式前向星,使用数组模拟邻接表的方法 */
struct Edge {
int to; // 目标顶点编号
int w; // 边的权值
int next; // 下一条边的编号
} edges[MAXM];
int head[MAXM]; // head[i]表示从顶点i出发的第一条边在edge[]中的位置
如果不使用 链式前向星,使用 vector
。
class Edge{
public:
int u, v;
T c;
bool is_residual;
Edge* counter_edge; // 指向反向边
};
完整代码:
int n, m, s, t, lv[MAXN], cur[MAXN]; // lv是每个点的层数,cur用于当前弧优化标记增广起点
inline bool bfs() { // BFS分层
memset(lv, -1, sizeof(lv));
lv[s] = 0;
memcpy(cur, head, sizeof(head)); // 当前弧优化初始化
queue<int> q;
q.push(s);
while (!q.empty()) {
int p = q.front();
q.pop();
for (int eg = head[p]; eg; eg = edges[eg].next) {
int to = edges[eg].to, vol = edges[eg].w;
if (vol > 0 && lv[to] == -1)
lv[to] = lv[p] + 1, q.push(to);
}
}
return lv[t] != -1; // 如果汇点未访问过说明已经无法达到汇点,此时返回false
}
int dfs(int p, int flow) {
if (p == t)
return flow;
int rmn = flow; // 剩余的流量
for (int eg = cur[p]; eg && rmn; eg = edges[eg].next) { // 如果已经没有剩余流量则退出
cur[p] = eg; // 当前弧优化,更新当前弧
int to = edges[eg].to, vol = edges[eg].w;
if (vol > 0 && lv[to] == lv[p] + 1) { // 往层数高的方向增广
int c = dfs(to, min(vol, rmn)); // 尽可能多地传递流量
rmn -= c; // 剩余流量减少
edges[eg].w -= c; // 更新残余容量
edges[eg ^ 1].w += c; // 链式前向星的cnt需要初始化为1(或-1)才能这样求反向边
}
}
return flow - rmn; // 返回传递出去的流量的大小
}
int dinic() {
int ans = 0;
while ( bfs() )
ans += dfs(s, INF);
return ans;
}
MPM 多源点多汇点最大流算法
MCF 最小费用流
PushRelabel 压入与重标记算法