简析网络流
DanielDingDang · · 个人记录
简析网络流
引言
在万物互联的数字时代,无数信息流在光纤中奔涌,物流网络支撑着全球贸易的血脉,电力网络点亮现代文明的星空。这些看似迥异的系统背后,隐藏着一种精妙的数学模型——网络流。这个诞生于二十世纪中期的图论分支,早已超越数学的藩篱,成为解码复杂系统运行规律的金钥匙。从福特-富尔克森算法掀起的第一次算法革命,到如今支撑着云计算资源调度的智能决策,网络流理论不断突破时空界限,在数字世界的每个节点上演绎着最优化的奇迹。
本文中,笔者将会简述网络流及其应用,希望能给您带来一些帮助!
最大流
概念简析
我们先作如下概念和规定。
网络 是指一个有向图
接下来,网络
- 容量限制:对于每条边,流经该边的流量不得超过该边的容量,即
0 \leq f(u,v) \leq c(u,v) 。这个可以理解为:管道中流过的水量不能超过管道的容量。 - 流量守恒:除源汇点外,任意结点
u 的净流量为0 。其中,我们定义u 的净流量为f(u) = \sum_{x \in V} f(u, x) - \sum_{x \in V} f(x, u) 。这个可以理解为:对于每一个连接管道的节点,流入该点的总水量等于流出该点的总水量。
显然,源节点发出的流量总和等于汇节点接受的流量总和。
上述概念不需要记住具体准确的定义,只需要大致了解含义即可。
算法简注
终于,我们可以定义最大流的问题了。令
我们来通过一个例子感受下:(以下所有图都用邻接矩阵的方式呈现,同学们可以自己画在纸上理解一下,空的格子表示没有边)
存在
故流量总计
那我们如何解决这个问题?接下来介绍的思想叫做 Ford-Fulkerson 思想。
注意到,在寻找增广路的时候,在前面找到的不一定是一个最优解,如果我们建一个流量相反的边,那么就代表可以从这条反向边流过而实现反悔。
我们对于原图中的每一条边
还是以刚才的图为例(整个解析过程中都是如此),建立这样的反向边。
接下来,我们每次从源点
找到一条这样的路径之后,我们令
我们不断寻找增广路,并进行上述过程,就可以得到一个可以证明正确性的网络流算法。
接下来,我们用上面的例子演示这个过程。一开始,我们的邻接矩阵如下:
我们先找到一条路径
接下来找到一条路径
这个时候,满足条件的增广路只有一条了,
容易发现,从
当我们考虑 FF 增广的具体实现时,最自然的方案就是使用 BFS。此时,FF 增广表现为 Edmonds-Karp 算法。换句话说,我们用 BFS,每次找一条增广路就可以了。
那么这个算法的复杂度是多少呢?显然,单轮 BFS 增广的时间复杂度是
bool bfs()
{
int hh = 0, tt = 0;
memset(st, false, sizeof st);
q[0] = S, st[S] = true, d[S] = INF;
while (hh <= tt)
{
int u = q[hh ++ ];
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!st[j] && f[i])
{
st[j] = true;
d[j] = min(d[u], f[i]);
pre[j] = i;
if (j == T) return true;
q[ ++ tt] = j;
}
}
}
return false;
}
int EK()
{
int r = 0;
while (bfs())
{
r += d[T];
for (int i = T; i != S; i = e[pre[i] ^ 1])
f[pre[i]] -= d[T], f[pre[i] ^ 1] += d[T];
}
return r;
}
不难发现,整个过程中,复杂度完全取决于我们寻找增广路的效率。接下来,我们逐步优化算法,争取能一次找到多条增广路。我们令,原图加上权值为
考虑在增广前先对
分层图建出来之后,有了每个点的层数编号,对任意点
这里有一个实用的技巧:多路增广。具体来说,如果我们在层次图上找到了一条从
但是直接这么做复杂度还是有点问题,这个时候我们考虑引入当前弧优化。
注意到在
这样以来,我们 Dinic 算法的时间复杂度为
void add(int a, int b, int c)
{
e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
}
bool bfs()
{
int hh = 0, tt = 0;
memset(d, -1, sizeof d);
q[0] = S, d[S] = 0, cur[S] = h[S];
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int ver = e[i];
if (d[ver] == -1 && f[i])
{
d[ver] = d[t] + 1;
cur[ver] = h[ver];
if (ver == T) return true;
q[ ++ tt] = ver;
}
}
}
return false;
}
int find(int u, int limit)
{
if (u == T) return limit;
int flow = 0;
for (int i = cur[u]; ~i && flow < limit; i = ne[i])
{
cur[u] = i; // 当前弧优化
int ver = e[i];
if (d[ver] == d[u] + 1 && f[i])
{
int t = find(ver, min(f[i], limit - flow));
if (!t) d[ver] = -1;
f[i] -= t, f[i ^ 1] += t, flow += t;
}
}
return flow;
}
int dinic()
{
int r = 0, flow;
while (bfs()) while (flow = find(S, INF)) r += flow;
return r;
}
算法应用
匹配问题
使用最大流算法,我们可以求出一个二分图的最大匹配。
给定一个二分图,其左部点的个数为
左部点从
我们可以先给出一个源点
这一点如何证明呢?我们只要证明:对于我们构造出的网络,上面的任意一条可行流都和原来的二分图匹配一一对应。
当然,我们也可以规定一个点可以和
习题 P2756 飞行员配对方案问题
const int N = 110, M = 5210, INF = 1e18;
int m, n, S, T;
int h[N], e[M], f[M], ne[M], idx;
int q[N], d[N], cur[N];
void add(int a, int b, int c)
{
e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
}
int dinic(); // dinic 模板省略
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> m >> n;
S = 0, T = n + 1;
memset(h, -1, sizeof h);
for (int i = 1; i <= m; i ++ ) add(S, i, 1);
for (int i = m + 1; i <= n; i ++ ) add(i, T, 1);
int a, b;
while (cin >> a >> b && a != -1) add(a, b, 1);
cout << dinic() << "\n";
for (int i = 0; i < idx; i += 2)
if (e[i] > m && e[i] <= n && !f[i])
cout << e[i ^ 1] << " " << e[i] << "\n";
return 0;
}
习题 P3254 圆桌问题
#define int long long
const int N = 430, M = (150 * 270 + N) * 2, INF = 1e18;
int m, n, S, T;
int h[N], e[M], ne[M], f[M], idx;
int q[N], d[N], cur[N];
void add(int a, int b, int c);
int dinic();
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> m >> n;
S = 0, T = m + n + 1;
memset(h, -1, sizeof h);
int tot = 0;
for (int i = 1, c; i <= m; i ++ )
{
cin >> c;
add(S, i, c);
tot += c;
}
for (int i = 1, c; i <= n; i ++ )
{
cin >> c;
add(m + i, T, c);
}
for (int i = 1; i <= m; i ++ )
for (int j = 1; j <= n; j ++ )
add(i, m + j, 1);
if (dinic() != tot) cout << "0\n";
else
{
cout << "1\n";
for (int i = 1; i <= m; i ++ )
{
for (int j = h[i]; ~j; j = ne[j])
if (e[j] > m && e[j] <= m + n && !f[j])
cout << e[j] - m << " ";
cout << "\n";
}
}
return 0;
}
习题 P2763 试题库问题
const int N = 2010, M = 44000, INF = 0x3f3f3f3f;
// 模板略去
signed main()
{
cin >> m >> n, S = 0, T = n + m + 1;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i ++ ) add(S, i, 1);
for (int i = 1, w; i <= m; i ++ )
cin >> w, k += w, add(i + n, T, w);
for (int p, id, i = 1; i <= n; i ++ )
{
cin >> p;
for (int j = 1; j <= p; j ++ )
cin >> id, add(i, id + n, 1);
}
int res = dinic();
if (res != k) puts("No Solution");
else
{
for (int id = 1; id <= m; id ++ )
{
cout << id << ":";
for (int i = h[id + n]; ~i; i = ne[i])
{
int j = e[i];
if (f[i] && j <= n) cout << " " << j;
}
cout << "\n";
}
}
return 0;
}
相信通过上述习题,大家已经对最大流解决二分图匹配的相关问题非常清晰了。
最小路径覆盖
最大流还有一个应用,可以用来解决 DAG 上的最小路径覆盖问题。请读者分析如下例题,先稍作思考,若无思路再阅读后文中的题解。
例题 P2764 最小路径覆盖问题
给定有向图
G=(V,E) 。设P 是G 的一个简单路(顶点不相交)的集合。如果V 中每个定点恰好在P 的一条路上,则称P 是G 的一个路径覆盖。P 中路径可以从V 的任何一个定点开始,长度也是任意的,特别地,可以为0 。G 的最小路径覆盖是G 所含路径条数最少的路径覆盖。设计一个有效算法求一个 DAG(有向无环图)G 的最小路径覆盖。
这题我们可以使用调整法。我们假设,一开始,我们用
我们可以使用 拆点 的方法处理。
将每个点拆开,拆成
具体证明过程:我们思考一次增广代表着什么。增广的时候,相当于找到一条从左部点
那么,方案可以如何输出呢?如何通过残量网络暴力构造?
首先我们需要找出所有的起点,什么样的点会是起点?如果对于右部点
确定了路径起点,我们就来解决对于某个特定起点
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2010, M = 44000, INF = 0x3f3f3f3f;
int m, n, S, T, k;
int h[N], e[M], f[M], ne[M], idx = 1;
int q[N], d[N], cur[N], ed[N];
bool st[N];
void add(int a, int b, int c)
{
e[ ++ idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx;
e[ ++ idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx;
}
// 模板略去
void write_path(int u)
{
cout << u << ' ';
st[u] = 1;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j > n && j <= 2 * n && !f[i] && !st[j - n])
write_path(j - n);
}
}
signed main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 1, a, b; i <= m; i ++ )
cin >> a >> b, add(a, b + n, 1);
S = 2 * n + 1, T = 2 * n + 2;
for (int i = 1; i <= n; i ++ )
add(S, i, 1), add(i + n, T, 1), ed[i + n] = idx - 1;
int res = n - dinic();
for (int i = n + 1; i <= 2 * n; i ++ )
if (f[ed[i]] == 1) write_path(i - n), cout << "\n";
cout << res << "\n";
return 0;
}
例题 P2765 魔术球问题
假设有
n 根柱子,现要按下述规则在这n 根柱子中依次放入编号为1,2,3,\cdots 的球:
每次只能在某根柱子的最上面放球。
同一根柱子中,任何
2 个相邻球的编号之和为完全平方数。试设计一个算法,计算出在
n 根柱子上最多能放多少个球。例如,在4 根柱子上最多可放11 个球。对于给定的
n ,计算在n 根柱子上最多能放多少个球。1\le n\le 55 .
考虑,如果已经知道要放
现在问:
如果数据范围变大,可以二分答案优化。但是此处直接暴力可过。
因为笔者在写代码的时候把 dinic 函数作了极小的修改,所以在此给出完整的代码。
#define int long long
const int N = 40010, MM = 1000010, INF = 0x3f3f3f3f, M = 20000;
int m, n, S = 2 * M + 1, T = 2 * M + 2, nodes, r, thres = -1;
int h[N], e[MM], f[MM], ne[MM], idx;
int q[N], d[N], cur[N], ed[N];
bool st[N];
bool check(int x)
{
int t = sqrt(x);
if (t * t != x && (t + 1) * (t + 1) != x) return 0;
return 1;
}
void add(int a, int b, int c)
{
e[ ++ idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx;
e[ ++ idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx;
}
bool bfs()
{
int hh = 0, tt = 0;
memset(d, -1, sizeof d);
q[0] = S, d[S] = 0, cur[S] = h[S];
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int ver = e[i];
if (d[ver] == -1 && f[i])
{
d[ver] = d[t] + 1;
cur[ver] = h[ver];
if (ver == T) return true;
q[ ++ tt] = ver;
}
}
}
return false;
}
int find(int u, int limit)
{
if (u == T) return limit;
int flow = 0;
for (int i = cur[u]; ~i && flow < limit; i = ne[i])
{
cur[u] = i; // 当前弧优化
int ver = e[i];
if (d[ver] == d[u] + 1 && f[i])
{
int t = find(ver, min(f[i], limit - flow));
if (!t) d[ver] = -1;
f[i] -= t, f[i ^ 1] += t, flow += t;
}
}
return flow;
}
int dinic()
{
int flow;
while (bfs()) while (flow = find(S, INF)) r += flow;
return r;
}
void write_path(int u)
{
cout << u << ' ';
st[u] = 1;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j > M && j <= 2 * M && !f[i] && !st[j - M])
write_path(j - M);
}
}
bool add(int i)
{
add(S, i, 1), add(i + M, T, 1), ed[i + M] = idx - 1;
for (int j = 1; j < i; j ++ )
if (check(i + j)) add(j, i + M, 1);
return i - dinic() <= n;
}
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n;
memset(h, -1, sizeof h); idx = 1; memset(ed, -1, sizeof ed);
for (int i = 1; ; i ++ ) if (add(i)); else { thres = i - 1; break; }
memset(h, -1, sizeof h); idx = 1; r = 0; memset(cur, -1, sizeof cur);
for (int i = 1; i <= thres; i ++ ) add(i);
cout << thres << "\n";
for (int i = M + 1; i <= M + thres; i ++ )
if (ed[i] != -1 && f[ed[i]] == 1) write_path(i - M), cout << "\n";
return 0;
}
上下界可行流
上下界网络流本质是给流量网络的每一条边设置了流量上界
根据题目要求,我们可以使用上下界网络流解决不同问题。
我们先介绍无源汇上下界可行流。
给定无源汇流量网络
不妨假设每条边已经流了
由于最大流需要满足初始流量平衡条件(最大流可以看成是下界为
若
若
若
如果附加边满流,说明这一个点的流量平衡条件可以满足,否则这个点的流量平衡条件不满足。(因为原图加上附加流之后才会满足原图中的流量平衡。)
在建图完毕之后跑
习题 Acwing 2188 无源汇上下界可行流
#define int long long
const int N = 210, M = 21111, INF = 1e18;
int n, m, S, T;
int h[N], e[M], f[M], ne[M], l[M], idx;
int q[N], d[N], cur[N], A[N];
void add(int a, int b, int c, int d);
int dinic();
signed main()
{
ios::sync_with_stdio(0);cin.tie(0); cout.tie(0);
cin >> n >> m;
S = 0, T = n + 1;
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ )
{
int a, b, c, d;
cin >> a >> b >> c >> d;
add(a, b, c, d);
A[a] -= c, A[b] += c;
}
int tot = 0;
for (int i = 1; i <= n; i ++ )
if (A[i] > 0) add(S, i, 0, A[i]), tot += A[i];
else if (A[i] < 0) add(i, T, 0, -A[i]);
if (dinic() != tot) cout << "NO\n";
else
{
cout << "YES\n";
for (int i = 0; i < m * 2; i += 2)
cout << f[i ^ 1] + l[i] << "\n";
}
return 0;
}
接下来考虑有源汇上下界可行流相关问题。原题如下:给定有源汇流量网络
对于这题,假设源点是
显然,若有解,则
对上述问题加强成有源汇上下界最大流:给定有源汇流量网络
我们找到网络上的任意一个可行流。
- 如果找不到解就可以直接结束;
- 如果有解,我们考虑删去所有附加边之后的残量网络并且在网络上进行调整。具体来说,我们在残量网络上再跑一次
S 到T 的最大流,将可行流流量和最大流流量相加即为答案。
习题 Acwing 2189 有源汇上下界最大流
#define int long long
const int N = 210, M = 25000, INF = 1e18;
int h[N], e[M], f[M], ne[M], idx, A[N];
int q[N], d[N], cur[N];
int n, m, S, T, s, t;
void add(int a, int b, int c);
int dinic();
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
memset(h, -1, sizeof h);
cin >> n >> m >> s >> t;
S = 0, T = n + 1;
while (m -- )
{
int a, b, c, d;
cin >> a >> b >> c >> d;
add(a, b, d - c);
A[a] -= c, A[b] += c;
}
int tot = 0;
for (int i = 1; i <= n; i ++ )
if (A[i] > 0) add(S, i, A[i]), tot += A[i];
else if (A[i] < 0) add(i, T, -A[i]);
add(t, s, INF);
if (dinic() < tot) cout << "No Solution\n";
else
{
int res = f[idx - 1];
S = s, T = t;
f[idx - 1] = f[idx - 2] = 0;
cout << dinic() + res << "\n";
}
return 0;
}
有源汇上下界最小流的做法和最大流类似。我们考虑将残量网络中不需要的流退掉,找到网络上的任意一个可行流。
- 如果找不到解就可以直接结束。
- 否则我们考虑删去所有附加边之后的残量网络,我们在残量网络上再跑一次
T 到S 的最大流,将可行流流量减去最大流流量即为答案。
习题 Acwing 2190 有源汇上下界最小流
// 模板略去
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
memset(h, -1, sizeof h);
cin >> n >> m >> s >> t;
S = 0, T = n + 1;
while (m -- )
{
int a, b, c, d;
cin >> a >> b >> c >> d;
add(a, b, d - c);
A[a] -= c, A[b] += c;
}
int tot = 0;
for (int i = 1; i <= n; i ++ )
if (A[i] > 0) add(S, i, A[i]), tot += A[i];
else if (A[i] < 0) add(i, T, -A[i]);
add(t, s, INF);
if (dinic() < tot) cout << "No Solution\n";
else
{
int res = f[idx - 1];
S = t, T = s;
f[idx - 1] = f[idx - 2] = 0;
cout << res - dinic() << "\n";
}
return 0;
}
其他应用
本节中将会讲解更多的最大流相关例题,以帮助读者理解最大流在算法竞赛中的应用。
例题 P2766 最长不下降子序列问题
给定正整数序列
x_1, \ldots, x_n 。
- 计算其最长不下降子序列的长度
s 。- 如果每个元素只允许使用一次,计算从给定的序列中最多可取出多少个长度为
s 的不下降子序列。- 如果允许在取出的序列中多次使用
x_1 和x_n (其他元素仍然只允许使用一次),则从给定序列中最多可取出多少个不同的长度为s 的不下降子序列。令
a_1, a_2, \ldots, a_s 为构造S 时所使用的下标,b_1, b_2, \ldots, b_s 为构造T 时所使用的下标。且\forall i \in [1,s-1] ,都有a_i \lt a_{i+1} ,b_i \lt b_{i+1} 。则S 和T 不同,当且仅当\exists i \in [1,s] ,使得a_i \neq b_i 。
第
第
这个时候,有很多同学可以想到:我们不妨以 DP 过程中的转移为参照连边 . 一个数可以作为起点,当且仅当
- 建立虚拟源点和汇点
S,T 。 - 对于所有
f_i=1 的点i ,连一条S\rightarrow i 的边,容量为1 。 - 对于所有
f_i=k 的点i ,连一条i\rightarrow T 的边,容量为1 。 - 对于每对
i,j(i>j) ,当a_i\ge a_j 且f_i=f_j+1 时,即j 是i 的最佳转移点(之一)时,从j\rightarrow i 连一条容量为1 的边。则S\rightarrow T 的最大流即为答案。
这看上去很有道理,但是存在一个非常严重的问题:一个点的出流可能不止是
我们举一个例子:当
如果你理解了上述内容,就很容易发现问题所在:我们没有办法通过改变容量限制直接增加对点的流量限制。什么意思呢?比如说在刚刚我们举的例子里面,尽管所有的边容量都是
讲到这里,正解也就呼之欲出了:我们考虑拆点!相当于,我们把每一个点在网络中用一条边来表示,这样我们就可以通过修改这一条边的容量来表示这一个点的流量限制了。
我们先在上述例子中演示一下,再给出形式化的描述。假设点
那么因为每一个点最多被选择一次,也就是流量最多是
关键是转移的边,我们要规定一下边的方向顺序。因为刚刚我们是把
现在我们已经分析出了建图的方式了,我们汇总一下,得出一个对建图的形式化表达:
- 从
S 向每个f_i=1 的i_x 点建边,容量为1 ; - 从每个
i_x 向i_y 建边,容量为1 ; - 对于每对
i, j(i>j) ,当a_i\ge a_j 且f_i=f_j+1 时,从j_y 向i_x 建边,容量为1 ; - 从
f_i=k 的i_y 向汇点建边,容量为1 .
终于,我们分析完了第
如果只改这两条边显然是有问题的,因为
所以第
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 10010, M = 2 * N, INF = 1e12;
int n, m, S, T, a[N], dp[N];
// 模板略过
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i ++ ) cin >> a[i];
int k = 0;
for (int i = 1; i <= n; i ++ )
{
dp[i] = 1;
for (int j = 1; j < i; j ++ )
if (a[j] <= a[i]) dp[i] = max(dp[i], dp[j] + 1);
k = max(k, dp[i]);
}
cout << k << "\n";
if (k == 1)
{
cout << n << "\n" << n;
return 0;
}
memset(h, -1, sizeof h);
S = 0, T = 2 * n + 1;
for (int i = 1; i <= n; i ++ )
if (dp[i] == 1) add(S, i, 1);
for (int i = 1; i <= n; i ++ )
if (dp[i] == k) add(i + n, T, 1);
for (int i = 1; i <= n; i ++ ) add(i, i + n, 1);
for (int j = 1; j <= n; j ++ )
for (int i = j + 1; i <= n; i ++ )
if (a[i] >= a[j] && dp[i] == dp[j] + 1) add(j + n, i, 1);
cout << dinic() << "\n";
memset(h, -1, sizeof h), idx = 0;
for (int i = 1; i <= n; i ++ )
if (dp[i] == 1)
{
if (i == 1) add(S, i, INF);
else add(S, i, 1);
}
for (int i = 1; i <= n; i ++ )
if (dp[i] == k)
{
if (i == n) add(i + n, T, INF);
else add(i + n, T, 1);
}
add(1, 1 + n, INF), add(n, 2 * n, INF);
for (int i = 2; i < n; i ++ ) add(i, i + n, 1);
for (int j = 1; j <= n; j ++ )
for (int i = j + 1; i <= n; i ++ )
if (a[i] >= a[j] && dp[i] == dp[j] + 1) add(j + n, i, 1);
cout << dinic() << "\n";
return 0;
}
例题 有
考虑求出一个
进一步地,同一类的球只需要一个,所以可以将每个盒子放入
例题 AGC037D
注意到初始
考虑
考虑
左边
因为
因为
我们现在倒回来看看连完后的图代表着什么,例如一条路径:
一组(
现在我们只需要知道如何给路径分组就能通过代入以上两个结论确定
那么如何分组呢?我们发现如果给原图加上源点和汇点,那么一组恰好对应一个完美匹配。于是跑
注意统计答案的时候有多种写法,最简单的方法是枚举
[!TIP]
写的时候要注意以下两点:第一,在统计答案的时候,已经遍历过的点不能重复统计;第二,每一轮最大流结束之后,一定要彻底删除所有满流的边、清空反向边的容量,并恢复残留网络上源点、汇点所有出边
/ 入边的容量为1 。这是因为,源点和汇点本身起到的作用就是约束 路径的分组过程,所以要把容量改回1 ,但是其他满流的边的目的本身就是 在路径的分组中作为路径的一部分,所以不能重复利用。在删除所有满流的边时,可以用这样的方式:注意到原网络中的边如果满流的话,容量就一定为
0 了,不需要进行任何处理就已经「自动清零」了。所以这个时候,只要清空所有反向边的容量,不仅可以规避掉 增广路径经过这一轮没有容量但是上一轮容量没有清空 的情况,而且还可以 彻底删除所有满流的边以及他的反向边。在下面提供的参考程序中,v 用于存储原图中所有反向边的编号。
参考程序中,
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int K = 510, N = 1000010, M = 2 * N, INF = 1e18;
int n, m, S, T;
int a[K][K], resa[K][K], resb[K][K];
int h[N], e[M], f[M], ne[M], idx;
int q[N], d[N], cur[N];
bool st[M];
vector<int> v;
void add(int a, int b, int c)
{
e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
v.push_back(idx);
e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
}
// 模板略去
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ ) cin >> a[i][j];
S = 0, T = n * m + 2 * n + 1;
for (int i = 1; i <= n; i ++ ) add(S, i, 1);
for (int i = n * m + n + 1; i <= n * m + 2 * n; i ++ ) add(i, T, 1);
int topidx = idx - 1;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
add(i, a[i][j] + n, 1), add(a[i][j] + n, n * m + n + (a[i][j] - 1) / m + 1, 1);
for (int i = 1; i <= m; i ++ )
{
for (int j = 0; j <= topidx; j += 2) f[j] = 1, f[j ^ 1] = 0;
dinic();
for (int u = 1; u <= n; u ++ )
for (int j = h[u]; ~j; j = ne[j])
if (!st[j] && f[j] == 0 && e[j] != S) st[j] = 1, resa[u][i] = e[j] - n;
for (auto j : v) f[j] = 0;
}
for (int i = 1; i <= n; i ++ , cout << "\n")
for (int j = 1; j <= m; j ++ ) cout << resa[i][j] << " ";
for (int L = 1; L <= m; L ++ )
for (int i = 1; i <= n; i ++ )
for (int j = 1; j < n; j ++ )
if (resa[j][L] > resa[j + 1][L]) swap(resa[j][L], resa[j + 1][L]);
for (int i = 1; i <= n; i ++ , cout << "\n")
for (int j = 1; j <= m; j ++ ) cout << resa[i][j] << " ";
return 0;
}
例题 CF2038H
第
- 若
p_{i+1}<p_i ,可知\text{val}_{i+1}=\text{val}_i 。 - 若
p_{i+1}=p_i ,可知\text{val}_{i+1}=\text{val}_i 。 - 若
p_{i+1}>p_i ,可知\text{val}_{i+1}=\text{val}_i+1 。
所以执政党票数知道了,其它党就是编号
特别地,党执政日的限制是
最大流需要是
Hall 定理:设二分图的左部点为集合
X ,右部点为集合Y, X 存在匹配当且仅当X 的任意子集S 都有它的邻节点个数N(S) \geq|S| 。特别地,若|X|=|Y| 则存在完美匹配。
例题 Gym105833F
设答案
基于此我们构造排列。
第
结论(正则二分图匹配):
问题 如何输出最小割的任意一种方案?
答案 我们可以通过从源点
问题 如何最小化割边数量?
答案 先求出最小割,把没有满流的边容量改成
算法应用
现在,我们已经可以求出一个网络的最小割了。那么这个算法在建模上面有哪些应用呢?
在思考经典运用之前,我们先看几个基础的思考题热身。
思考题 给定一张无向图,割
解答 对于割点问题,我们将一个点拆成两个。对于入边连接第一个点,对于出边连接第二个点,再加入一条第一个点到第二个点的带权有向边,权值为
思考题 有
解答 易证:奇数和奇数的平方和一定不是完全平方数,偶数和偶数的
二者选其一(集合划分)
例题 一道经典题
有
n 个物品和两个集合A,B ,如果一个物品没有放入A 集合会花费a_i ,没有放入B 集合会花费b_i ;还有若干个形如u_i,v_i,w_i 限制条件,表示如果u_i 和v_i 同时不在一个集合会花费w_i 。每个物品必须且只能属于一个集合,求最小的代价。
这是一个经典的 二者选其一 的最小割题目。我们对于每个集合设置源点
注意到当源点和汇点不相连时,代表这些点都选择了其中一个集合。如果将连向
最小割就是最小花费。
此处有一个拓展:如果
例题 SP839
给你一个无向图
G(V,E) 。 每个顶点都有一个[0,2^{31}-1] 范围内的整数的标记\text{mark}_i 。不同的顶点可能有相同的标记。对于边u,v ,定义\text{cost}(u, v)=\text{mark}_u \oplus \text{mark}_v ,其中\oplus 表示按位异或运算。现在有
k 个点已经有\text{mark} 了,你需要确定其他n-k 个点的\text{mark} ,以使\displaystyle \sum_{(u,v)\in E}\text{cost}(u,v) 尽可能小。如果有多解,请输出\displaystyle\sum_{i=1}^n\text{mark}_i 最小的方案。如果仍有多解,输出任意一个。多测,
1 \leq T \leq 10,0<n \leq 500,0 \leq m \leq 3000,0 \leq \operatorname{mark}_i<2^{31} 。
拆位,问题转化成每个点的
注意到这张图可以被分为两个点集,对于一条边的两个端点,如果他们的值相同则没有贡献,否则对答案有
建图方式如下:首先对题目中给出的所有边建立无向边,容量都为
接下来对这张图求最小割,割边显然都是原边。求得的最小割即为当前二进制位的最优解。考虑如何计算
#define int long long
const int N = 10010, M = 20010;
const int INF = 1e12;
int n, m, S, T, k, nd[N], mark[N];
int u[N], v[N];
int h[N], e[M], f[M], ne[M], idx, res[N];
int q[N], d[N], cur[N];
bool st[N];
void add();
int dinic();
void dfs(int u, int add)
{
st[u] = 1, res[u] += add;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (f[i] && !st[j]) dfs(j, add);
}
}
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int TT;
cin >> TT;
while (TT -- )
{
cin >> n >> m;
for (int i = 1; i <= m; i ++ ) cin >> u[i] >> v[i];
memset(res, 0, sizeof res);
cin >> k;
for (int i = 1; i <= k; i ++ ) cin >> nd[i] >> mark[i];
S = 0, T = n + 1;
for (int wei = 0; wei < 31; wei ++ )
{
idx = 0;
memset(h, -1, sizeof h), memset(st, 0, sizeof st);
for (int i = 1; i <= m; i ++ )
add(u[i], v[i], 1), add(v[i], u[i], 1);
for (int i = 1; i <= k; i ++ )
if (mark[i] >> wei & 1) add(S, nd[i], INF);
else add(nd[i], T, INF);
dinic();
dfs(S, 1 << wei);
}
for (int i = 1; i <= n; i ++ ) cout << res[i] << "\n";
}
return 0;
}
思考变式 P1361 小 M 的作物
形式化题意:有
n 个物品,划分到两个集合A, B 中,第i 个物品划分到集合A 收益a_i ,划分到集合B 收益b_{i 。} 有m 个组合,每个组合有c_j 个人,若组合中的全部划分在A 中,收益w_{a _j} ,若全部划分在B 中,收益w_{b _j} 。求最大收益。
对于最大收益,我们可以考虑转化一下问题再用最小割。然后用总边权减去最小割,得到的就是最大收益。具体来说,第
对于组合问题,一种很常见的思路就是将这个组合打包到一起(建立虚点)。具体来说建图方法是,在上一问建图的基础上,对每个组合增加两个虚点,两个虚点分别与
下图表示:若
最大权闭合子图
在许多实际应用中,给出的有向图常常是一个有向无环图(DAG),闭合图的性质恰好反映了事件间的必要条件的关系:一个事件的发生,它所需要的所有前提也都要发生。一个常见的例子就是制定大学的选课计划,其中一些课程需要以另一些课程为基础,这就是给出了有向无环图。若给所有课程打分,最大权闭合图对应了获益最大或效率最高的选课计划,解决了这个问题,就可以容易地选出一个最合理的选课计划。
在另外一些实际应用中,给出的是一个一般有向图,即有可能出现圈。常见的例子就是工程分期,一个工程可能含有很多项目,项目之间也有依赖关系。有些项目是需要同期一起开发,互相依赖的(即出现圈)。这样,也可以利用最大权闭合图,将最先应该完成的项目选出来作为工程的第一期。
最大权值闭合图,即给定一张有向图,每个点都有一个权值(可以为正或负或
做法:建立超级源点
直接看这个结论可能无法理解这个算法的本质,接下来我们考虑换一个角度理解。
如果我们选了
例题 P2762 太空飞行计划问题
W 教授正在为国家航天中心计划一系列的太空飞行。每次太空飞行可进行一系列商业性实验而获取利润。现已确定了一个可供选择的实验集合
E = \{ E_1, E_2, \cdots, E_m \} ,和进行这些实验需要使用的全部仪器的集合I = \{ I_1, I_2, \cdots, I_n \} 。实验E_j 需要用到的仪器是I 的子集R_j \subseteq I 。配置仪器
I_k 的费用为c_k 美元。实验E_j 的赞助商已同意为该实验结果支付p_j 美元。W 教授的任务是找出一个有效算法,确定在一次太空飞行中要进行哪些实验并因此而配置哪些仪器才能使太空飞行的净收益最大。这里净收益是指进行实验所获得的全部收入与配置仪器的全部费用的差额。对于给定的实验和仪器配置情况,编程找出净收益最大的试验计划。
使用上述模型解题:对于每个实验,连一条从
按照上面建图,求最小割即最大流,然后用实验利益总和减去最大流即为最大净收益。
#define int long long
const int N = 30010, M = 2 * N, INF = 1e18;
// 模板略
signed main()
{
scanf("%lld%lld", &m, &n);
memset(h, -1, sizeof h);
S = 0, T = n + m + 1;
int res = 0;
for (int i = 1, x; i <= m; i ++ ) // 注意本题读入方式
{
char c; scanf("%lld", &x);
res += x;
add(S, n + i, x);
while (1)
{
scanf("%lld%c", &x, &c);
add(n + i, x, INF);
if (c == '\n' || c == '\r') break;
}
}
for (int i = 1, w; i <= n; i ++ ) scanf("%lld", &w), add(i, T, w);
res -= dinic();
for (int i = 1; i <= m; i ++ ) if (d[i + n] > 0) printf("%lld ", i);
printf("\n");
for (int i = 1; i <= n; i ++ ) if (d[i] > 0) printf("%lld ", i);
printf("\n%lld\n", res);
return 0;
}
离散变量取值问题
感谢 大佬的博客,在本节中很有参考价值。
这是最小割中非常经典的一个模型,也叫做 广义切糕模型。
有若干个变量,每个变量取值为
先不考虑限制,考虑用最小割刻画这个问题。
如图所示,对每一个变量都建立
- 从
S 到X_1 建立一条边权为+\infty 的边; - 从
X_{n+1} 到T 建立一条边权为+\infty 的边; - 从
X_i 到X_{i+1} 建立一条边权为v_i 的边; - 从
X_{i+1} 到X_i 建立一条边权为+\infty 的边。
这保证了,每个变量所构造出的一条链上,在最小割中 有且仅有 一条边被割掉,被割掉的边的边权就代表了我们为这个变量的取值。
为什么有且仅有一条边被割掉?
\textbf{Proof.}$ **$\textcolor{red}{\textbf{Part~1.}}$** **证明存在性**:如果没有边被割,那么显然存在 $S \rightarrow T$ 的路径,矛盾。如果不加反向边存在一个割一条边的割,加上反向边依然存在这个割。**$\textcolor{blue}{\textbf{Part~2.}}$ 证明必要性**:假设存在两条边被割,即 $X_i \rightarrow X_{i+1}$ 和 $X_j \rightarrow X_{j+1}(i<j)$ 。我们知道,对于一条最小割中的割边 $(u, v)$ ,在被割完的图中一定存在 $S \rightarrow u, v \rightarrow T$ 的路径 (否则这条边不割也行)。那么我们可以得出存在路径 $S \rightarrow X_{j+1}, X_{i+1} \rightarrow T$ 。因为无穷的边不会被割掉,所以还存在路径 $X_{j+1} \rightarrow X_{i+1}$ ,所以存在 $S \rightarrow T$ 的路径,矛盾!$\square
知道了这样的基本模型,我们来看例题。
例题 P6054 开门大吉
我们转化到刚刚的模型上,这题的变量就是
- 有
n 个变量,每个变量X 有m 个取值,设变量X 选取的取值编号为f[X] \in[1, m] 。 - 有
y 条限制,形如对于两个变量X, Y ,有f[X] \geq f[Y]+k 。(k 不一定是正数) - 所有变量和最小。
我们考虑如何处理这个限制,事实上,如果要让
下证算法的正确性。
为了方便读者理解,下举有
若
这满足我们的限制。这样就可以更加直观地理解到其正确性。
由于浮点数的 Dinic 算法的实现和整数略有不同,所以这里给出完整的代码。
注意 这里代码中有一个细节,注意这里的重要优化:当总流量已经超过设的阈值
#define int long long
const int N = 50010, M = 200010;
const double INF = 1e15;
int n, m, S, T, p, y, c[N];
int h[N], e[M], ne[M], idx;
double f[M];
int q[N], d[N], cur[N];
void add(int a, int b, double c) // 添加一条边a->b,容量为c;同时添加一条反向边
{
e[idx] = b; f[idx] = c; ne[idx] = h[a]; h[a] = idx ++ ;
e[idx] = a; f[idx] = 0; ne[idx] = h[b]; h[b] = idx ++ ;
}
bool bfs() // 创建分层图
{
int hh = 0, tt = 0;
memset(d, -1, sizeof d);
q[0] = S; d[S] = 0; cur[S] = h[S];
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int ver = e[i];
if (d[ver] == -1 && f[i] > 0)
{
d[ver] = d[t] + 1;
cur[ver] = h[ver];
if (ver == T) return true;
q[ ++ tt] = ver;
}
}
}
return false;
}
double find(int u, double limit) // 在残留网络中增广
{
if (u == T || limit < 1e-10) return limit;
double flow = 0;
for (int i = cur[u]; ~i && flow < limit; i = ne[i])
{
cur[u] = i; // 当前弧优化
int ver = e[i];
if (d[ver] == d[u] + 1 && f[i] > 0)
{
double t = find(ver, min(f[i], limit - flow));
if (!t) d[ver] = -1;
f[i] -= t; f[i ^ 1] += t; flow += t;
}
}
return flow;
}
double dinic()
{
double r = 0, flow = 0;
while (bfs() && r < INF) // 注意这里的重要优化:当流已经超过 INF 就说明答案肯定是 -1,直接退出即可 .
while (flow = find(S, INF)) r += flow;
return r;
}
int get(int i, int j)
{
return (i - 1) * (m + 1) + j + 1;
}
void solve()
{
cin >> n >> m >> p >> y;
idx = 0;
memset(h, -1, sizeof h);
S = 0, T = (m + 1) * n + 1;
for (int i = 1; i <= n; i ++ )
add(S, get(i, 0), INF), add(get(i, m), T, INF);
for (int i = 1; i <= p; i ++ ) cin >> c[i];
for (int j = 1; j <= m; j ++ )
for (int i = 1; i <= n; i ++ )
{
double v = 0, prod = 1, x;
for (int k = 1; k <= p; k ++ )
cin >> x, prod *= x, v += c[k] * prod;
add(get(i, j - 1), get(i, j), v);
add(get(i, j), get(i, j - 1), INF);
}
while (y -- )
{
int i, j, k;
cin >> i >> j >> k;
for (int x = 0; x <= m; x ++ )
if (0 <= x + k && x + k <= m)
add(get(j, x), get(i, x + k), INF);
}
for (int i = 0; i <= (m + 1) * n + 2; i ++ )
cur[i] = h[i];
double t = dinic();
if (t >= INF) cout << "-1\n";
else cout << fixed << setprecision(4) << t << "\n";
}
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int T;
cin >> T;
while (T -- ) solve();
return 0;
}
模拟流和最大流最小割的相互转化
例题 ARC125E
我们从源点向每个零食连流量为
网络流求解类二分图匹配可以做到
考虑转化为对于上面这张图求最小割。
我们发现,我们要割掉左边一些零食点集与
我们可以将这个过程看成删掉左右两边某些点,再删掉中间的某些边,有一个非常妙的性质就是,删掉左右两边的某些点后,必须将没删掉的那些点之间的所有连边全部删除才能保证图不联通,因为对于每一个零食,都对每个人连了边。
设左边零食删去的点集为
性质:每次寻找最短路的时候,找出的最短路是凸的。
下面给出一段模板,用 EK 算法为基础实现费用流,可以通过 P3381 【模板】最小费用最大流 .
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5010, M = 100010, INF = 1e18;
int n, m, S, T;
int h[N], e[M], f[M], w[M], ne[M], idx;
int q[N], d[N], pre[N], incf[N];
bool st[N];
void add(int a, int b, int c, int d)
{
e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx ++ ;
e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx ++ ;
}
bool spfa()
{
int hh = 0, tt = 1;
memset(d, 0x3f, sizeof d);
memset(incf, 0, sizeof incf);
q[0] = S, d[S] = 0, incf[S] = INF;
while (hh != tt)
{
int t = q[hh ++ ];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (f[i] && d[j] > d[t] + w[i])
{
d[j] = d[t] + w[i];
pre[j] = i;
incf[j] = min(f[i], incf[t]);
if (!st[j])
{
q[tt ++ ] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return incf[T] > 0;
}
void EK(int& flow, int& cost)
{
flow = cost = 0;
while (spfa())
{
int t = incf[T];
flow += t, cost += t * d[T];
for (int i = T; i != S; i = e[pre[i] ^ 1])
f[pre[i]] -= t, f[pre[i] ^ 1] += t;
}
}
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
memset(h, -1, sizeof h);
cin >> n >> m >> S >> T;
for (int a, b, c, d; m -- ; )
{
cin >> a >> b >> c >> d;
add(a, b, c, d);
}
int flow, cost;
EK(flow, cost);
cout << flow << " " << cost << "\n";
return 0;
}
下面另给出一份基于 dinic 的实现,更为高效。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010, M = 2 * N, INF = 0x3f3f3f3f;
int n, m, S, T;
int h[N], e[M], ne[M], f[M], w[M], idx;
int cur[N];
int q[N], hh, tt, dist[N];
bool st[N];
int min_cost, max_flow;
void add(int a, int b, int c, int d)
{
e[idx] = b, ne[idx] = h[a], f[idx] = c, w[idx] = d, h[a] = idx ++ ;
e[idx] = a, ne[idx] = h[b], f[idx] = 0, w[idx] = -d, h[b] = idx ++ ;
}
bool spfa()
{
for (int i = 1; i <= n; i ++ )
dist[i] = INF, st[i] = false;
dist[S] = hh = tt = 0, q[0] = S;
cur[S] = h[S];
st[S] = true;
while (hh <= tt)
{
int t = q[hh ++ ];
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i] && f[i])
{
dist[j] = dist[t] + w[i];
cur[j] = h[j];
if (!st[j]) st[j] = true, q[ ++ tt] = j;
}
}
}
return dist[T] != INF;
}
int find(int u, int limit)
{
if (u == T) return limit;
int flow = 0;
st[u] = true;
for (int i = cur[u]; ~i; i = ne[i])
{
cur[u] = i;
int j = e[i];
if (!st[j] && dist[j] == dist[u] + w[i] && f[i])
{
int t = find(j, min(limit - flow, f[i]));
if (!t) dist[j] = INF;
min_cost += t * w[i], f[i] -= t;
f[i ^ 1] += t, flow += t;
}
}
st[u] = false;
return flow;
}
void dinic()
{
int flow = 0;
while (spfa()) while (flow = find(S, INF)) max_flow += flow;
}
int main()
{
cin >> n >> m >> S >> T;
memset(h, -1, sizeof h);
while (m -- )
{
int a, b, c, d;
cin >> a >> b >> c >> d;
add(a, b, c, d);
}
dinic();
cout << max_flow << " " << min_cost << "\n";
return 0;
}
大家可能有一个疑问:如果图存在负环,到底应该如何处理?
例题 P7173
为了消除负环,强制所有负权边
强制满流可以解释为加入流量上界为
下面,给出形式化的加边过程。
注意下文中提及的边的"容量"代表该边的流量下界为 0 ,流量上界为容量。
-
读入边的信息
x, y, f, v 并根据边权分类讨论: -
-
建立附加源汇点
\text{SS}, \text{TT} 并连边,具体地,对于点i : -
-
加入容量为
\inf 的边(T, S) ,在图上跑以\text{SS} 为源点,\text{TT} 为汇点的最小费用最大流(求解有源汇最小费用可行流)。将可行流流量(最后加入的边(T, S) 的流量)计入答案流量,将此次最小费用最大流的费用计入答案费用。 -
撤去所有与附加源汇点
\text{SS}, \text{TT} 相连的边和最后加入的边(T, S) 。跑一次以S 为源点,T 为汇点的最小费用最大流。将此次最小费用最大流的流量和费用计入答案流量和答案费用。
刚开始的强制满流可以保证任意时刻网络无负环。
答案流量为可行流流量与最小费用最大流流量之和,答案费用为强制满流的费用、可行流的费用与最小费用最大流的费用之和。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5010, M = 100010, INF = 1e18;
int n, m, S, T, tot[N], minc;
int h[N], e[M], f[M], w[M], ne[M], idx;
int q[N], d[N], pre[N], incf[N];
bool st[N];
void add(int a, int b, int c, int d)
{
e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx ++ ;
e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx ++ ;
}
bool spfa()
{
int hh = 0, tt = 1;
memset(d, 0x3f, sizeof d);
memset(incf, 0, sizeof incf);
q[0] = S, d[S] = 0, incf[S] = INF;
while (hh != tt)
{
int t = q[hh ++ ];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (f[i] && d[j] > d[t] + w[i])
{
d[j] = d[t] + w[i];
pre[j] = i;
incf[j] = min(f[i], incf[t]);
if (!st[j])
{
q[tt ++ ] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return incf[T] > 0;
}
void EK(int& flow, int& cost)
{
while (spfa())
{
int t = incf[T];
flow += t, cost += t * d[T];
for (int i = T; i != S; i = e[pre[i] ^ 1])
f[pre[i]] -= t, f[pre[i] ^ 1] += t;
}
}
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
memset(h, -1, sizeof h);
cin >> n >> m >> S >> T;
for (int a, b, c, d; m -- ; )
{
cin >> a >> b >> c >> d;
if (d >= 0) add(a, b, c, d);
else add(b, a, c, -d), tot[a] -= c, tot[b] += c, minc += c * d;
}
add(T, S, INF, 0);
int SS = n + 1, TT = SS + 1;
for (int i = 1; i <= n; i ++ )
if (tot[i] > 0) add(SS, i, tot[i], 0);
else if (tot[i] < 0) add(i, TT, -tot[i], 0);
int flow, cost; flow = cost = 0;
swap(SS, S), swap(TT, T);
EK(flow, cost); flow = 0;
swap(SS, S), swap(TT, T);
EK(flow, cost);
cout << flow << " " << cost + minc << "\n";
return 0;
}
算法应用
一般来说,如果题目要求求出最大的代价,常用的算法有动态规划、贪心等。
但是如果题目不具备最优子结构,贪心也没有明显的正确性,数据范围较小,我们就容易想到 费用流。
值得一提的是,在处理有源汇上下界最小费用最大流问题中,在有源汇上下界最大流的基础上加上每条边的费用,附加边以及
在可行流最小费用 + 最大流最小费用的基础上还需要加上
费用递增
首先,我们要掌握用费用递增的方法处理一些费用流问题。
由于一条边的费用是固定,即
那么可以将边拆成若干条,起点和终点仍然保持不变,但是第
例题 CF863E
注意到每个位置
从这里可以想到网络流,由于最终每个数都要出现一次,考虑将原问题求的总代价作为费用,而最大流量设计为能有效安排的位置个数,这样保证了使用最小费用最大流可以在有解的情况下求出最小代价,当然,此时最大流是
考虑一下发现,上述过程对应的是源点向每个位置
由于每种数值
区间覆盖
接下来,我们来学习区间
问题 数轴上存在一些区间
由于区间范围较大,需要将区间端点进行离散化。
需要将数轴上的整数点连接起来,
在目前的网络进行最大流算法,显然每一条边都会有
那么如何才能表示选中一个区间,然后将区间内所有边的流量减一呢?
考虑区间
由于总流量为
例题 P7425
首先如果问题存在解的话,那么可以将坏的停机坪视作无限多,并且飞机要移动的话,一定从好的停机坪移动到坏的停机坪。
如果存在解,将坏的停机坪扩充,并不会影响答案,在原来限制条件下非法的解一定劣于答案。(这是在很多题目中的重要思想)
首先所有好的停机坪等价,坏的停机坪等价,好的转到好的,坏的转到坏的没有意义。如果是从坏的转到好的,由于在降落时已经登机,此时移动只会增加成本。
那么假设所有飞机都在坏的停机坪上,此时一架飞机有三种选择
- 在
[l, r] 时间上占用一个好的停机坪,收益为x ; - 在时刻
l 占用一个好的停机坪,之后移动到坏的停机坪(由于无限,在任意合法时间内移动都行),收益x-p x ; - 始终停留在坏的停机坪上,收益
0 。
这就类似区间
棋盘相关
关于棋盘的最优化问题,往往可以通过黑白染色,或者对于每一行每一列建点,转化为二分图相关模型。
例题 ABC231H
首先对于所有的行建出一个节点,作为二分图的左部点,将所有的列建出一个节点,作为二分图的右部点。
然后可以发现如果用网络流来看的话,每一个点都至少需要流过
那么连上容量为
然后考虑棋子
例题 P4003
本题思路非常简单,黑白染色即可,连边时需要考虑这些边:格子之间的转移,管道转向,到源点和汇点的链接。
具体建图可以参照下图理解,蓝色和黄色的边分别表示从源点出和进入汇点的边,紫色和绿色的边表示管道转动时连接情况的转移,橙色的边表示水在不同的方格中转移。注意图片仅为示意,没有画出完整的点和边,具体建边实现时考虑即可。
下附部分建图代码,作者为 kele7,笔者也参与了调试。
int h(int x,int y,int d){return 4*((x-1)*m+y-1)+d;}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>v[i][j];
}
}t=h(n,m,5);//s=0
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if((i+j)&1){
if(v[i][j]&1)add(h(i,j,1),t,1,0);
if(v[i][j]&2)add(h(i,j,2),t,1,0);
if(v[i][j]&4)add(h(i,j,3),t,1,0);
if(v[i][j]&8)add(h(i,j,4),t,1,0);
if(__builtin_popcount(v[i][j])==3){
if(!(v[i][j]&1)){
add(h(i,j,1),h(i,j,2),1,1);
add(h(i,j,1),h(i,j,3),1,2);
add(h(i,j,1),h(i,j,4),1,1);
}else if(!(v[i][j]&2)){
add(h(i,j,2),h(i,j,1),1,1);
add(h(i,j,2),h(i,j,3),1,1);
add(h(i,j,2),h(i,j,4),1,2);
}else if(!(v[i][j]&4)){
add(h(i,j,3),h(i,j,1),1,2);
add(h(i,j,3),h(i,j,2),1,1);
add(h(i,j,3),h(i,j,4),1,1);
}else if(!(v[i][j]&8)){
add(h(i,j,4),h(i,j,1),1,1);
add(h(i,j,4),h(i,j,2),1,2);
add(h(i,j,4),h(i,j,3),1,1);
}
}else if(__builtin_popcount(v[i][j])==2){
if((v[i][j]&1)&&(v[i][j]&4))continue;//check if it is straight
if((v[i][j]&2)&&(v[i][j]&8))continue;
if(v[i][j]&1)add(h(i,j,3),h(i,j,1),1,1);
if(v[i][j]&2)add(h(i,j,4),h(i,j,2),1,1);
if(v[i][j]&4)add(h(i,j,1),h(i,j,3),1,1);
if(v[i][j]&8)add(h(i,j,2),h(i,j,4),1,1);
}else if(__builtin_popcount(v[i][j])==1){
if(v[i][j]&1){
add(h(i,j,2),h(i,j,1),1,1);
add(h(i,j,3),h(i,j,1),1,2);
add(h(i,j,4),h(i,j,1),1,1);
}else if(v[i][j]&2){
add(h(i,j,1),h(i,j,2),1,1);
add(h(i,j,3),h(i,j,2),1,1);
add(h(i,j,4),h(i,j,2),1,2);
}else if(v[i][j]&4){
add(h(i,j,1),h(i,j,3),1,2);
add(h(i,j,2),h(i,j,3),1,1);
add(h(i,j,4),h(i,j,3),1,1);
}else if(v[i][j]&8){
add(h(i,j,1),h(i,j,4),1,1);
add(h(i,j,2),h(i,j,4),1,2);
add(h(i,j,3),h(i,j,4),1,1);
}
}
}else{
if(v[i][j]&1)add(s,h(i,j,1),1,0);
if(v[i][j]&2)add(s,h(i,j,2),1,0);
if(v[i][j]&4)add(s,h(i,j,3),1,0);
if(v[i][j]&8)add(s,h(i,j,4),1,0);
if(i>1)add(h(i,j,1),h(i-1,j,3),1,0);//^
if(j<m)add(h(i,j,2),h(i,j+1,4),1,0);//>
if(i<n)add(h(i,j,3),h(i+1,j,1),1,0);//v
if(j>1)add(h(i,j,4),h(i,j-1,2),1,0);//<
if(__builtin_popcount(v[i][j])==1){
if(v[i][j]&1){
add(h(i,j,1),h(i,j,2),1,1);
add(h(i,j,1),h(i,j,3),1,2);
add(h(i,j,1),h(i,j,4),1,1);
}else if(v[i][j]&2){
add(h(i,j,2),h(i,j,1),1,1);
add(h(i,j,2),h(i,j,3),1,1);
add(h(i,j,2),h(i,j,4),1,2);
}else if(v[i][j]&4){
add(h(i,j,3),h(i,j,1),1,2);
add(h(i,j,3),h(i,j,2),1,1);
add(h(i,j,3),h(i,j,4),1,1);
}else if(v[i][j]&8){
add(h(i,j,4),h(i,j,1),1,1);
add(h(i,j,4),h(i,j,2),1,2);
add(h(i,j,4),h(i,j,3),1,1);
}
}else if(__builtin_popcount(v[i][j])==2){
if((v[i][j]&1)&&(v[i][j]&4))continue;
if((v[i][j]&2)&&(v[i][j]&8))continue;
if(v[i][j]&1)add(h(i,j,1),h(i,j,3),1,1);
if(v[i][j]&2)add(h(i,j,2),h(i,j,4),1,1);
if(v[i][j]&4)add(h(i,j,3),h(i,j,1),1,1);
if(v[i][j]&8)add(h(i,j,4),h(i,j,2),1,1);
}else if(__builtin_popcount(v[i][j])==3){
if(!(v[i][j]&1)){
add(h(i,j,2),h(i,j,1),1,1);
add(h(i,j,3),h(i,j,1),1,2);
add(h(i,j,4),h(i,j,1),1,1);
}else if(!(v[i][j]&2)){
add(h(i,j,1),h(i,j,2),1,1);
add(h(i,j,3),h(i,j,2),1,1);
add(h(i,j,4),h(i,j,2),1,2);
}else if(!(v[i][j]&4)){
add(h(i,j,1),h(i,j,3),1,2);
add(h(i,j,2),h(i,j,3),1,1);
add(h(i,j,4),h(i,j,3),1,1);
}else if(!(v[i][j]&8)){
add(h(i,j,1),h(i,j,4),1,1);
add(h(i,j,2),h(i,j,4),1,2);
add(h(i,j,3),h(i,j,4),1,1);
}
}
}
}
}while(spfa(s))ans1+=dfs(s,inf,0);
(g[0].size()==ans1&&ans2!=0)?cout<<ans2:cout<<-1;
}
建图优化
如果把左部点的区间和右部点的区间中的所有点两两相连,可以使用线段树优化建图。但是如果是左部点前缀和右部点后缀连边的话,直接对左部点和右部点都分别建一条链,然后把左部点前缀的末尾和右部点后缀的头部两点相连,实现更为简单高效。
例题 CF818G
拆点,把每个位置拆成入点和出点,之间连一条容量为
这样建图产生了
同余部分大致同理,对于每个模