割与最小割
割 :在容量网络 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、费用设置为正向弧的相反数就可以了。
接下来介绍一个最常见的最小费用最大流算法:
- 网络初始流量为 0
- 在当前的剩余网络上求出从 S 到 T 的最短增广路,以每条弧的单位流量费用为边权。如果不存在,则算法结束
- 修改增广路上弧的流量,重复步骤 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;
}
网络流基础的讲解就到这里,希望大家能有所收获。