网络流
网络流
基本性质
网络:一张带权有向图,有源点和汇点,源点可以无限提供流量,中间的每个点不产生新流量,但是会用一个流量上限(容量),要求源点只有流出的流,汇点只有流入的流,这样的图被称作网络流
同时根据网络的定义,我们也可以引申出其他名词的定义
源点:只有流出流的点
汇点:只有流入流的点
流量:一条边上可以流过的流量
容量:一条边上可供流过的最大流量
残量(flow):一条边上的容量减去流量
增广路:找到一条源点到汇点的路径使得这条边上的残量大于0
我们据此,可以推理出以下性质:
- 对于任何一条流,都有流量 <= 容量
- \(\sum_{p\in E} k_{p,u} = \sum_{q\in E}k_{u, q}\) 原因:一条边的入流与出流相等
- 对于任何一条有向边,总有:\(k_{u,v} = - k_{v, u}\) 原因是对于一条有向边来说,有 \(k_{u,v}\) 的流从 \(u\) 到 \(v\),就相当于有 \(-k_{v,u}\) 从 \(v\) 到 \(u\)
网络流基本算法
网络流最大流
找到一个分配流量的方案使得网络中流量最大 :
1.找到这条路径上最小的 \(F[u][v]\)(我们设 \(F[u][v]\) 表示 \(u->v\) 这条边上的残量即剩余流量),下面记为 \(flow\)
2.将这条路径上的每一条有向边 \(u->v\) 的残量减去 \(flow\),同时对于起反向边 \(v->u\) 的残量加上 \(flow\)
3.重复上述过程,直到找不出增广路,此时我们就找到了最大流
以上算法是基于增广路定理:网络达到最大流当且仅当残留网络中没有增广路(证明咕了)
我们考虑,假如说我们不想走这条图,我们肯定是要反悔的,那么我们建一条反向边,这样的话我们反悔时可以走反向边退回去,同时根据反向边的权值抵消掉原有的权值即可(注意:如果要建边的话 \(\mathrm{cnt}\) 要从1开始)
考虑优化上述算法,我们使用 \(\mathrm{Dinic}\) 算法
\(\mathrm{Dinic}\) 算法的核心很简单,其实就是我们直接跑这个图不一定达到最优,我们不妨进行一些操作使得其更优
我们考虑分层图分配深度,这样防止一些残量为0的节点被重复选择,同时便于判断程序是否结束
inline bool bfs() {
memset(dep, 0, sizeof(dep));
queue <int> q;
q.push(S); dep[S] = 1;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to, w = e[i].val;
if(w && !dep[v]) dep[v] = dep[u] + 1, q.push(v);
}
}
return dep[T];
}
inline int dfs(int u, int flow_in) {
if (u == T) return flow_in;
int flow_out = 0;
for (int i = head[u]; i && flow_in; i = e[i].nxt) {
int v = e[i].to;
if (e[i].val && dep[v] == dep[u] + 1) {
int res = dfs(v, min(e[i].val, flow_in));
e[i].val -= res;
e[i ^ 1].val += res;
flow_in -= res;
flow_out += res;
}
}
if (flow_out == 0) dep[u] = 0;//这样写就不会再次跑到这个点了,是一个巨大的优化
return flow_out;
}
inline void Dinic() {
read(n, m, S, T);
rep (i, 1, m) {
int u, v, w;
read(u, v, w);
add(u, v, w);
add(v, u, 0);
}
while (bfs())
ans += dfs(S, inf);
write(ans, '\n');
}
网络流最小费用最大流
我们在网络流最小流的基础上增加一个费用 \(w_i\),每当水流流过这条边时,这条边没流1的流量会花费 \(w_i\) 的费用。要在最大流的前提下求出最小费用
做法:我们每次增广一条从源点到汇点的最短路,保证路上的边都没有满流。我们可以考虑通过spfa来进行操作
我们考虑一个问题,就是我们每次只会增广一条路径,那么我们对于当前整条增广路来说,同时减少的流量是相等的,所以大概就是说,我们对于当前这条路来说,我们等于是 \(flow_i\) 这条路径分别乘上每个点的费用,就是走这一次增广所用的费用,好了,这道题我们就能合理的解决了
同时,我们要关注,如果你要走回头边的话,你得把费用减回去,因此,你的反向边应该是费用的相反数
问题:我们现在学会了网络流最小费用最大流,那么请问网络流最大费用最大流怎么办捏?
答: 我们直接跑费用的相反数,然后最后输出 \(\mathrm{-min_cost}\)
代码如下:
inline void adde(int u, int v, int flow, int val) {
add(u, v, flow, val);
add(v, u, 0, -val);
}
inline bool spfa() {
rep (i, 0, N - 1) dis[i] = inf, flow[i] = 0;
queue <int> q;
q.push(S); dis[S] = 0; flow[S] = inf;
while (!q.empty()) {
int u = q.front(); q.pop();
ins[u] = false;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to, flo = e[i].flow, w = e[i].val;
if (flo && dis[u] + w < dis[v]) {
dis[v] = dis[u] + w; pre[v] = i;
flow[v] = min(flow[u], flo);
if (!ins[v]) q.push(v), ins[v] = true;
}
}
}
return flow[T];
}
inline void update() {
int x = T, t = flow[T];
while (x != S) {
e[pre[x]].flow -= t;
e[pre[x] ^ 1].flow += t;
x = e[pre[x] ^ 1].to;
}
min_cost += t * dis[T];
max_flow += t;
}
inline void EK() {
while (spfa()) update();
write(-min_cost, '\n');
}
最小割
割的定义:在原网络图中移出的边集,要求使得网络不连通
最小割:边权和最小的割
最大流最小割定理:最大流的值等于最小割的值
技巧:
求最小边的最小割
设总边数为 \(E\),跑最大流前我们把所有的边都乘 \(E + 1\) 然后再 \(+1\)
得到的结果应该是 $min_{cut} \times {E+1} + $ 割边数量、
输出任意一种最小割的方案
跑过一次最大流之后,在残量网络上,s和t之间不连通了
进行一次 \({dfs/bfs}\),求出从 \(s\) 出发能到达的点集 \(S\),和不能到达的点集 \(T\)
所有从 \(S\) 跨越到 \(T\) 的满流边(残留网络为0)构成了一组最小割
判断一条边是否满流
运行一次最大流算法,得到一个残量网络
取残量网络上的一条满流边(u, v),判断这条边是否一定满流
对残量网络运行Tarjan算法,求出所有SCC
当u和v不属于同一个SCC的时候,这条边一定满流
否则,我们可以在SCC中找到一个包含这条边的反向边的环,沿着环增广一次,仍然不破坏流量平衡,但是这条边已经不满流了
判断某一条边是否可能为最小割中的一条
所有一定满流的边都可能为最小割
判断某条边是否一定出现在最小割中
首先还是对残量网络求SCC
考虑一条满流边(u, v),判断她是否一定出现在最小割中
当u和s属于同一个SCC,并且v和t属于同一个SCC的时候,这条边一定出现在最小割中
清新例题
[P2891 [USACO07OPEN]Dining G]([P2891 USACO07OPEN]Dining G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))
题目描述:有F种食物和D种饮料,每种食物或饮料只能供一头牛享用,且每头牛只享用一种食物和一种饮料。现在有n头牛,每头牛都有自己喜欢的食物种类列表和饮料种类列表,问最多能使几头牛同时享用到自己喜欢的食物和饮料。(1 <= f <= 100, 1 <= d <= 100, 1 <= n <= 100)
我们首先考虑题意,题意是说,对于每头牛,只能同时选一种食物和一种饮料,但是一种食物和一种饮料可以被多头牛喜欢,且一种饮料只能被一头牛选
我们会发现这是个匹配问题,可以考虑用网络流来解决,大概长成 food --> cow --> drink
那么我们可以考虑用网络流来进行操作,我们考虑先从源点到食物全部连边,然后食物向牛连边,牛向饮料连边,饮料向汇点连边
我们考虑其他限制,我们会发现,每一头牛只能搭配一种食物和一种饮料,那么我们考虑所有的流量都为1,跑最大流即可解决
但是我们会发现答案大了,原因是什么?
我们考虑这样一个图
我们发现3直接跑了好多次,所以,我们要让 3 只跑一次
因此决定我们用一种重要的方法——拆点
把 3 拆开,把每一头牛拆成入牛和出牛,中间连边容量为1,即可限制牛只进出一次
//把头文件和define删了以后长得好怪异啊
read(n, F, D);
S = 0, T = F + n + n + D + 1;
rep (i, 1, n) {
add(F + i, F + n + i, 1);
add(F + n + i, F + i, 0);
}
rep (i, 1, F) {
add(S, i, 1);
add(i, S, 0);
}
rep (i, 1, D) {
add(F + n + n + i, T, 1);
add(T, F + n + n + i, 0);
}
rep (i, 1, n) {
int a, b, c;
read(a, b);
rep (j, 1, a) {
read(c);
add(c, F + i, 1);
add(F + i, c, 0);
}
rep (j, 1, b) {
read(c);
add(F + n + i, F + n + n + c, 1);
add(F + n + n + c, F + n + i, 0);
}
}
Dinic();read(n, F, D);
S = 0, T = F + n + n + D + 1;
rep (i, 1, n) {
add(F + i, F + n + i, 1);
add(F + n + i, F + i, 0);
}
rep (i, 1, F) {
add(S, i, 1);
add(i, S, 0);
}
rep (i, 1, D) {
add(F + n + n + i, T, 1);
add(T, F + n + n + i, 0);
}
rep (i, 1, n) {
int a, b, c;
read(a, b);
rep (j, 1, a) {
read(c);
add(c, F + i, 1);
add(F + i, c, 0);
}
rep (j, 1, b) {
read(c);
add(F + n + i, F + n + n + c, 1);
add(F + n + n + c, F + n + i, 0);
}
}
Dinic();
P2045 方格取数加强版
给出一个 \(n*n\) 的矩阵,每一格有一个非负整数 \(A_{ij}\),(\(A_{ij} <= 1000\))现在从 \((1,1)\) 出发,可以往右或者往下走,最后到达 \((n,n)\),每达到一格,把该格子的数取出来,该格子的数就变成 \(0\),这样一共走 \(K\) 次,现在要求 \(K\) 次所达到的方格的数的和最大
这道题的题意很明确,虽然 $n \le 50,k\le 10 $ 很小,但是搜索仍然是过不去
所以我们考虑其他方法,我们会发现一个点只会通往相邻两个点,我们可以考虑用费用流,把每个点拆成入点和出点,然后入点向出点连两条边,一条表示要取这个点,那么就是费用为 \(a_{i,j}\) ,容量为 \(1\)(防止重复取),另一条表示不取这条边,那么容量为 \(k\) ,费用为0即可。同时,从出点往下面和右面的入点各连一条边,容量为 \(k\),费用为0即可
inline int in(int i, int j) {
return (i - 1) * n + j;
}
inline int out(int i, int j) {
return (i - 1) * n + j + n * n;
}
signed main() {
read(n, k);
T = n * n * 2 + 1;
adde(S, 1, k, 0);
adde(n * n * 2, T, k, 0);
rep (i, 1, n) {
rep (j, 1, n) {
int x; read(x);
adde(in(i, j), out(i, j), 1, -x);
adde(in(i, j), out(i, j), k, 0);
if (i < n) adde(out(i, j), in(i + 1, j), k, 0);
if (j < n) adde(out(i, j), in(i, j + 1), k, 0);
}
}
EK();
return 0;
}
P4016 负载平衡问题
\(G\) 公司有 \(n\) 个沿铁路运输线环形排列的仓库,每个仓库存储的货物数量不等。如何用最少搬运量可以使 \(n\) 个仓库的库存数量相同。搬运货物时,只能在相邻的仓库之间搬运。
\(1 \le n \le 100\)
这个题是个贪心题……
但是既然它可以用网络流写,那我们就写一写
我们考虑,对于一个车站来说,其实它最后的值是确定的
那么我们可以考虑,最后的边可以都连向终点,容量为平均数,费用为 \(0\)
那么我们同时可以将源点连向每个点,容量均为 \(inf\),费用为 \(0\)
最后,我们在相邻两点之间连边,容量为 \(inf\),费用为 \(1\)
这是第一种方法
另一种方法就是
按照 比最终库存多与少分开
多的连源点,少的连汇点,这种方法说实话,我不知道怎么想出来的
P2770 航空路线问题
给定一张航空图,图中顶点代表城市,边代表两城市间的直通航线,并且不存在任何两个城市在同一条经线上。现要求找出一条满足下述限制条件的且途经城市最多的旅行路线。
- 从最西端城市出发,单向从西向东途经若干城市到达最东端城市,然后再单向从东向西飞回起点(可途经若干城市)。
- 除起点城市外,任何城市只能访问一次。
对于给定的航空图,试设计一个算法找出一条满足要求的最佳航空旅行路线。
\(1 \le n \le 100\)
首先,题目要求是求一个方案从左边走到最右边,再从最右边回到左边,每个点只能经过一次,同时要求经过的点尽可能多
首先我们发现有两个限制,一是只经过一次,而是经过的点尽可能多,那我们就可以考虑费用流了
每个点只经过一次我们肯定熟啊,肯定是拆点,中间连一条容量为1的边,由于我们每经过一个点,对答案的贡献为1,我们直接让费用也是1即可
其次,我从左到右再从右到左的不相交路径,我们可以看成从起点到终点的两条不同路径
那么我们给起点和终点的容量赋成2,费用为0,连边的点容量为1,费用为0即可
同时我们考虑统计答案,由于我们每条路顶多走一次,同时容量为1,所以我们考虑最后走过的边必然是残量为0的那几条
我们首先正着跑一遍,然后输出每一条边并进行标记,然后倒着跑一遍,倒着输出即可
注意:
- 可能存在一条从 \(1\sim n\) 再从 \(n\sim 1\) 的路径
- 输出的时候要输出 \(\Huge !\)
map<int, string> mp1;
map<string, int> mp2;
inline int in(int x) {return x;}
inline int out(int x) {return x + n;}
bool vis[N];
inline void dfs1(int u) {
vis[u] = true;
cout << mp1[u - n] << endl;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to, flo = e[i].flow;
if (v <= n && !flo) {
dfs1(out(v));
break;
}
}
}
inline void dfs2(int u) {
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to, flo = e[i].flow;
if (v <= n && !vis[out(v)] && !flo)
dfs2(out(v));
}
cout << mp1[u - n] << endl;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m;
S = 1, T = out(n);
rep (i, 1, n) {
string a;
cin >> a;
mp1[i] = a;
mp2[a] = i;
if (i == 1 || i == n)link(in(i), out(i), 2, 0);
else link(in(i), out(i), 1, 0);
}
bool flag = 0;
rep (i, 1, m) {
string a, b;
cin >> a >> b;
int u = mp2[a], v = mp2[b];
if (u > v) swap(u, v);
if (u == 1 && v == n) flag = true;
link(out(u), in(v), 1, -1);
}
EK();
if (max_flow == 0) return puts("No Solution!"), 0;
if (max_flow == 1 && !flag) return puts("No Solution!"), 0;
if (max_flow == 1) {
cout << 2 << endl << mp1[1] << endl << mp1[n] << endl << mp1[1] << endl;
return 0;
}
cout << -min_cost << endl;
dfs1(out(1));
dfs2(out(1));
return 0;
}
P2766 最长不下降子序列问题
给定正整数序列 \(x_1,x_2,...x_n\)
- 计算其最长不下降子序列的长度 \(s\)
- 如果每一个元素只允许用一次,求最多有几个长度为 \(s\) 的最长不下降子序列
- 如果允许一个 \(x_1\) 和 \(x_n\) 被多次使用,但是一个序列中每个元素只允许使用一次,求最多有几个长度为 \(s\) 的最长不下降子序列
第一问我们运用传统DP思想,设 \(f_i\) 表示前 \(i\) 位最长长度,转移方程可以很轻松的写成
第二问的话,我们考虑如果一个数在长度为 \(s\) 的最长不下降子序列中,那么它们必然在位置 \(f_i\) 上
简单证明一下:
如果 \(pos_i < f_i\),此时整个数列的长度为 \(s\),我们会考虑,对于 \(pos_i\) 之前必然会小于 \(f_i - 1\) 个数,这样的话,必然存在另一个长度等于 \(f_i - 1\) 的序列代替它的最长不下降子序列必然大于等于 \(s+1\)
换一种方式解释:设 \(pos_1\) 前的长度为 \(len\),那么 \(len < f_i - 1\),所以必然存在一个长度大于 \(len\) 的一个位于 \(pos_i\)之前的数列来代替它,这样的话长度就会大于 \(s\)
如果 \(pos_i > f_i\),说明 \(pos_i\) 前面有大于等于 \(f_i\) 个数,那么 \(f_i+1 = f_i\),显然矛盾
因此 \(pos_i = f_i\),证明完毕
我们重新考虑这个题的写法,对于每个元素,它的位置我们已经固定且只能用一次,那么我们可以考虑用最大流
老规矩,只能用一次,直接拆点,拆成两个点,中间的容量为 1,对于每个点的位置固定,所以从源点 \(S\) 向每个 \(f_i\) 为 1 的点连边,由于这个 \(f_i= 1\) 的点只能被用一次,所以我们直接让其容量为 1 即可,最后再让 \(f_i\) 为 \(s\) 的连向 \(T\)
第三问,允许多次使用?直接容量为inf即可
int f[N];
inline int in(int x) {return x;}
inline int out(int x) {return x + n;}
inline void ask1() {
rep (i, 1, n){
f[i] = 1;
rep (j, 1, i - 1)
if (a[i] >= a[j])
f[i] = max(f[i], f[j] + 1);
}
rep (i, 1, n) maxn = max(maxn, f[i]);
write(maxn, '\n');
}
inline void ask2() {
S = 0, T = 2 * n + 1;
rep (i, 1, n) link(in(i), out(i), 1);
rep (i, 1, n) {
if (f[i] == 1) link(S, in(i), 1);
if (f[i] == maxn) link(out(i), T, 1);
rep (j, 1, i - 1)
if (a[i] >= a[j] && f[i] == f[j] + 1)
link(out(j), in(i), 1);
}
Dinic();
}
inline void ask3() {
if (n == 1) return cout << 1 << endl, void();
memset(head, 0, sizeof(head));
memset(e, 0, sizeof(e));
cnt = 1;
rep (i, 1, n) {
if (i == 1 || i == n) link(in(i), out(i), inf);
else link(in(i), out(i), 1);
if (f[i] == 1) {
if(i == 1) link(S, in(i), inf);
else link(S, in(i), 1);
}
if (f[i] == maxn) {
if(i == n) link(out(i), T, inf);
else link(out(i), T, 1);
}
rep (j, i + 1, n) {
if (f[i] + 1 == f[j] && a[i] <= a[j])
link(out(i), in(j), 1);
}
}
Dinic();
}
//write:RevolutionBP