【学习笔记】网络流基础(2)

割与最小割
:在容量网络 G(V, E) 中,设 E’ ⊆ E ,如果在 G 中删去 E’ 后,图 G 不再连通,则称 E’ 是 G 的割。割将图的顶点集合 V 划分成两部分:S 和 T = V - S,我们用 来表示一个割。
s-t 割:如果割划分的两个点集满足源点 V[s] ∈ S 、汇点 V[t] ∈ T,那么称这个割为网络的 s-t 割。
s-t 割 (S, T) 中的弧 <u, v> 称为割的前向弧,弧 <u, v>(u ∈ T, v ∈ S) 称为割的反向弧。
(注意,如无特殊说明,接下来所说的割均指 s-t 割)。
割的容量:设 (S, T) 为容量网络 G(V, E) 的一个割,其容量定义为所有前向弧的容量总和,用 c(S, T) 表示,即:
c(S, T) = ∑c(u, v) u ∈ S, v ∈ T, <u, v> ∈ E
例如下图中虚线经过的边组成了一个 s-t 割,它的容量是 3 + 7 = 10。
在这里插入图片描述
最小割:容量网络 G(V, E) 的最小割是指容量最小的割。
割的净流量:设 f 是容量网络 G(V, E) 的一个可行流, (S, T) 是 G 的一个
割,定义割的净流量 f(S, T) 为:
f(S,T) = ∑f(u, v) u ∈ S, v ∈ T, <u, v> ∈ E 或 <v, u> ∈ E
最大流最小割定理
对流量网络 G(V, E),其最大流的流量等于最小割的容量。
这个定理极为常用,借助这个定理,我们可以将很多问题抽象成最小割模型,再借助最大流算法来解决。
最小割将容量网络 G(V, E) 中的点集 V 分割成两个集合,其中点集 S 表示在残量网络中与源点联通的点集,点集 T = V - S。
求两个点集可以分两步:
1 求得最大流
2 在得到最大流后的残量网络中,从源点开始 DFS,所有被遍历到的点,即构成点集 S,剩余的点构成点集 T。
注意,虽然最小割中的边都是满流边,但满流边不一定都是最小割中边。
部分 C++ 代码

vector<int> vec; // 点集 S
bool vis[MAX_N];
void dfs1(int u) {
    
    
    vec.push_back(u);
    vis[u] = true;
    for (int i = p[u]; i != -1; i = e[i].fail) {
    
    
 		int v = e[i].v;
 		if (!vis[v] && e[i].c > 0) {
    
    
 			dfs1(v);
 		}
 	}
}
void minimum_cut() {
    
    
 	memset(vis, false, sizeof(vis));
 	dfs1(S);
 	for (int i = 0; i < (int)vec.size(); ++i) {
    
    
 		cout << vec[i] << " ";
 	}
 	cout << endl;
}

实现 DFS 求割集

#include <iostream>
#include <string.h>
#include <queue>
#include <vector>
using namespace std;
const int INF = 0x3f3f3f3f;
const int MAX_N = 100;
const int MAX_M = 10000;
struct edge {
    
    
    int v, c, fail;
} e[MAX_M];
int p[MAX_N], eid;
void init() {
    
    
    memset(p, -1, sizeof(p));
    eid = 0;
}
void insert(int u, int v, int c) {
    
    
    e[eid].v = v;
    e[eid].fail = p[u];
    e[eid].c = c;
    p[u] = eid++;
}
void addedge(int u, int v, int c) {
    
    
    insert(u, v, c);
    insert(v, u, 0);
}
int S, T;
int d[MAX_N];
bool bfs() {
    
    
    memset(d, -1, sizeof(d));
    queue<int> q;
    q.push(S);
    d[S] = 0;
    while (!q.empty()) {
    
    
        int u = q.front();
        q.pop();
        for (int i = p[u]; i != -1; i = e[i].fail) {
    
    
            int v = e[i].v;
            if (e[i].c > 0 && d[v] == -1) {
    
    
                q.push(v);
                d[v] = d[u] + 1;
            }
        }
    }
    return (d[T] != -1);
}
int dfs(int u, int flow) {
    
    
    if (u == T) {
    
    
        return flow;
    }
    int res = 0;
    for (int i = p[u]; i != -1; i = e[i].fail) {
    
    
        int v = e[i].v;
        if (e[i].c > 0 && d[u] + 1 == d[v]) {
    
    
            int tmp = dfs(v, min(flow, e[i].c));
            flow -= tmp;
            e[i].c -= tmp;
            res += tmp;
            e[i ^ 1].c += tmp;
            if (flow == 0) {
    
    
                break;
            }
        }
    }
    if (res == 0) {
    
    
        d[u] = -1;
    }
    return res;
}
int Dinic() {
    
    
    int res = 0;
    while (bfs()) {
    
    
        res += dfs(S, INF);
    }
    return res;
}
vector<int> vec;
bool vis[MAX_N];
void dfs1(int u) {
    
    
	vec.push_back(u);
 	vis[u] = true;
 	for (int i = p[u]; i != -1; i = e[i].fail) {
    
    
 		int v = e[i].v;
 		if (!vis[v] && e[i].c > 0) {
    
    
 			dfs1(v);
 		}
 	}
}
void minimum_cut() {
    
    
	memset(vis, false, sizeof(vis));
 	dfs1(S);
 	for (int i = 0; i < vec.size(); ++i) {
    
    
 		cout << vec[i] << " ";
	}
 	cout << endl;
}
int main() {
    
    
    int n, m;
    init();
    cin >> n >> m;
    for (int i = 0; i < m; ++i) {
    
    
        int u, v, flow;
        cin >> u >> v >> flow;
        addedge(u, v, flow);
    }
    cin >> S >> T;
    cout << Dinic() << endl;
    minimum_cut();
    return 0;
}

最小费用最大流
在前面的课程里,我们学习了最大流算法。在同一个网络中,可能存在多个总流量相同的最大流 f,我们可以在计算流量的基础之上,给网络中的弧增加一个单位流量的费用(简称费用),在确保流量最大的前提下总费用最小,这样的最大流被称为 最小费用最大流。
在这里插入图片描述
在上面这个网络中,弧上用逗号分隔的两个数分别为弧的容量和单位流量费用。
例如,一条流量为 2 、经过 S -> 1 -> 4 -> T 的流的费用为 (3 + 5 + 4) × 2 = 24。
最小费用最大流解决方法
在费用流问题中,弧相比一般最大流增加了一个费用属性,那么对于反向弧应该如何设置呢?

void addedge(int u, int v, int c, int w) {
    
     // 容量为 c,费用为 w
	insert(u, v, c, w);
	insert(v, u, 0, -w);
}

上述代码表示的是插入一条单向边的情况,如果插入的是双向边,就插入正反向两条边就可以了。
对于费用流中的反向弧,将剩余容量初始化为 0、费用设置为正向弧的相反数就可以了。
接下来介绍一个最常见的最小费用最大流算法:

  1. 网络初始流量为 0
  2. 在当前的剩余网络上求出从 S 到 T 的最短增广路,以每条弧的单位流量费用为边权。如果不存在,则算法结束
  3. 修改增广路上弧的流量,重复步骤 2

在大多数情况下,步骤 2 使用 SPFA 算法进行求解。在该算法结束时,求得的一定是所有最大流中总费用最小的。如果要求解最大费用最大流(所有最大流中费用最大的),用 SPFA 计算最长路,或将费用都设为相反数后求解最短路都可以。
C++ 示例代码

const int MAX_N = 1000;
const int MAX_M = 10000;
const int inf = 0x3f3f3f3f;
struct edge {
    
    
	int v, c, w, next; // v 表示边的另一个顶点,c 表示当前剩余容量,w 表示单位流量费用
} e[MAX_M];
int p[MAX_N], s, t, eid; // s 表示源点,t 表示汇点,需要在进行 costflow 之前设置完毕
void init() {
    
    
	memset(p, -1, sizeof(p));
    eid = 0;
}
void insert(int u, int v, int c, int w) {
    
    
	e[eid].v = v;
	e[eid].c = c;
	e[eid].w = w;
	e[eid].next = p[u];
	p[u] = eid++;
}
void addedge(int u, int v, int c, int w) {
    
    
	insert(u, v, c, w);
	insert(v, u, 0, -w);
}
bool inq[MAX_N];
int d[MAX_N]; // 如果到顶点 i 的距离是 0x3f3f3f3f,则说明不存在源点到 i 的最短路 
int pre[MAX_N]; // 最短路中连向当前节点的编号 
bool spfa() {
    
     // 以源点 s 为起点计算单源最短路,如果不存在从 s 到 t 的最短路则返回 false,否则返回 true 
	memset(inq, 0, sizeof(inq));
	memset(d, 0x3f, sizeof(d));
	memset(pre, -1, sizeof(pre));
	d[s] = 0;
	inq[s] = true;
	queue<int> q;
	q.push(s);
	while (!q.empty()) {
    
    
		int u = q.front();
		q.pop();
		inq[s] = false;
		for (int i = p[u]; i != -1; i = e[i].next) {
    
    
			if (e[i].c) {
    
    
				int v = e[i].v;
				if (d[u] + e[i].w < d[v]) {
    
    
					d[v] = d[u] + e[i].w;
					pre[v] = i;
					if (!inq[v]) {
    
    
						q.push(v);
						inq[v] = true;
					}
				}
			}
		}
	}
	return pre[t] != -1;
} 
int costflow() {
    
     // 计算最小费用最大流 
	int ret = 0;
	while (spfa()) {
    
    
		int flow = inf;
		for (int i = t; i != s; i = e[pre[i] ^ 1].v) {
    
    
			flow = min(flow, e[pre[i]].c); // 计算当前增广路上的最小流量 
		}
		for (int i = t; i != s; i = e[pre[i] ^ 1].v) {
    
    
			e[pre[i]].c -= flow;
			e[pre[i] ^ 1].c += flow;
			ret += e[pre[i]].w * flow;
		}
	}
	return ret;
}

实现最小费用最大流

#include <iostream>
#include <string.h>
#include <queue>
using namespace std;
const int MAX_N = 1000;
const int MAX_M = 10000;
const int inf = 0x3f3f3f3f;
struct edge {
    
    
    int v, c, w, next;
} e[MAX_M];
int p[MAX_N], s, t, eid;
void init() {
    
    
    memset(p, -1, sizeof(p));
    eid = 0;
}
void insert(int u, int v, int c, int w) {
    
    
    e[eid].v = v;
    e[eid].c = c;
    e[eid].w = w;
    e[eid].next = p[u];
    p[u] = eid++;
}
void addedge(int u, int v, int c, int w) {
    
    
    insert(u, v, c, w);
    insert(v, u, 0, -w);
}
bool vis[MAX_N];
int d[MAX_N];
int pre[MAX_N];
bool spfa() {
    
    
	memset(vis, 0, sizeof(vis));
	memset(d, 0x3f, sizeof(d));
 	memset(pre, -1, sizeof(pre));
 	d[s] = 0;
 	vis[s] = true;
 	queue<int> q;
 	q.push(s);
 	while (!q.empty()) {
    
    
 		int u = q.front();
 		q.pop();
 		vis[u] = false;
 		for (int i = p[u]; i != -1; i = e[i].next) {
    
    
 			if (e[i].c) {
    
    
 				int v = e[i].v;
 				if (d[u] + e[i].w < d[v]) {
    
    
 					d[v] = d[u] + e[i].w;
 					pre[v] = i;
 					if (!vis[v]) {
    
    
 						q.push(v);
 						vis[v] = true;
 					}
 				}
 			}
 		}
 	}
 	return pre[t] != -1;
}
int costflow() {
    
    
	int ret = 0;
 	while(spfa()) {
    
    
        int flow = inf;
		for (int i = t; i != s; i = e[pre[i]^1].v) {
    
    
 			flow = min(e[pre[i]].c, flow);
		}
        for (int i = t; i != s; i = e[pre[i]^1].v) {
    
    
 			e[pre[i]].c -= flow;
 			e[pre[i]^1].c += flow;
 			ret += e[pre[i]].w * flow;
		}
 	}
 	return ret;
}
int main() {
    
    
	int n, m;
	init();
	cin >> n >> m;
	for (int i = 0; i < m; ++i) {
    
    
 		int u, v, c, w;
 		cin >> u >> v >> c >> w;
 		addedge(u, v, c, w);
	}
	cin >> s >> t;
	cout << costflow() << endl;
    return 0;
}

网络流基础的讲解就到这里,希望大家能有所收获。

猜你喜欢

转载自blog.csdn.net/yueyuedog/article/details/111879520