网络流初学整理
网络流顾名思义,是一个专门用来求网络流动问题的算法(也称最大流)
具体定义可参考百度百科
光动嘴皮子讲不明白,通过题来分析比较好。
洛谷最大网络流裸题
题目大意:
给出源点和汇点,求出从源点到汇点最大的网络流。这好像就是题目/误
显然这道题暴力dfs是肯定过不了的(那么直接切入主题)
EK算法:
(此算法相对较简单,但也需要一段时间的细心思考)
EK算法是网络流中最简单易懂,也是最慢的算法,复杂度为\(O(nm^2)\)
这题样例不太好讲,于是我自己编了一组
4 5 1 4
1 2 5
2 3 1
3 4 4
1 3 5
2 4 5
这个图建后就长这样。我们可以把每条边当作单向管道,1为源点,4为汇点,显然这个样例的输出为9(从1到2到4这条路可以疏通数量为5的水,从1到3到4疏通数量为4的水,因为如果在1到3中疏通为5的水是走不通3到4的,因为3到4的容量是4)。
对于网络流,最关键的一点就是要维护残量图。
顾名思义,残嘛,就是剩的意思,即剩下的量,我们把一条边的最大容量MAXV和其实际的流量F的差值叫做残量,即
残量\(=MAXV−F\)
然后我们将残量作为每一条边的权值,构建一个图就叫做残量图,若权值为0,那么就相当于一条断
残量图:(由于画图网站双向会重叠,所以yzh只能手绘)
有人会问:建一个边权(管道容量)为0的边有什么用吗,当然,这个反向边就是用来对于每条路进行分配管道的(注意:在题目没有说明管道流量的情况下,正向边为容量大小,反向为0,如果给出流量,那么正向为流量,反向为容量-流量),举个比较好懂的例子:
某zb市的cgl老师想要带领学生去打csp,然而发现这班车上已经有3个坐子被占用了,然后cgl老师就和这位老师商量了一下,让这位老师退票。
这样不太好讲,那么我们就通过代码分析
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<queue>
using namespace std;
typedef long long ll;
const int N = 1010, M = 20010, INF = 1e9;
struct edge {
int v, f, nxt;
};
edge e[M];
int head[N], d[N], pre[N];
bool st[N];
int cnt, n, m, s, t;
void add(int a, int b, int c) {
e[cnt].v = b; e[cnt].f = c; e[cnt].nxt = head[a]; head[a] = cnt++;
e[cnt].v = a; e[cnt].f = 0; e[cnt].nxt = head[b]; head[b] = cnt++;
}
bool bfs() {
memset(st, 0, sizeof st);
queue<int> q;
q.push(s); st[s] = 1; d[s] = INF;
while (q.size()) {
int tmp = q.front(); q.pop();
for (int i = head[tmp]; i != -1; i = e[i].nxt) {
int ver = e[i].v;
if (!st[ver] && e[i].f) {//判断是否符合扩增路径
st[ver] = 1;
d[ver] = min(d[tmp], e[i].f);
pre[ver] = i; //走过的前一条边
if (ver == t) return true;//走到汇点了
q.push(ver);
}
}
}
return false;
}
ll EK() {
ll res = 0;
while (bfs()) {//bfs判断是否有扩增路径
res += d[t];
for (int i = t; i != s; i = e[pre[i] ^ 1].v) {//处理正向与反向边的流量(下面说明)
//e[pre[i]^1].v反向边的v点就是当前点的前驱结点
e[pre[i]].f -= d[t];
e[pre[i] ^ 1].f += d[t];
}
}
return res;
}
int main() {
memset(head, -1, sizeof head);
cin >> n >> m >> s >> t;
while (m--) {
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
printf("%lld\n", EK());
return 0;
}
注意:代码中的i^1是求反向边的编号(可以自己证明)
这个建图刚刚已经讲过了,建好双向边中,我们需要bfs在图中找到扩增路径。
顾名思义,扩增就是如果有一路径从源点到汇点,且这条路径中任意一条边权值>0那么也就说当前状态还可以再扩充,只要找到有这中路径的存在这么一条扩增路径(用bfs判断),那么就将这条路径上走过的正向边的权值减去这条路径能够流通的流量(容量的最小值),再讲反向边的权值加上这个流量,将答案加上这个流量(答案最初状态是0),这样就可以得到最终答案。
(此代码相对较简单,但也需要一段时间的细心思考)
显然复杂度是\(O(nm^2)\),很多题都能卡死,那么我们开始学下一个复杂度为\(O(n^2m)\)算法dinic
dinic:
(此算法思路比较复杂,可能需要一段时间的细心思考)
老规矩,上题
洛谷P2756 飞行员配对方案问题
显然,这道题建图是一大难点,在题目没给出源点和汇点时,我们通常将0设为源点,n+1设为汇点(这道题目中是n+1,具体还是得看题目最大数据)
首先我们将可以配合的飞机连一条边,然后,将外籍飞机与源点连接,将英国飞机与汇点连接,然后如果两架飞机可以配合,那么就将两架飞机连接(这里边的容量设为1就可以)。
下面是建图部分:
int m, n, head[1010], tot=1, s, t, dep[1010], cur[1010];
bool vis[100010];
struct node
{
int v, nt, w;
}edge[100010];
void add(int u, int v, int w) {
edge[++tot] = { v,head[u],w };
head[u] = tot;
edge[++tot] = { u,head[v],0 };
head[v] = tot;
}
int main() {
memset(head, -1, sizeof(head));
scanf("%d%d", &m, &n);
s = 0;//源点
t = n + 1;//汇点
while (1) {
int u, v;
scanf("%d%d", &u, &v);
if (u == -1 && v == -1) break;
add(u, v, 0x3f3f3f3f);
}
for (int i = 1; i <= m; ++i) {
add(s, i, 1);
}
for (int i = m + 1; i <= n; ++i) {
add(i, t, 1);
}
dinic();
return 0;
}
建完图后的读者可以尝试用一下EK算法,提交会发现T掉,
那么我们就开始进入正题。
首先,判断有无扩增路径依旧是用bfs判断:
bool bfs() {
queue<int> q;
q.push(s);
memset(dep, -1, sizeof(dep));
dep[s] = 0;
cur[s] = head[s];
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = head[u]; i; i = edge[i].nt) {
int v = edge[i].v;
cur[v] = head[v];
if (dep[v] == -1 && edge[i].w) {
dep[v] = dep[u] + 1;//记录深度
if (v == t) return true;
q.push(v);
}
}
}
return false;
}
这里为了优化记录了深度,防止接下来的dfs倒着走而增加不必要的复杂度
然后就说dfs的操作(老规矩先粘代码):
int dfs(int u, int f) {
if (u == t) {//到达汇点直接返回
return f;
}
int dlt = 0;
for (int i = cur[u]; i; i = edge[i].nt) {
int v = edge[i].v;
cur[u] = i;
if (edge[i].w && dep[v] == dep[u] + 1) {
int dt = dfs(v, min(f - dlt, edge[i].w));
edge[i].w -= dt;
edge[i ^ 1].w += dt;
dlt += dt;
//if (dlt == f) return f;
}
}
if (!dlt) dep[u] = -1;
return dlt;
}
这个dfs中第一个参数是当前的节点,第二个f是记录目前为止的最大流量,dlt是记录已经用过的流量,传min(f - dlt, edge[i].w)的目的就是为了维护残量图,dt则是向v走的最大流量,然后dfs时候对权值进行加减操作即可,cur是一个类似于剪枝的东西(这个题中不明显)
dinic代码有点难懂,需要思考亿段时间
输出处理这里不做过多解释,看不懂的可以参考洛谷题解。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<functional>
#include<queue>
#pragma warning(disable:4996)
using namespace std;
int m, n, head[1010], tot=1, s, t, dep[1010], cur[1010];
bool vis[100010];
struct node
{
int v, nt, w;
}edge[100010];
void add(int u, int v, int w) {
edge[++tot] = { v,head[u],w };
head[u] = tot;
edge[++tot] = { u,head[v],0 };
head[v] = tot;
}
bool bfs() {
queue<int> q;
q.push(s);
memset(dep, -1, sizeof(dep));
dep[s] = 0;
cur[s] = head[s];
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = head[u]; i; i = edge[i].nt) {
int v = edge[i].v;
cur[v] = head[v];
if (dep[v] == -1 && edge[i].w) {
dep[v] = dep[u] + 1;
if (v == t) return true;
q.push(v);
}
}
}
return false;
}
int dfs(int u, int f) {
if (u == t) {
return f;
}
int dlt = 0;
for (int i = cur[u]; i; i = edge[i].nt) {
int v = edge[i].v;
cur[u] = i;
if (edge[i].w && dep[v] == dep[u] + 1) {
int dt = dfs(v, min(f - dlt, edge[i].w));
edge[i].w -= dt;
edge[i ^ 1].w += dt;
dlt += dt;
//if (dlt == f) return f;
}
}
if (!dlt) dep[u] = -1;
return dlt;
}
void dinic() {
int res = 0;
while (bfs()) {
res += dfs(s, 0x3f3f3f3f);
}
if (!res) {
cout << "No Solution!";
return;
}
printf("%d\n", res);
int cnt = 0;
for (int i = 2; i <= tot; i+=2) {
if (edge[i].v != s && edge[i ^ 1].v != s)
if (edge[i].v != t && edge[i ^ 1].v != t) {
if (edge[i ^ 1].w != 0) {
printf("%d %d\n", edge[i ^ 1].v, edge[i].v);
}
}
}
}
int main() {
memset(head, -1, sizeof(head));
scanf("%d%d", &m, &n);
s = 0;
t = n + 1;
while (1) {
int u, v;
scanf("%d%d", &u, &v);
if (u == -1 && v == -1) break;
add(u, v, 0x3f3f3f3f);
}
for (int i = 1; i <= m; ++i) {
add(s, i, 1);
}
for (int i = m + 1; i <= n; ++i) {
add(i, t, 1);
}
dinic();
return 0;
}
最小割
定义
割掉若干条边使得头尾不连通,问割掉边的权值最大值是多少?
结论
最小割=最大流,可以感性理解一下,详细证明自行搜索
费用流
思路
就是在网络流的基础上给每条边添加了一个 \(val\) 值,代表每流 \(1\) 单位的流量需要花费 \(val\) 个费用,问从头流到尾花费最小值,不难想到直接将找增广路的过程改为SPFA即可。