网络流
1 网络流基础概念
网络流的概念分为网络和流。
网络是指一种特殊的有向图
对于一个网络,一个流
- 每条边上的流量
不能大于它的容量 。 - 每个点流入的流量等于流出的流量。
而对于整个网络和它上面的流
网络流有很多问题和模型,下面详细讲解。
2 最大流
2.1 问题概述
对于一个网络,找到一个流,使得流的流量最大。
通常情况下,我们使用 Dinic 算法求解最大流。在此之前需要先了解 FF 增广。
2.2 FF 算法
即 Ford-Fulkerson 算法,是一种计算最大流的算法的总称,基于贪心思想。
首先我们对于一个网络
- 对于一条边
,我们将其容量与流量之差称为剩余容量,记作 。 - 将
中所有剩余容量大于 的边和节点构成的子图称为残量网络,记作 。
我们将
显然,我们可以将求解最大流的过程看做不断增广。因此 FF 的本质就是不断找增广路进行增广,直到找不到为止。
此时考虑这样的情况:

假如我们此时找到的增广路为

此时已经不存在增广路了,然而最大流是
为了解决这样的问题,我们引入反向边。我们约定
可能我们会觉得负数的流量很诡异,不过我们的重点并不在于流量本身,而是残量网络。当正向边流量增加时,剩余容量减少;同时反向边流量减少,剩余容量增加。
例如下图:

我们在反方向建边权为

这时,我们还可以再找到一条增广路,即

此时我们发现,
因此反向边的实质就是一种撤销,由于反向边时刻在加上正向边丢失的容量,它就代表着可以撤回的容量。
这就是 FF 算法的核心思想:残量网络和反向边。
接下来考虑如何实现这个算法,显然暴力 DFS 可行,但是时间复杂度过高,必须改进优化。
2.3 EK 算法
即 Edmonds-Karp 算法,利用 BFS 进行 FF 增广。
EK 算法的具体流程如下:
- 在
上如果可以从 出发走到 ,代表我们找到了增广路。 - 在增广路上,求出剩余容量的最小值,给每条边的流量加上它,同时给反向边容量减去它。
- 我们重复上述过程,直到没有增广路为止。
这就是 EK 算法。单轮 BFS 增广复杂度为
因此 FF 增广和 EK 算法都不是求最大流的主流算法,真正最有用的是下面这个。
2.4 Dinic 算法
Dinic 算法是对于 FF/EK 算法的优化,将两者使用的 DFS 和 BFS 相结合。
考虑到 EK 算法不优在哪里,由于每一次 BFS 都只能找到一条增广路,效率底下;同时受反向边影响,DFS 可能会来回绕圈子。
为了解决第二个问题,我们在增广前对
接下来我们进行 DFS,为了解决第一个问题,我们采用多路增广。对于一个节点,如果从一个儿子走找到了增广路,我们不必回到
有了这样的指导,我们可以写出一个朴素的 Dinic 出来:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;
int n, m, s, t;
int head[Maxn], edgenum = 1;//和 tarjan 求桥很像,利用 edgenum=1 判断双向边
struct node {
int nxt, to, w;
}edge[Maxn];
void add(int from, int to, int w) {//加边要加双向边
edge[++edgenum] = {head[from], to, w};
head[from] = edgenum;
edge[++edgenum] = {head[to], from, 0};
head[to] = edgenum;
}
int dis[Maxn];
bool bfs() {
for(int i = 1; i <= n; i++) dis[i] = 0;
queue <int> q;
q.push(s);
dis[s] = 1;
while(!q.empty()) {
int x = q.front();
q.pop();
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to, w = edge[i].w;
if(w > 0 && dis[to] == 0) {//找到还未遍历且还有剩余容量的
dis[to] = dis[x] + 1;//预处理距离
if(to == t) return 1;//找到一条 s->t 的路径
q.push(to);
}
}
}
return 0;//不存在增广路了
}
int dfs(int x, int flow) {//上一层传入的流量
if(x == t) return flow;//找到汇点就返回
int rest = flow;//残量网络
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to, w = edge[i].w;
if(w > 0 && dis[to] == dis[x] + 1) {//还有剩余容量且在下一层
int k = dfs(to, min(rest, w));//计算下面的层能经过的流量,也就是当前点能经过的流量
rest -= k;//剩余容量减少
edge[i].w -= k;
edge[i ^ 1].w += k;//反向剩余容量增加
}
}
return flow - rest;//返回当前节点能经过的流量
}
int ans = 0;
void dinic() {
while(bfs()) {//重复找有无增广路,建立分层图
ans += dfs(s, Inf);//从源点出发找
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> s >> t;
for(int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
}
dinic();
cout << ans << '\n';
return 0;
}
我们发现,此时的 Dinic 使用的 DFS 本质上还是一个暴力,复杂度也并不优秀,因此我们还需要一个东西:当前弧优化。
观察代码,我们发现如果有一个节点
放到 Dinic 中来看,我们对于一个节点下一层的节点,一定会把这个节点之后的边的剩余容量榨干,此时我们就不需要再走这个节点了。因此对于每个节点 head[x]
换成这个指针 cur[x]
。
于是我们就可以保证 Dinic 的正确时间复杂度。对于单次 DFS,我们可以找到不超过
但是仔细思考会发现,如果一个图要满足上面提到的所有 “不超过” 条件来卡满复杂度是比较困难的。实际运用中,很少会有人专门卡 Dinic,因此这个复杂度仅仅是一个理论上限,大部分图中,Dinic 的表现都十分优秀。
当然 Dinic 还有一些别的常数优化,如下:
- 剩余流量判断:如果上一层节点传递的流量已经消耗完了,就不用再进行 DFS。
- 无用节点删除:如果我们将一些流量传给下一层节点,但是下一层节点返回的流量为
,意味着这个节点无法再进行增广,我们将它删除。
所以最终版本的 Dinic 代码如下:
#include <bits/stdc++.h>
#define int long long
using namespace std;
typedef long long LL;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;
int n, m, s, t;
int head[Maxn], edgenum = 1;//和 tarjan 求桥很像,利用 edgenum=1 判断双向边
struct node {
int nxt, to, w;
}edge[Maxn];
int cur[Maxn];//当前弧
void add(int from, int to, int w) {//加边要加双向边
edge[++edgenum] = {head[from], to, w};
head[from] = edgenum;
edge[++edgenum] = {head[to], from, 0};
head[to] = edgenum;
}
int dis[Maxn];
bool bfs() {
for(int i = 1; i <= n; i++) {
dis[i] = 0;
cur[i] = head[i];//当前弧优化的初始化
}
queue <int> q;
q.push(s);
dis[s] = 1;
while(!q.empty()) {
int x = q.front();
q.pop();
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to, w = edge[i].w;
if(w > 0 && dis[to] == 0) {//找到还未遍历且还有剩余容量的
dis[to] = dis[x] + 1;//预处理距离
if(to == t) return 1;//找到一条 s->t 的路径
q.push(to);
}
}
}
return 0;//不存在增广路了
}
int dfs(int x, int flow) {//上一层传入的流量
if(x == t) return flow;//找到汇点就返回
int rest = flow;//残量网络
for(int i = cur[x]; i && rest/*剩余容量判断*/; i = edge[i].nxt) {
cur[x] = i;//当前弧优化
int to = edge[i].to, w = edge[i].w;
if(w > 0 && dis[to] == dis[x] + 1) {//还有剩余容量且在下一层
int k = dfs(to, min(rest, w));//找到下面的层能经过的流量
if(k == 0) dis[to] = 0;//无用节点删除,dis 数组为 0 就永远不会在走了
rest -= k;//剩余容量减少
edge[i].w -= k;
edge[i ^ 1].w += k;//反向剩余容量增加
}
}
return flow - rest;//返回当前节点能经过的流量
}
int ans = 0;
void dinic() {
while(bfs()) {//重复找有无增广路,建立分层图
ans += dfs(s, Inf);//从源点出发找
}
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> s >> t;
for(int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
}
dinic();
cout << ans << '\n';
return 0;
}
3 最小割
3.1 问题概述
对于一个网络
对于一个
最小割问题就是求出这个容量的最小值。
3.2 最大流最小割定理
3.2.1 定理内容
对于一个网络,其最大流
3.2.2 定理证明
这个定理看上去很奇妙,下面我们尝试证明它。
首先我们先证明一个引理:对于一个网络
首先让我们定义一个点的净流量为
,显然由网络流性质 可得一个流的流量就是 的净流量。然后得到下列式子: 引理得证。
接下来我们需要证明第二个引理:对于一个网络,总会存在流
考虑 FF 增广的相关思路。
假设在几轮增广后,得到的流
使得在 上不存在增广路,即不存在 到 的路径。此时我们记从 出发可以到达的点集为 ,同时记剩下的所有点集为 。显然此时 是 的一个割,且 。 接下来我们对
中的边分为原图上的边和反向边进行讨论:
- 对于原图上的边
:此时 ,因此 ,即满足 都是满流。 - 对于反向边
:此时 。由于反向边的 是 ,因此 也都是 ,即满足 都是空流。 因此总会存在流
和割 ,满足 。引理得证。
接下来我们看最大流
- 由第一条引理,
。 - 由第二条引理,由于
,而又必然存在 ,则可以得到 。
因此对于最大流
那么显然就可以得到
3.3 最小割树
3.3.1 概念及思想
我们知道求两点之间的最小割可以用 Dinic 算法,那如果我们要多次询问两点之间的最小割呢?
首先明晰我们建的这个东西有什么性质,最小割树的性质就是对于任意两点
这个性质听上去很美妙,现在我们看如何构建最小割树。
首先我们随意在图中选两个点
接下来我们在一张新图上连接
此时我们查询树上
最后我们查询直接在树上查询即可,利用倍增或树剖。
据说好像这并不是真正的最小割树,而是一种叫等价流树的东西。
3.3.2 代码
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;
int n, m, q;
int u[Maxn], v[Maxn], w[Maxn];
struct MinCut {
int head[Maxn], edgenum = 1;
struct node {
int nxt, to, w;
}edge[Maxn];
void add(int u, int v, int w) {
edge[++edgenum] = {head[u], v, w};
head[u] = edgenum;
edge[++edgenum] = {head[v], u, 0};
head[v] = edgenum;
}
int dis[Maxn], cur[Maxn];
bool bfs(int s, int t) {
for(int i = 1; i <= n; i++) {
dis[i] = 0;
cur[i] = head[i];
}
queue <int> q;
q.push(s);
dis[s] = 1;
while(!q.empty()) {
int x = q.front();
q.pop();
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to, w = edge[i].w;
if(w > 0 && !dis[to]) {
dis[to] = dis[x] + 1;
if(to == t) return 1;
q.push(to);
}
}
}
return 0;
}
int dfs(int x, int t, int flow) {
if(x == t) return flow;
int rest = flow;
for(int i = cur[x]; i && rest; i = edge[i].nxt) {
int to = edge[i].to, w = edge[i].w;
if(w > 0 && dis[to] == dis[x] + 1) {
int k = dfs(to, t, min(rest, w));
if(k == 0) dis[to] = 0;
rest -= k;
edge[i].w -= k;
edge[i ^ 1].w += k;
}
}
return flow - rest;
}
bool vis[Maxn];
void solve(int x) {//从源点 s 出发,走没有满流的边,记录到的点都属于 S 集合
vis[x] = 1;
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to, w = edge[i].w;
if(w > 0 && !vis[to]) {
solve(to);
}
}
}
int dinic(int s, int t) {//注意:每次找最小割的时候都是在原图上找,不是在割下来的集合里找!
int ans = 0;
edgenum = 1;
for(int i = 1; i <= n; i++) {
head[i] = 0;
vis[i] = 0;
}
for(int i = 1; i <= m; i++) {//初始化
add(u[i], v[i], w[i]);
add(v[i], u[i], w[i]);
}
while(bfs(s, t)) {
ans += dfs(s, t, Inf);
}
solve(s);
return ans;
}
}MC;
int p[Maxn];
struct MinCutTree {
int head[Maxn], edgenum;
struct node {
int nxt, to, w;
}edge[Maxn];
void add(int u, int v, int w) {
edge[++edgenum] = {head[u], v, w};
head[u] = edgenum;
}
int t1[Maxn], t2[Maxn];
void build(int l, int r) {//建树
if(l >= r) return ;
int s = p[l], t = p[l + 1];//挑两个节点
int res = MC.dinic(s, t);
add(s, t, res), add(t, s, res);//连边
int p1 = 0, p2 = 0;//下面这一部分就是一个普通的分治,很像归并排序
for(int i = l; i <= r; i++) {
if(MC.vis[p[i]]) {
t1[++p1] = p[i];
}
else {
t2[++p2] = p[i];
}
}
for(int i = 1; i <= p1; i++) {
p[l + i - 1] = t1[i];
}
for(int i = 1; i <= p2; i++) {
p[l + p1 + i - 1] = t2[i];
}
build(l, l + p1 - 1);
build(l + p1, r);
}
int fa[Maxn][21], minn[Maxn][21], dep[Maxn];
void dfs(int x, int f) {//倍增预处理
dep[x] = dep[f] + 1;
for(int i = 1; i <= 20; i++) {
fa[x][i] = fa[fa[x][i - 1]][i - 1];
minn[x][i] = min(minn[x][i - 1], minn[fa[x][i - 1]][i - 1]);
}
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to, w = edge[i].w;
if(to == f) continue;
fa[to][0] = x, minn[to][0] = w;
dfs(to, x);
}
}
int query(int x, int y) {//倍增求解
if(dep[x] > dep[y]) swap(x, y);
int ans = 2e9;
for(int i = 20; i >= 0; i--) {
if(dep[fa[y][i]] >= dep[x]) {
ans = min(ans, minn[y][i]);
y = fa[y][i];
}
}
if(x == y) return ans;
for(int i = 20; i >= 0; i--) {
if(fa[x][i] != fa[y][i]) {
ans = min(ans, min(minn[x][i], minn[y][i]));
x = fa[x][i];
y = fa[y][i];
}
}
ans = min(ans, min(minn[x][0], minn[y][0]));
return ans;
}
}MCT;
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i++) {
cin >> u[i] >> v[i] >> w[i];
u[i]++, v[i]++;
}
n++;//注意这道题目的特性
for(int i = 1; i <= n; i++) {
p[i] = i;
}
MCT.build(1, n);
MCT.dfs(1, 0);
cin >> q;
while(q--) {
int x, y;
cin >> x >> y;
x++, y++;
cout << MCT.query(x, y) << '\n';
}
return 0;
}
3.4 最大权闭合子图
3.4.1 问题概述
给定一个有向图
在给每一个点附上点的权值之后,所有闭合子图中点权之和最大的就是最大权闭合子图。
3.4.2 解法及证明
首先我们给这个图加上两个点:源点
- 源点向所有点权为正的点连边,容量为点权。
- 所有点权为负的点向汇点连边,容量为点权的绝对值。
- 点权为
的点可以任选一种方式,没有影响。 - 原图中的边不变,容量为
。
接下来在这个网络中跑出最大流,接着结论就是最大权闭合子图 = 所有正点权之和 - 最大流。
接下来我们证明这个结论:
首先每一个闭合子图都对应着网络中的一个割。显然一个割将网络分成了两部分,而与
相连的那一部分没有边指向另一部分,于是这就是一个闭合子图。 然后我们知道,我们选的最小割中的边一定和
中一者相连,否则我们就会选到中间边权为 的边,显然不符合最小割定义。 此时对于我们选择的闭合子图,权值和 = 所有正点权之和 - 我们未选择的正点权之和 + 我们选择了的负点权之和。我们考虑,没有选择的正点权与
连的边就会断开,而选择了的负点权就会和 的连边断开。接着我们就可以得到: 权值和 = 所有正点权之和 - 我们未选择的正点权之和 + 我们选择了的负点权之和
= 所有正点权之和 - (我们未选择的正点权之和 - 我们选择了的负点权之和)
= 所有正点权之和 - (我们未选择的正点权之和 + 我们选择了的负点权的绝对值之和)
= 所有正点权之和 - 割的容量
由于正点权之和是一定的,要让权值和最大,那么就要最小化割的容量,也就是最小割。最后再由最大流最小割定理,就可以得到最大权值和 = 所有正点权之和 - 最大流。
3.5 最大密度子图
3.5.1 问题概述
定义一个无向图
现在要求解一个无向图
3.5.2 解法
首先我们将
我们二分密度
我们不难发现一件事,就是对于一条边
然后我们在看上面的式子,它告诉我们选一条边的代价是
此时我们发现问题转化为,在新图上选出一个子图,满足每个点及其后继都要选,求出子图点权之和的最大值。我们惊奇的发现这就是最大权闭合子图问题,按照 3.4 讲解的内容直接求解即可。
3.6 平面图和对偶图
3.6.1 平面图
对于一张图
例:显然网格图就是一张平面图。
对于平面图
当然上面这些定义都是枯燥的,它真正的用途还要和下面的东西有关。
3.6.2 对偶图
对偶图是和平面图有紧密联系的一个东西。
设
- 在
的每个面 中放置 的一个顶点 。 - 设
是 的一条边,若 在 的面 和 的公共边界上,做 的一条边 与 相交,且 的两个端点是 ;若 是一座桥(即它只在一个面 )上,那么做 为 的自环。同时 的边权应与 相同。
将图
如下图所示是一个将平面图转化为对偶图的例子:
3.6.3 平面图最小割
事实上,它被放入最小割当中就是因为它和最小割还有一丝联系。
考虑一张
考虑这反映到对偶图上是什么,我们将图中一些边割掉,而这些边在对偶图上则恰好是一条连续的、从
自然地,最小割就可以转化为对偶图上的最短路问题。
那么接下来我们看一道例题:[ICPC-Beijing 2006] 狼抓兔子。显然这道题是一个裸的最小割,然而点数
由于这道题是一个网格图,所以网格图建边还算简单,代码如下:
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 9e6 + 5;
const int Inf = 2e9;
int n, m;
int head[Maxn], edgenum = 1;
struct node {
int nxt, to, w;
}edge[Maxn];
void add(int u, int v, int w) {
edge[++edgenum] = {head[u], v, w};
head[u] = edgenum;
edge[++edgenum] = {head[v], u, w};
head[v] = edgenum;
}
int mat(int x, int y, int z) {
return (x - 1) * (m - 1) * 2 + (y - 1) * 2 + z;
}
int s, t;
int dis[Maxn], vis[Maxn];
#define mk make_pair
void dijkstra() {
for(int i = 1; i <= t; i++) {
dis[i] = Inf;
vis[i] = 0;
}
priority_queue <pair<int, int> > q;
q.push(mk(0, s));
dis[s] = 0;
while(!q.empty()) {
int x = q.top().second;
q.pop();
if(vis[x]) continue;
vis[x] = 1;
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to, w = edge[i].w;
if(dis[x] + w < dis[to]) {
dis[to] = dis[x] + w;
q.push(mk(-dis[to], to));
}
}
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
s = mat(n - 1, m - 1, 2) + 1, t = s + 1;
int p;
for(int i = 1; i <= n; i++) {
for(int j = 1; j < m; j++) {
cin >> p;
if(n == 1) add(s, t, p);
else if(i == 1) add(mat(i, j, 1), t, p);
else if(i == n) add(s, mat(i - 1, j, 2), p);
else add(mat(i, j, 1), mat(i - 1, j, 2), p);
}
}
for(int i = 1; i < n; i++) {
for(int j = 1; j <= m; j++) {
cin >> p;
if(m == 1) add(s, t, p);
else if(j == 1) add(s, mat(i, j, 2), p);
else if(j == m) add(mat(i, j - 1, 1), t, p);
else add(mat(i, j - 1, 1), mat(i, j, 2), p);
}
}
for(int i = 1; i < n; i++) {
for(int j = 1; j < m; j++) {
cin >> p;
add(mat(i, j, 2), mat(i, j, 1), p);
}
}
dijkstra();
cout << dis[t];
return 0;
}
4 费用流
4.1 问题概述
给定网络
当一条边的流量为
现在在满足最大流的情况下,求出总花费最少的流,这就是最小费用最大流问题;当然如果在最大流的情况下求出总花费最大的流,就是最大费用最大流问题,我们将他们统称为费用流问题。
4.2 SPFA 费用流
首先让我们回到最大流的场景,FF 算法的思路就是不断找增广路进行增广。
接下来我们考虑,对于两条可增广流量相同的两条增广路,我们一定是选费用之和较小的那一个。也就是说,对于费用和较小的增广路,我们是希望它的流量更大的,因此要先走。
所以我们考虑贪心,每次找增广路的时候找费用之和最短的一条,然后对于这条增广路进行增广。首先我们运用的还是基础的 FF 增广,因此反向边必不可少,于是费用的反向边也需要建立。
那么此时我们要在费用的图上求源点到汇点的最短路,由于反向的费用是负的,因此我们需要用到复活的 SPFA。
现在我们再关注一件事,我们上面提到是对这个费用最小的增广路增广,因此不难想到 EK 算法。于是最小费用最大流的一种简单的解决方式就是 EK + SPFA。这里只需要将 EK 中的 BFS 换成 SPFA 即可。
众所周知 SPFA 复杂度是
当然这也是一个理论上界,实际运用中,同样几乎没有人卡 SPFA 费用流。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;
int n, m, s, t;
int head[Maxn], edgenum = 1;
struct node {
int nxt, to, w, c;
}edge[Maxn];
void add(int u, int v, int w, int c) {
edge[++edgenum] = {head[u], v, w, c};
head[u] = edgenum;
edge[++edgenum] = {head[v], u, 0, -c};
head[v] = edgenum;
}
int dis[Maxn], pre[Maxn], minw[Maxn];
//费用之和;增广路上每一个点上一条边的编号;剩余容量最小值
bool vis[Maxn];
bool SPFA() { //普通 SPFA
for(int i = 1; i <= n; i++) {
dis[i] = Inf;
vis[i] = 0;
}
queue <int> q;
q.push(s);
dis[s] = 0;
minw[s] = Inf;
vis[s] = 1;
while(!q.empty()) {
int x = q.front();
q.pop();
vis[x] = 0;
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to, w = edge[i].w;
if(w > 0 && dis[x] + edge[i].c < dis[to]) {
dis[to] = dis[x] + edge[i].c;
minw[to] = min(minw[x], w);
pre[to] = i;
if(!vis[to]) {
vis[to] = 1;
q.push(to);
}
}
}
}
return dis[t] < Inf;//说明无法到达 t 了
}
int mf, mc;//最大流量和最小费用
void EK() {
while(SPFA()) {
mf += minw[t];
mc += minw[t] * dis[t];
int i;
for(int x = t; x != s; x = edge[i ^ 1].to) {
i = pre[x];
edge[i].w -= minw[t];
edge[i ^ 1].w += minw[t];
}
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> s >> t;
for(int i = 1; i <= m; i++) {
int u, v, w, c;
cin >> u >> v >> w >> c;
add(u, v, w, c);
}
EK();
cout << mf << " " << mc << '\n';
return 0;
}
那么还有最大费用最大流,直接将 SPFA 改为求最长路的即可(这也是用 SPFA 的另一个好处,dijkstra 没法跑最长路)。
5 上下界网络流
5.1 总述
上下界网络流指的是对于一个网络,其流量不再是只有上界
5.2 无源汇上下界可行流
5.2.1 问题概述
给定一个有上下界,没有源点汇点的网络
5.2.2 解法
不妨假设此时我们每一条边都已经流了
此时显然我们会有不满足流量守恒的点,那么我们就尝试调整,考虑下界必须保证,那我们在新图中通过增减流量来抵消下界满流后产生的流量不守恒。我们知道,网络流中流入的流量都来自
- 若
,不做处理。 - 若
,表示此时原先的流入流量过大,需要在新图上增加流出量。为了增加流出量,我们就需要增加流入量。所以从超源向当前节点连边,容量为 。这样当新图中流量平衡时,除去加上的这条边,新图上流出量就大于了流入量 ,与原先下界满流相差的正好抵消。 - 若
,表示此时原先的流出流量过大。同上我们可以得到,此时应该从这个点向超汇连容量为 的边。
根据上面分析可以知道,只有当所有附加的边都跑满流的时候才能够与下界满流的差相抵消,达到流量平衡。因此我们在新图中跑最大流,如果
模板题:无源汇有上下界可行流,代码如下:
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;
int n, m, l[Maxn], r[Maxn], fl[Maxn];
int head[Maxn], edgenum = 1;
struct node {
int nxt, to, w;
}edge[Maxn];
int s, t;
void add(int u, int v, int w) {
edge[++edgenum] = {head[u], v, w};
head[u] = edgenum;
edge[++edgenum] = {head[v], u, 0};
head[v] = edgenum;
}
int dis[Maxn], cur[Maxn];
bool bfs() {
for(int i = 1; i <= t; i++) {
dis[i] = 0;
cur[i] = head[i];
}
queue <int> q;
q.push(s);
dis[s] = 1;
while(!q.empty()) {
int x = q.front();
q.pop();
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to, w = edge[i].w;
if(w > 0 && !dis[to]) {
dis[to] = dis[x] + 1;
if(to == t) return 1;
q.push(to);
}
}
}
return 0;
}
int dfs(int x, int flow) {
if(x == t) return flow;
int rest = flow;
for(int i = cur[x]; i && rest; i = edge[i].nxt) {
cur[x] = i;
int to = edge[i].to, w = edge[i].w;
if(w > 0 && dis[to] == dis[x] + 1) {
int k = dfs(to, min(rest, w));
rest -= k;
edge[i].w -= k;
edge[i ^ 1].w += k;
}
}
return flow - rest;
}
int ans = 0;
void dinic() {
while(bfs()) {
ans -= dfs(s, Inf);
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v >> l[i] >> r[i];
add(u, v, r[i] - l[i]);//新图
fl[u] -= l[i], fl[v] += l[i];//记录每个点的 M
}
s = n + 1, t = s + 1;
for(int i = 1; i <= n; i++) {
if(fl[i] > 0) {
add(s, i, fl[i]);
ans += fl[i];//看从 S 出发的边能否流满
}
else if(fl[i] < 0){
add(i, t, -fl[i]);
}
}
dinic();
if(ans != 0) {
cout << "NO\n";
}
else {
cout << "YES\n";
for(int i = 1; i <= m; i++) {
cout << l[i] + edge[(i << 1) ^ 1].w << '\n';
}
}
return 0;
}
5.3 有源汇上下界可行流
5.3.1 问题概述
给定一个有上下界的网络
5.3.2 解法
我们考虑沿用无源汇上下界可行流的做法,但是此时
5.4 有源汇上下界最大流
5.4.1 问题概述
给定一个有上下界的网络
5.4.2 解法
我们先跑一遍有源汇上下界可行流,如果没有可行流就直接无解。
接下来我们关注这个新图上的残量网络,此时超源和超汇连的边应该已经流满,但是最开始图中源点和汇点之间的边显然还可能没有流满。那么我们此时再在原图的残量网络中调整,让他从可行流变成最大流。具体的,我们删去多余的附加边,然后直接在残量网络上求出原先的从源点到汇点的最大流,将可行流流量加上这个最大流流量就是答案。
模板题:有源汇有上下界最大流,代码如下:
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;
int n, m;
int fl[Maxn];
int head[Maxn], edgenum = 1;
struct node {
int nxt, to, w;
}edge[Maxn];
void add(int u, int v, int w) {
edge[++edgenum] = {head[u], v, w};
head[u] = edgenum;
edge[++edgenum] = {head[v], u, 0};
head[v] = edgenum;
}
int S, T, s, t;
int dis[Maxn], cur[Maxn];
bool bfs() {
for(int i = 1; i <= T; i++) {
dis[i] = 0;
cur[i] = head[i];
}
queue <int> q;
q.push(S);
dis[S] = 1;
while(!q.empty()) {
int x = q.front();
q.pop();
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to, w = edge[i].w;
if(w > 0 && !dis[to]) {
dis[to] = dis[x] + 1;
if(to == T) return 1;
q.push(to);
}
}
}
return 0;
}
int dfs(int x, int flow) {
if(x == T) return flow;
int rest = flow;
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to, w = edge[i].w;
if(w > 0 && dis[to] == dis[x] + 1) {
int k = dfs(to, min(rest, w));
rest -= k;
edge[i].w -= k;
edge[i ^ 1].w += k;
}
}
return flow - rest;
}
int dinic() {
int res = 0;
while(bfs()) {
res += dfs(S, Inf);
}
return res;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> s >> t;
for(int i = 1; i <= m; i++) {
int u, v, l, r;
cin >> u >> v >> l >> r;
fl[u] -= l, fl[v] += l;
add(u, v, r - l);
}
S = n + 1, T = n + 2;
add(t, s, Inf);//先跑可行流
int cnt = 0;
for(int i = 1; i <= n; i++) {
if(fl[i] > 0) {
add(S, i, fl[i]);
cnt += fl[i];
}
else if(fl[i] < 0){
add(i, T, -fl[i]);
}
}
int flow = dinic();
if(flow != cnt) {
cout << "please go home to sleep";
return 0;
}
flow = edge[((m + 1) << 1) + 1].w;//t -> s 边上的流量就是可行流流量
edge[((m + 1) << 1) + 1].w = edge[((m + 1) << 1)].w = 0;//注意要清空
S = s, T = t;
int ans = dinic();//在跑一遍最大流,将剩余容量榨干
cout << ans + flow;
return 0;
}
5.5 有源汇上下界最小流
5.5.1 问题概述
给定一个有上下界的网络
5.5.2 解法
在一般的网络中,没有最小流问题,因为显然是
那么考虑最小流与最大流本质上还是一致的。在最大流中,我们在残量网络中由
模板题:有源汇有上下界最小流,代码如下:
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 4e5 + 5;
const int Inf = 2e9;
int n, m;
int fl[Maxn];
int head[Maxn], edgenum = 1;
struct node {
int nxt, to, w;
}edge[Maxn];
void add(int u, int v, int w) {
edge[++edgenum] = {head[u], v, w};
head[u] = edgenum;
edge[++edgenum] = {head[v], u, 0};
head[v] = edgenum;
}
int S, T, s, t;
int dis[Maxn], cur[Maxn];
bool bfs() {
for(int i = 1; i <= T; i++) {
dis[i] = 0;
cur[i] = head[i];
}
queue <int> q;
q.push(S);
dis[S] = 1;
while(!q.empty()) {
int x = q.front();
q.pop();
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to, w = edge[i].w;
if(w > 0 && !dis[to]) {
dis[to] = dis[x] + 1;
if(to == T) return 1;
q.push(to);
}
}
}
return 0;
}
int dfs(int x, int flow) {
if(x == T) return flow;
int rest = flow;
for(int i = cur[x]; i && rest; i = edge[i].nxt) {
cur[x] = i;
int to = edge[i].to, w = edge[i].w;
if(w > 0 && dis[to] == dis[x] + 1) {
int k = dfs(to, min(rest, w));
rest -= k;
edge[i].w -= k;
edge[i ^ 1].w += k;
}
}
return flow - rest;
}
int dinic() {
int res = 0;
while(bfs()) {
res += dfs(S, Inf);
}
return res;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> s >> t;
for(int i = 1; i <= m; i++) {
int u, v, l, r;
cin >> u >> v >> l >> r;
fl[u] -= l, fl[v] += l;
add(u, v, r - l);
}
S = n + 1, T = n + 2;
add(t, s, Inf);
int cnt = 0;
for(int i = 1; i <= n; i++) {
if(fl[i] > 0) {
add(S, i, fl[i]);
cnt += fl[i];
}
else if(fl[i] < 0){
add(i, T, -fl[i]);
}
}
int flow = dinic();
if(flow != cnt) {
cout << "please go home to sleep";
return 0;
}
flow = edge[((m + 1) << 1) + 1].w;
edge[((m + 1) << 1) + 1].w = edge[((m + 1) << 1)].w = 0;
S = t, T = s;//唯一的区别
int ans = dinic();
cout << flow - ans;
return 0;
}
5.6 有源汇上下界最小费用可行流
5.6.1 问题概述
给定一个有上下界,有费用的网络
5.6.2 解法
显然我们可以先转化为有源汇上下界可行流,只是在建
然后我们在新图上跑最小费用最大流,最后再加上跑满下界需要的费用就是答案。
例题:[AHOI2014/JSOI2014] 支线剧情,解法及代码如下:
对于每一条
的路径设上界为 ,下界为 ,费用为时间。这样可以保证每条边走一遍。 然后对于每个点,向
连一条边,上界为 ,下界为 。这样可以算出重复看剧情的数量。 最后我们直接跑有源汇上下界最小费用可行流即可。
代码:
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 2e6 + 5;
const int Inf = 2e9;
int n;
int fl[Maxn];
int head[Maxn], edgenum = 1;
struct node {
int nxt, to, w, c;
}edge[Maxn];
void add(int u, int v, int w, int c) {
edge[++edgenum] = {head[u], v, w, c};
head[u] = edgenum;
edge[++edgenum] = {head[v], u, 0, -c};
head[v] = edgenum;
}
int S, T, s, t;
int dis[Maxn], minw[Maxn], pre[Maxn];
bool vis[Maxn];
bool SPFA() {
for(int i = 1; i <= T; i++) {
dis[i] = Inf;
vis[i] = 0;
}
queue <int> q;
q.push(S);
dis[S] = 0;
minw[S] = Inf;
vis[S] = 1;
while(!q.empty()) {
int x = q.front();
q.pop();
vis[x] = 0;
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to, w = edge[i].w, c = edge[i].c;
if(w > 0 && dis[x] + c < dis[to]) {
dis[to] = dis[x] + c;
pre[to] = i;
minw[to] = min(minw[x], w);
if(!vis[to]) {
vis[to] = 1;
q.push(to);
}
}
}
}
return dis[T] != Inf;
}
int ans = 0;
int EK() {
int mc = 0;
while(SPFA()) {
mc += minw[T] * dis[T];
int i;
for(int x = T; x != S; x = edge[i ^ 1].to) {
i = pre[x];
edge[i].w -= minw[T];
edge[i ^ 1].w += minw[T];
}
}
return mc;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
s = 1, t = n + 1;
for(int i = 1; i <= n; i++) {
int k;
cin >> k
add(i, t, Inf, 0);
for(int j = 1; j <= k; j++) {
int b, t;
cin >> b >> t;
add(i, b, Inf, t);
ans += t;
fl[i]--, fl[b]++;
}
}
add(t, s, Inf, 0);
S = t + 1, T = S + 1;
for(int i = 1; i <= t; i++) {
if(fl[i] > 0) {
add(S, i, fl[i], 0);
}
else {
add(i, T, -fl[i], 0);
}
}
ans += EK();
cout << ans;
return 0;
}
5.7 有源汇上下界最小费用最大流
5.7.1 问题概述
给定一个有上下界,有费用的图
5.7.2 解法
按照有源汇上下界最大流的做法,我们在求出最小费用可行流之后还要将剩余容量榨干。那我们直接在残量网络上从
Tips:显然所有有源汇的上下界网络流都可以转化为无源汇进行求解。
附 网络流建模技巧
F.1 基础技巧
- 多源多汇时建立超级源点和超级汇点。
F.2 拆点
- 对于权值在点上的题目,将每个点拆成入点和出点。
- 如果整张图有时间上的限制,考虑把每个点按照时间拆点。也就是把一个点拆成在不同时刻的点。
- 如果每一个点有不同的状态,将每一个状态拆成一个点。(其实时间也可以理解成一个点的不同状态)
F.3 网格类问题
- 对于网格问题,如果行列上有限制,考虑将一个点
从 到 连边。 - 对于网格问题,如果点之间会互相影响,考虑黑白染色。然后从黑点向白点连边(准确来讲并不只限制于黑白两种颜色,其实可以是红黄蓝绿等等,染色的主要目的在于让原先颜色间互相影响变为人为规定的单向影响,方便网络流连边)。
F.4 最小割类问题
- 如果出现类似二者选其一的模型,考虑将两者分别建立源点和汇点,然后求出最小割就可以将所有元素分成两部分。
- 在上一条的基础上,如果两个元素都选 / 都放在一边才能产生贡献,我们称这种结构为与结构。此时可以新建一个节点,从源(汇)点向这个点连容量为贡献的边,然后从这个点向那两个元素连容量为
的边。这样跑最小割时就可以满足刚才说到的条件。 - 求解一条边能否在最小割和是否一定在最小割中的问题时,考虑在最终的残量网络上用 tarjan 求解。
F.4.1 切糕模型
有必要单独拿出来讲。该模型就是因 [HNOI2013] 切糕 考察而得名,基本模型如下:
题意:给出
个变量 ,其中每个变量的取值范围是 。对于一个变量 ,取 的代价为 。同时,给出若干限制条件 ,表示 (或 )。 请求出一种合法的赋值方案,使得代价总和最小。
首先我们考虑拆点,将每一个变量拆成
现在我们考虑连边,首先要建出
这样我们断掉
我们以
但是此时我们还可能出现这样的 bug:我们在一条链上割掉了两条边,这样显然是不符合题意的。考虑进行这样一步操作:我们再连上
F.5 费用流类问题
F.5.1 动态加点
准确来讲这并不是一种建模思想,而是优化。
有这样一类题目,它们的数据范围比正常的题目要大,因而正常的网络流跑不过去。但是这种题目还有一个特征,那就是可以把点一个一个加入,对于最后结果没有影响。
既然这样,我们就可以在每次增广结束之后再加入需要的点,可以让整体的时间复杂度更低,达到优化的目的。
使用这种优化的注意事项:
- 这种优化大多见于费用流,因为普通的最大流可以使用当前弧优化,而大多数人的费用流都是朴素的 SPFA + EK / Dinic。
- 显然需要满足我们说的“可以把点一个一个加入,对于最后结果没有影响”,具体来讲,我们普通的网络流是利用反向边来实现撤销,然而你要是真的一个一个加点没有办法解决后面的点与前面的点的冲突。然而,如果能够保证后面加进来的点一定不比前面的点优,就不用考虑这个冲突,自然满足上面的条件。
F.5.2 解线性规划问题
这是一种非常暴力的建图方式,只需要你会代数推导并且题目符合一定条件,就一定能做出来。
首先是线性规划问题,我们有如下定义:
已知一组实数
和一组变量 有函数 。显然这个函数是线性的。如果 是实数且要满足 ,则称之为线性等式。相似的,还有线性不等式。两者统称线性约束。 线性规划就是一个线性函数的极值问题,而这个线性函数需要满足一定的线性约束。
那么考虑先将所有线性约束转化为线性等式(转化可以通过添加非负的变量来完成)。然后假设得到了
接下来求出的差式需要满足这样的性质:每个未知数只在两个等式中出现且一正一负。而我们要求的是线性函数的极值,自然想到费用流,然后我们就可以开始建图了:
- 每个等式代表图中的一个顶点,同时添加源点
和汇点 。 - 如果第
个等式右边是非负整数 ,连边 。否则连边 。 - 如果一个变量
在第 个等式中出现为 ,在第 个等式中出现为 ,同时在目标线性函数中的系数为 ,那么连边 。
理解一下上述的建边会发现,我们本质上就是在通过网路流的流量守恒达到等式的成立。我们可以认为是将所有项移到左边,这样正数之和就等于负数之和。容易发现,在上面的建图中,流向
F.5.3 平方费用费用流
我们正常的费用流都是规定走过一条边的费用是
。那如果现在定义走过一条边的费用为 ,就是平方费用费用流问题。
容易发现平方费用的难点就在于这个平方,于是我们就想到将平方转化为线性的费用然后用朴素费用流求解。
如何转化?我们考虑这样一种方式:假如我们在考虑
考虑正确性,由于费用流增广的是最短路径,所以费用小的一定选在大的之前,换句话说,我们如果要走这些边,最后走过了的边一定是从第一条边开始连续的一段。
假如我们原先要走
F.6 其他
- 如果图不完整,考虑二分容量限制。
- 当元素可以按照奇偶 / 特征值等拆成两部分时,考虑二分图,只从一部点向另一部点连边。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律