「笔记」网络流
写在前面
简单记录下板子= =
如果之后有空会来完善(gugugu
upd on 2023.3.18:
咕咕咕咕咕
基础概念
网络是一张有向图 ,每条边 都有一个权值 ,被称为边容量。其中有两个关键点,被称为源点 和汇点 。
一张图的流函数 被定义为满足下述三个条件的实数函数:
- 。
- 。
- 。
其中 表示一条边,在满足上述三个条件的情况下,流函数可以是任意实数。
对于一条边, 被称为边的流量, 称为边的剩余容量,整个网络的流量为 ,即源点的流出量。
可以将流的概念形象地理解为通过许多中继节点的两台电脑之间的数据传输,边可以看做传输线路。每条传输线路的传输量限制是边的容量(对应条件 1),总传输量等于源点的流出量,也等于汇点的接收量(对应条件 2)。
最大流
最大流问题即求得从源点到汇点的最大流量 。
FF
对原图中的有向边添加反向边,反向边初始容量为 0。
对于一张图的流函数,其残量网络被定义为网络中所有节点及剩余容量大于 0 的边构成的子图。每一个当前流对应着一个残量网络。
定义一条从源点到汇点的路径上所有边的 剩余容量都大于 0 的路径为增广路。残量网络中一条从源点到汇点的路径一定是增广路。
FF 算法每次从源点开始 dfs 找增广路,找到一条增广路后,令增广路上所有边容量减去增加的流量,并使它们的反边容量加上增加的流量。之后遍历到反边可以认为是流量的“反悔”。
残量网络不存在增广路的充要条件是当前流已是最大流,因此找不到任何增广路后即得最大流。每轮 dfs 可行流流量至少增加 1,总时间复杂度为 。
太菜了不配拥有代码。
EK
Edmonds-Karp
从源点开始 bfs 实现 FF,到达汇点后停止 bfs 并增广一次。增广时记录转移前驱并更新路径上边的残余容量,答案加上最小流量。
时间复杂度 级别。
复制复制//知识点:网络最大流,EK /* By:Luckyblock */ #include <algorithm> #include <cctype> #include <cstdio> #include <cstring> #include <queue> #define LL long long const int kN = 210 + 10; const int kM = 1e4 + 10; const LL kInf = 9e18 + 2077; //============================================================= int n, m, s, t; int e_num = 1, head[kN], v[kM], ne[kM], from[kN]; LL f[kN], w[kM]; bool vis[kN]; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } void Chkmax(int &fir_, int sec_) { if (sec_ > fir_) fir_ = sec_; } void Chkmin(int &fir_, int sec_) { if (sec_ < fir_) fir_ = sec_; } void AddEdge(int u_, int v_, int w_) { v[++ e_num] = v_; w[e_num] = 1ll * w_; ne[e_num] = head[u_]; head[u_] = e_num; } bool Bfs() { //找到一条增广路 memset(vis, false, sizeof (vis)); std::queue <int> q; vis[s] = false; f[s] = kInf; q.push(s); while (!q.empty()) { int u_ = q.front(); q.pop(); for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; LL w_ = w[i]; if (!w_ || vis[v_]) continue; f[v_] = std::min(f[u_], w_); vis[v_] = true; from[v_] = i; q.push(v_); if (v_ == t) return true; } } return false; } LL EK() { LL ret = 0; while (Bfs()) { //更改路径的残余容量 for (int u_ = t; u_ != s; u_ = v[from[u_] ^ 1]) { w[from[u_]] -= f[t]; w[from[u_] ^ 1] += f[t]; } ret += f[t]; } return ret; } //============================================================= int main() { n = read(), m = read(), s = read(), t = read(); for (int i = 1; i <= m; ++ i) { int u_ = read(), v_ = read(), w_ = read(); AddEdge(u_, v_, w_); AddEdge(v_, u_, 0); } printf("%lld\n", EK()); return 0; }
Dinic
使用 dfs 找增广路,不过在每轮增广前使用 bfs 将残余网络分层。一个点的层数为残余网络上它与源点的最小边数。分层时若汇点的层数不存在,说明不存在增广路,即可停止增广。
之后 dfs 增广时,每次转移仅向比当前点层数多 1 的点转移。这可以保证每次找到的增广路是最短的。
但朴素地进行上述过程时,如果某个点同时具有大量的入边和出边,在该点进行流量传递时的复杂度最坏可达到 级别。为了保证 Dinic 的时间复杂度,必须使用当前弧优化——对于每个点维护该点的所有出边中还有必要尝试的第一条边,使得一条边只增广一次。
除此之外,多路增广是 Dinic 的一个常数优化——一次 dfs 找多条增广路。
Dinic 的时间复杂度上界为 级别,不过实际运行效率玄学,在大多数时候 Dinic 都能取得较高的运行效率。
//知识点:网络最大流,Dinic /* By:Luckyblock https://www.luogu.com.cn/problem/P3376 */ #include <bits/stdc++.h> #define LL long long const int kN = 210; const int kM = 2e6 + 10; const LL kInf = 1e18 + 2077; //============================================================= int n, m, S, T; int edgenum = 1, v[kM], ne[kM], head[kN]; int cur[kN], dep[kN]; LL w[kM]; //============================================================= void Add(int u_, int v_, LL w_) { v[++ edgenum] = v_; w[edgenum] = w_; ne[edgenum] = head[u_]; head[u_] = edgenum; } void Init() { std::cin >> n >> m >> S >> T; for (int i = 1; i <= m; ++ i) { int u_, v_, w_; std::cin >> u_ >> v_ >> w_; Add(u_, v_, w_), Add(v_, u_, 0); } } bool BFS() { std::queue <int> q; memset(dep, 0, (n + 1) * sizeof (int)); dep[S] = 1; //注意初始化 q.push(S); while (!q.empty()) { int u_ = q.front(); q.pop(); for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; LL w_ = w[i]; if (w_ > 0 && !dep[v_]) { dep[v_] = dep[u_] + 1; q.push(v_); } } } return dep[T]; } LL DFS1(int u_, LL into_) { if (u_ == T) return into_; LL ret = 0; for (int i = cur[u_]; i && into_; i = ne[i]) { int v_ = v[i]; LL w_ = w[i]; if (w_ && dep[v_] == dep[u_] + 1) { LL dist = DFS1(v_, std::min(into_, w_)); if (!dist) dep[v_] = kN; into_ -= dist; ret += dist; w[i] -= dist, w[i ^ 1] += dist; if (!into_) return ret; } } if (!ret) dep[u_] = 0; return ret; } LL Dinic() { LL ret = 0; while (BFS()) { memcpy(cur, head, (n + 1) * sizeof (int)); ret += DFS1(S, kInf); } return ret; } //============================================================= int main() { //freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); Init(); // for (int i = 1; i <= nodenum; ++ i) { // for (int j = head1[i]; j; j = ne[j]) { // if (w[j] == 0) continue; // std::cout << i << " " << v[j] << " " << w[j] << "\n"; // } // } std::cout << Dinic() << "\n"; return 0; }
最小割
对于一个网络流图 ,其割的定义为一种点的划分方式:将所有的点划分为 和 两个集合,其中源点 ,汇点 。定义割的容量为所有从 到 的边的容量之和,最小割问题就是求得一个割 ,使得割的容量最小。
更通俗一点的定义:对于一个网络流图,求断掉其中的数条边后使得 到 的流量为 0 时,断掉的边容量之和的最小值。
定理:
最大流 = 最小割。
证明详见:https://oi-wiki.org/graph/flow/min-cut/。
可以感性地理解,为了得到最小割,我们应当尽可能选择流量较小的边断开。
输出方案
跑出最大流后从源点 开始 dfs,每次走残量大于 0 的边即可找到点集 内的所有点。
割边数量
如需要在最小割的前提下最小化割边数量,那么先求出最小割,把没有满流的边容量改成 ,满流的边容量改成 1,重新就求一遍最小割即得最小割边数量。
若没有最小割的前提,直接把所有边的容量设成 1 求最小割即可。
费用流
Primal-Dual 原始对偶算法
// /* By:Luckyblock https://www.luogu.com.cn/problem/P3381 */ #include <bits/stdc++.h> #define LL long long #define pli std::pair<LL,int> #define mp std::make_pair const int kN = 5e3 + 10; const int kM = 2e5 + 10; const LL kInf = 1e18 + 2077; //============================================================= int n, m, S, T; int edgenum = 1, head[kN], v[kM], ne[kM]; bool vis[kN]; LL w[kM], c[kM], h[kN], dis[kN]; LL maxf, minc; struct Previous_Node { int node, edge; } from[kN]; //============================================================= void Add(int u_, int v_, LL w_, LL c_) { v[++ edgenum] = v_; w[edgenum] = w_; c[edgenum] = c_; ne[edgenum] = head[u_]; head[u_] = edgenum; } void Spfa(int s_) { std::queue <int> q; for (int i = 0; i <= n; ++ i) { vis[i] = 0; h[i] = kInf; } q.push(s_); h[s_] = 0; vis[s_] = true; while (!q.empty()) { int u_ = q.front(); q.pop(); vis[u_] = false; for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i], w_ = w[i], c_ = c[i]; if (w_ && h[u_] + c_ < h[v_]) { h[v_] = h[u_] + c_; if (!vis[v_]) q.push(v_), vis[v_] = true; } } } } bool Dijkstra(int s_) { std::priority_queue<pli> q; for (int i = 1; i <= n; ++ i) { vis[i] = 0, dis[i] = kInf; } dis[s_] = 0; q.push(mp(0, s_)); while (!q.empty()) { int u_ = q.top().second; q.pop(); if (vis[u_]) continue; vis[u_] = true; for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i], w_ = w[i], nc_ = c[i] + h[u_] - h[v_]; if (w_ && dis[u_] + nc_ < dis[v_]) { dis[v_] = dis[u_] + nc_; from[v_] = (Previous_Node) {u_, i}; if (!vis[v_]) q.push(mp(-dis[v_], v_)); } } } return dis[T] != kInf; } void MCMF() { Spfa(S); while (Dijkstra(S)) { LL minf = kInf; for (int i = 1; i <= n; ++ i) h[i] += dis[i]; for (int i = T; i != S; i = from[i].node) minf = std::min(minf, w[from[i].edge]); for (int i = T; i != S; i = from[i].node) { w[from[i].edge] -= minf; w[from[i].edge ^ 1] += minf; } maxf += minf; minc += minf * h[T]; } return ; } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); std::cin >> n >> m >> S >> T; for (int i = 1; i <= m; ++ i) { int u_, v_, w_, c_; std::cin >> u_ >> v_ >> w_ >> c_; Add(u_, v_, w_, c_); Add(v_, u_, 0, -c_); } MCMF(); std::cout << maxf << " " << minc; return 0; }
写在最后
参考:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】