您的位置:首页 > 游戏 > 游戏 > 【图论】网络流

【图论】网络流

2024/10/5 21:15:58 来源:https://blog.csdn.net/dhxbshbdjzxy/article/details/140305202  浏览:    关键词:【图论】网络流

算法进阶课网络流部分总结

文章目录

  • 基本概念
    • 流网络
    • 可行流
    • 最大流
    • 残留网络
    • 增广路径
    • 最小割
    • 最大流最小割定理
  • 算法
    • EK算法
    • Dinic算法
  • 最大流模型
    • 基本思想
    • 二分图匹配
      • P2756 飞行员配对方案问题
      • P3254 圆桌问题
    • 上下界可行流
      • 无源汇上下界可行流
      • 有源汇上下界可行流
      • 有源汇上下界最大流
      • 有源汇上下界最小流
    • 多源汇最大流
    • 关键边
    • 最大流判定
      • P1674 [USACO05FEB] Secret Milking Machine G (二分)
      • P2754 [CTSC1999] 家园 / 星际转移问题(分层图)
    • 拆点
      • P2891 [USACO07OPEN] Dining G
      • P2766 最长不下降子序列问题
  • 最小割模型
    • 直接应用
      • 网络战争(01分数规划)
      • 最优标号
    • 最大权闭合子图
      • P4174 [NOI2006] 最大获利
    • 最大密度子图
  • 费用流模型
    • 定义
    • 求法 - 基于EK算法
    • 模板 - zkw费用流

基本概念

流网络

由点集和边集组成的有向图,其中包含一个源点 s 和一个汇点 t,边上的权值表示

可以将其理解为水流和管道,从源点流出水流,流入汇点,边上的权值表示管道可以流过的水量
在这里插入图片描述

可行流

满足以下条件的叫做可行流:

  • 容量限制:一条边上流过的流量不能超过其最大限制,即 0 ≤ f ( u , v ) ≤ c ( u , v ) 0\le f(u, v)\le c(u, v) 0f(u,v)c(u,v)
  • 流量守恒:流入每个点的流量等于流出该点的流量,即 ∑ f ( v , x ) = ∑ f ( x , v ) \sum{f(v, x)}=\sum{f(x, v)} f(v,x)=f(x,v)

∣ f ∣ |f| f:从源点流向汇点的流量值,有 ∣ f ∣ = f ( s , v ) − f ( v , s ) |f|=f(s,v) -f(v,s) f=f(s,v)f(v,s)

最大流

最大流,全称最大可行流,即流量值最大的可行流

残留网络

残留网络 G f G_f Gf 是针对某一条可行流而言的,可行流不同,残留网络也不同

残留网络中:

  • 点集和原图里所有边一样
  • 边集:
    • 包含原来的所有边,这些边的容量定义为原容量减去流量(也就是还可以流过的流量)
    • 包含原边的所有反向边,这些边的容量定义为原来的流量(可以理解为能退回去的流量)

定理

可行流 f f f 的残留网络定义为 f ′ f' f,有这样的关系: f + f ′ f+f' f+f 也是原网络的一个可行流,且流量等于 ∣ f ∣ + ∣ f ′ ∣ |f|+|f'| f+f

其中 f + f ′ f+f' f+f 意为同一条边的流量,方向相同就相加,方向不同就减去

增广路径

增广路径指:在残留网络里,从源点出发,沿着流量大于 0 的边,如果能走到汇点,这条路径称为增广路径

增广路径一定是可行流,流量一定大于 0

也就是在残留网络取一个流量大于 0 的可行流

没有增广路,就可以断定当前的可行流是最大流

定义:对于流网络 G = ( V , E ) G=(V,E) G=(V,E),将点集 V V V 不重不漏分成两个子集 S , T S,T S,T,使得源点 s 在 S S S 中,汇点 t 在 T T T

容量:所有从 S S S 指向 T T T 的边的容量之和 (不算从 T T T 指向 S S S 的边)

流量:所有从 S S S 指向 T T T 的边的流量之和 - 所有从 T T T 指向 S S S 的边的流量之和

f ( X , Y ) f(X,Y) f(X,Y):从 X 流向 Y 的净流量(等于从X流向Y的流量减去从Y流向X的流量)

f ( X , Y ) = ∑ u ∈ X ∑ v ∈ Y f ( u , v ) − ∑ u ∈ X ∑ v ∈ Y f ( v , u ) f(X,Y)=\sum_{u\in X}\sum_{v\in Y}f(u,v)-\sum_{u\in X}\sum_{v\in Y}f(v,u) f(X,Y)=uXvYf(u,v)uXvYf(v,u)

有:

f ( X , Y ) = − f ( Y , X ) f(X,Y)=-f(Y,X) f(X,Y)=f(Y,X)

f ( X , X ) = 0 f(X,X)=0 f(X,X)=0

f ( Z , X ∪ Y ) = f ( Z , X ) + f ( Z , Y ) f(Z, X\cup Y)=f(Z,X)+f(Z,Y) f(Z,XY)=f(Z,X)+f(Z,Y)

性质

  1. 对于任意的割,割的流量 < 割的容量
  2. 对于任意的割和可行流,割的流量 = 可行流的流量
  3. 对于任意的割和可行流,可行流的流量 < 割的容量

最小割

最小割:最小容量

最大流最小割定理

对于某一个流网络 G 来说,以下三个条件可以相互等价:

  • 一个可行流 f f f 是最大可行流
  • 可行流 f f f 的残留网络中不存在增广路
  • 存在一个割 [ S , T ] [S,T] [S,T] 使得可行流的流量等于割的容量

最大流等于最小割

算法

思想:维护残留网络,在残留网络里不断找增广路,将增广路删去,一直重复直到找不到增广路为止

EK算法

时间复杂度: O ( n m 2 ) O(nm^2) O(nm2)

步骤:

  1. 找增广路
  2. 更新残留网络(正向边容量 - 流量,反向边容量 + 流量)

c o d e code code

int n, m, S, T;
int h[N], e[M], f[M], ne[M], idx;
int d[N], pre[N]; // d[i]:从起点走到某个点的这条路径上的容量最小值 pre[i]:一条增广路径的前缀边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()
{queue<int> q;vector<bool> st(n + 1);q.push(S), st[S] = true, d[S] = INF;while (q.size()){int t = q.front();q.pop();for (int i = h[t]; i != -1; i = ne[i]){int ver = e[i];if (!st[ver] && f[i]) // ver没访问过且容量剩余{st[ver] = true;d[ver] = min(d[t], f[i]);pre[ver] = i;if (ver == T) return true;q.push(ver);}}}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;
}void solve()
{cin >> n >> m >> S >> T;for (int i = 1; i <= n; i ++ ) h[i] = -1;while (m -- ){int a, b, c;cin >> a >> b >> c;add(a, b, c);}cout << EK() << '\n';
}

Dinic算法

时间复杂度: O ( n 2 m ) O(n^2m) O(n2m)

EK算法是每次只增广一条路径,而Dinic算法是每次增广尽可能多的路径

通过bfs完成建图和分层两个操作,然后dfs直到没有增广路为止

优化:

  • 当前弧优化:使用 cur 数组,标记每个结点当前遍历到哪条边了,之前遍历过的已经用完了,所以从 cur[i] 开始遍历,防止重复

c o d e code code

int n, m, S, T;
int h[N], e[M], f[M], ne[M], idx;
int d[N], pre[N], cur[N]; // d[i]:每个点的层数,源点为0 pre[i]:一条增广路径的前缀边 cur[i]:当前弧优化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()
{queue<int> q;for (int i = 1; i <= n; i ++ ) d[i] = -1;q.push(S), d[S] = 0, cur[S] = h[S]; // 初始化while (q.size()){auto t = q.front();q.pop();for (int i = h[t]; i != -1; 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.push(ver);}}}return false; // 没有增广路
}int find(int u, int limit) // u是当前点 limit是流入这个点的流量 函数返回从这个点最多能流出的流量
{if (u == T) return limit;int flow = 0; // 从当前点流出的最大流量for (int i = cur[u]; i != -1 && flow < limit; i = ne[i]){cur[u] = i; // 当前弧优化 能访问到第i条边说明第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; // 没有流量流出 说明这个点已经废了else 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;
}void solve()
{cin >> n >> m >> S >> T;for (int i = 1; i <= n; i ++ ) h[i] = -1;while (m -- ){int a, b, c;cin >> a >> b >> c;add(a, b, c);}cout << dinic() << '\n';
}

最大流模型

基本思想

问题 P P P 的所有解决方案为集合 S S S,最大流的所有可行流为集合 f f f S S S f f f 必须是一一对应的关系

二分图匹配

P2756 飞行员配对方案问题

题目链接

题意是有两堆点,特定的两点之间可以进行匹配,问最大匹配数

很显然是个二分图匹配问题,可以用匈牙利算法解决,但利用dinic可以将复杂度降到 O ( n m ) O(n\sqrt{m}) O(nm )

我们将源点连向第一堆点,将第二堆点连向汇点,合法的匹配方案即转化为两堆点之间的连边,容量均为1,求最大流即可

void solve()
{cin >> m >> n;S = n + 1, T = n + 2;for (int i = 1; i <= n + 2; i ++ ) h[i] = -1;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;cin >> a >> b;while (a != -1 && b != -1){add(a, b, 1);cin >> a >> b;}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';}}
}

P3254 圆桌问题

题目链接

题意是,有 m 个单位和 n 张桌子,告诉你每个单位的人数和每张桌子的人数,同一个单位的人不能坐同一张桌子,问是否有合法分配方案

又是匹配问题,我们将 m 个单位和 n 张桌子分别看成两堆点

  • 单位 a a a 向桌子 b b b 连一条容量为 1 的边意为 a a a 单位派了一个人去桌子 b b b
  • 源点向单位 a a a 连一条容量为 c c c 的边表示单位 a a a 派了 c c c 个人参加
  • 桌子 b b b 向汇点连了一条容量为 c c c 的边表示桌子 b b b 能坐 c c c 个人

求得的最大流就是参加的人数,如果等于总人数的话,说明有这样一种匹配方案

void solve()
{cin >> m >> n;for (int i = 1; i <= n + m + 2; i ++ ) h[i] = -1;S = n + m + 1, T = n + m + 2;vector<int> r(m + 1), c(n + 1);int sum = 0;for (int i = 1; i <= m; i ++ ){cin >> r[i];sum += r[i];}for (int i = 1; i <= n; i ++ ) cin >> c[i];for (int i = 1; i <= m; i ++ ) add(S, i, r[i]);for (int i = m + 1; i <= n + m; i ++ ) add(i, T, c[i - m]);for (int i = 1; i <= m; i ++ ){for (int j = m + 1; j <= n + m; j ++ ){add(i, j, 1);}}if (dinic() != sum){cout << 0 << '\n';return;}cout << 1 << '\n';for (int i = 1; i <= m; i ++ ){for (int j = h[i]; j != -1; j = ne[j]){if (f[j]) continue;int v = e[j];cout << v - m << ' ';}cout << '\n';}
}

上下界可行流

无源汇上下界可行流

给每条边规定的容量限制为 c l ( u , v ) ≤ f ( u , v ) ≤ c u ( u , v ) c_l(u,v)\le f(u,v)\le c_u(u, v) cl(u,v)f(u,v)cu(u,v)

怎么对其进行转换,使得能满足 f ( u , v ) ≤ c f(u,v)\le c f(u,v)c 的形式呢?

很显然,同减 c l ( u , v ) c_l(u,v) cl(u,v) 就可以了

于是限制变成 0 ≤ f ( u , v ) − c l ( u , v ) ≤ c u ( u , v ) − c l ( u , v ) 0\le f(u,v)-c_l(u,v)\le c_u(u,v)-c_l(u,v) 0f(u,v)cl(u,v)cu(u,v)cl(u,v)

也就是,我们将每一条边的流量减去 c l ( u , v ) c_l(u,v) cl(u,v),容量变为上界减下界,这显然是满足容量限制的

那满不满足流量守恒呢,不一定

设少进入点 x x x 的流量 c 入 c_{入} c,少流出点 x x x 的流量 c 出 c_{出} c,点 x x x 就少流入了 c 入 − c 出 c_{入}-c_{出} cc,怎么处理这个少流入的流量呢,我们连一条边从源点到 x x x,流量是 c 入 − c 出 c_{入}-c_{出} cc 就可以了(如果少流出了,就连一条 x x x 到汇点的边,道理是一样的)

模板题链接

void add(int a, int b, int c, int d)
{e[idx] = b, f[idx] = d - c, l[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
}void solve()
{cin >> n >> m;S = n + 1, T = n + 2;for (int i = 1; i <= n + 2; i ++ ) h[i] = -1;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";return;}cout << "YES\n";for (int i = 0; i < m * 2; i += 2){cout << f[i ^ 1] + l[i] << '\n';}
}

有源汇上下界可行流

有源汇和无源汇的区别就是,无源汇流网络中,所有点都是满足流量守恒的,而有源汇流网络中,源点和汇点是不满足流量守恒的

于是我们可以将有源汇流网络变成无源汇流网络,也就是让源点和汇点也满足流量守恒,之后根据无源汇上下界可行流的方法去做

怎么让源点和汇点也满足流量守恒?直接连一条从汇点指向源点的边,容量为 I N F INF INF 即可

有源汇上下界最大流

模板题链接

我们先将问题转换成有源汇上下界最大流,这个时候,我们已经在原图的基础上,进行了以下操作(设原图中给定的源点和汇点分别用 s s s t t t 表示):

  • 连了一条由 t t t 指向 s s s 的边,容量为 I N F INF INF
  • 创建虚拟源点 S S S,弥补每个点少流入的流量
  • 创建虚拟源点 T T T,处理每个点多流出的流量

先跑一遍dinic,通过看从 S S S 点流出的流量是否等于 T T T 点流出的流量来判断是否存在这样的上下界可行流,如果不存在这样的方案就不用谈什么最大流了

此时,由于虚拟源点和虚拟汇点的特殊作用,与它们相连的边都是满流,不可能再被用到,也就是说不可能对之后的最大流造成任何贡献,所以我们直接把这些边删去(可以直接将 S S S T T T 更新为原图的 s s s t t t),同时,由 t t t 指向 s s s 的那条边也是满流,直接删去(可以将 f[idx - 1]f[idx - 2] 直接置为 0),之后再跑一遍最大流,两次叠加的结果即为最终的最大流

void solve()
{int s, t;cin >> n >> m >> s >> t;S = n + 1, T = n + 2;for (int i = 1; i <= n + 2; i ++ ) h[i] = -1;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]);}add(t, s, 0, INF);if (dinic() != tot){cout << "please go home to sleep\n";return;}int res = f[idx - 1]; // 原图中s到t的最大流S = s, T = t;f[idx - 2] = f[idx - 1] = 0; // 把新加的从t指向s的边删掉cout << res + dinic() << '\n';
}

有源汇上下界最小流

反过来想,因为从源点到汇点的最大流等于从汇点到源点的流量的相反数,所以想求最小流,只需要求汇点到源点的最大流就可以了

所以一开始还是按之前的方式创建虚拟源点汇点求可行流,第二次dinic的时候将 t t t 标记为源点, s s s 标记为汇点,答案是第一次的流量减第二次的流量

模板题链接

void solve()
{int s, t;cin >> n >> m >> s >> t;S = n + 1, T = n + 2;for (int i = 1; i <= n + 2; i ++ ) h[i] = -1;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]);}add(t, s, 0, INF);if (dinic() != tot){cout << "please go home to sleep\n";return;}int res = f[idx - 1]; // 原图中s到t的最大流S = t, T = s;f[idx - 2] = f[idx - 1] = 0; // 把新加的从t指向s的边删掉cout << res - dinic() << '\n';
}

多源汇最大流

很显然是虚拟源点和虚拟汇点的思路

void solve()
{int sc, tc;cin >> n >> m >> sc >> tc;S = n + 1, T = n + 2;for (int i = 1; i <= n + 2; i ++ ) h[i] = -1;for (int i = 0; i < sc; i ++ ) // 将虚拟源点和原题源点连边{int x; cin >> x;add(S, x, INF);}for (int i = 0; i < tc; i ++ ) // 将虚拟汇点和原题汇点连边{int x; cin >> x;add(x, T, INF);}for (int i = 0; i < m; i ++ ){int a, b, c;cin >> a >> b >> c;add(a, b, c);}cout << dinic() << '\n';
}

关键边

题目链接

题意是给你一个 n n n 个点, m m m 条边的流网络,每条边有最大容量限制,问能不能增加一条边的容量使得最大流增加,输出这样的边有多少条

画图即可知道,这样的边有个条件就是一定是流量等于容量,如果流量小于容量,说明在前面的边里对其有流量限制,尽管加这条边的容量也没办法让它多流

另外如果没有多余流量可以流的话,光加容量也没用,所以判断一下源点和汇点能否到达当前边的两个端点,能的话当前边即为符合条件的边

bool st_s[N], st_t[N];void dfs_r(int u, bool st[], int t)
{st[u] = true;for (int i = h[u]; i != -1; i = ne[i]){int j = i ^ t;int v = e[i];if (f[j] && !st[v]) dfs_r(v, st, t);}
}void solve()
{cin >> n >> m;S = 0, T = n - 1;for (int i = 0; i <= n - 1; i ++ ) h[i] = -1;for (int i = 0; i < m; i ++ ){int a, b, c;cin >> a >> b >> c;add(a, b, c);}dinic();dfs_r(S, st_s, 0);dfs_r(T, st_t, 1);int ans = 0;for (int i = 0; i < m * 2; i += 2){int v1 = e[i], v2 = e[i ^ 1];if (!f[i] && st_s[v2] && st_t[v1]) ans ++ ;}cout << ans << '\n';
}

最大流判定

P1674 [USACO05FEB] Secret Milking Machine G (二分)

题目链接

题意是给出一个流网络,判断从 1 号点到 n 号点是否存在 t 条相互之间没有公共边的可行流,输出最长道路的最小长度

首先,如果我们不看最长道路的最小长度这个限制,如何计算从 1 号点到 n 号点存在多少条没有公共边的可行流呢?只需要根据所给条件建图,然后每条边的容量为1就可以了

现在问我们最小长度,很容易发现这个问题是具有二段性的,所以二分去做,符合条件的边容量为 1,不符合的容量为 0

void solve()
{cin >> n >> m >> K;S = 1, T = n;for (int i = 1; i <= n; i ++ ) h[i] = -1;for (int i = 0; i < m; i ++ ){int a, b, c;cin >> a >> b >> c;add(a, b, c);}int l = 0, r = 1e6;auto check = [&](int mid){for (int i = 0; i < idx; i ++ ){if (w[i] <= mid) f[i] = 1;else f[i] = 0;}if (dinic() >= K) return true;else return false;};while (l < r){int mid = l + r >> 1;if (check(mid)) r = mid;else l = mid + 1;}cout << r << '\n';
}

P2754 [CTSC1999] 家园 / 星际转移问题(分层图)

题意是给出几个太空站和太空飞船,飞船可以顺次经过一些太空站,每次能载的人数有限,一共有 k k k 个人,问所有人从 0 号点到 n + 1 号点的最少天数

很经典的建图方式,值得学习一下

首先并查集判断起点终点是否连通

n n n 个太空站看做点,太空飞船看做边,对于每一天,我们都建 n + 1 n+1 n+1 个太空站(还要包括终点,所以多1),然后这样建边:

  • 首先,源点 S S S 向第一天的 0 0 0 号点建一条容量为 k k k 的边
  • 每一天的终点 n + 1 n+1 n+1 向汇点 T T T 建一条容量为 I N F INF INF 的边
  • 对于第 i i i 天的第 j j j 个点来说,第 i − 1 i-1 i1 天到达第 j j j 个点的人可以停在这个点不动,所以连一条从第 i − 1 i-1 i1 天的第 j j j 个点向第 i i i 天的第 j j j 个点的边
  • 对于第 i i i 个飞船来说,下一天会到达下一个太空站,所以连一条前一天所到达太空站向下一天所到达太空站的边

之后从小到大枚举天数(这里为什么不使用二分呢?因为天数越少,我们需要的点数越少),判断是否满足条件即可

struct Ship {int h, r, id[30];
} ship[30];int find(int x)
{if (p[x] != x) p[x] = find(p[x]);return p[x];
}int get(int id, int day)
{return day * (n + 2) + id;
}void solve()
{cin >> n >> m >> k;S = N - 2, T = N - 1;for (int i = 0; i < N; i ++ ) h[i] = -1;for (int i = 0; i < 30; i ++ ) p[i] = i;for (int i = 0; i < m; i ++ ){int a, b;cin >> a >> b;ship[i] = {a, b};for (int j = 0; j < b; j ++ ){int id; cin >> id;if (id == -1) id = n + 1;ship[i].id[j] = id;if (j){int x = ship[i].id[j - 1];p[find(x)] = find(id);}}}if (find(0) != find(n + 1)) cout << 0 << '\n';else{add(S, get(0, 0), k);add(get(n + 1, 0), T, INF);int day = 1, res = 0;while (1){add(get(n + 1, day), T, INF);for (int i = 0; i <= n + 1; i ++ ) add(get(i, day - 1), get(i, day), INF);for (int i = 0; i < m; i ++ ){int r = ship[i].r;int a = ship[i].id[(day - 1) % r], b = ship[i].id[day % r];add(get(a, day - 1), get(b, day), ship[i].h);}res += dinic();if (res >= k) break;day ++ ;}cout << day << '\n';}
}

拆点

P2891 [USACO07OPEN] Dining G

题目链接

题意是给出所有牛和每一头牛喜欢的食物和饮料,现在要给每一头牛分配一个食物和一个饮料,问最多有多少匹配

这一题乍一看像是二分图匹配,于是我们仿照二分图的建图方式,源点向所有食物连一条容量为 1 的边,所有饮料向汇点连一条容量为 1 的边,然后根据牛的喜好来连牛和食物、饮料的边

但是这样真的正确吗?我们看下面这张图:
在这里插入图片描述
这张图里最大流是 2,但是我们只有一头牛,为什么会有 2 的流量呢,因为我们两组食物和饮料共用了一头牛,这在题目中是被禁止的,那我们怎么防止这种情况发生呢?

拆点,将每一头牛从一个点拆成两个点(入点和出点),从入点向出点连一条容量为 1 的边,就可以控制每一头牛只匹配一对食物和饮料了

void solve()
{cin >> n >> F >> D;for (int i = 0; i < N; i ++ ) h[i] = -1;S = N - 2, T = N - 1;for (int i = 1; i <= F; i ++ ) add(S, i, 1);for (int i = F + 1; i <= F + D; i ++ ) add(i, T, 1);for (int i = 1; i <= n; i ++ ){int c1, c2; cin >> c1 >> c2;for (int j = 0; j < c1; j ++ ){int x; cin >> x;add(x, F + D + (i - 1) * 2 + 1, 1);}for (int j = 0; j < c2; j ++ ){int x; cin >> x;add(F + D + (i - 1) * 2 + 2, x + F, 1);}add(F + D + (i - 1) * 2 + 1, F + D + (i - 1) * 2 + 2, 1);}cout << dinic() << '\n';
}

P2766 最长不下降子序列问题

题目链接

题意是给出一个序列,回答三个问题:

  1. 最长不下降子序列的元素个数 s s s
  2. 长度为 s s s 且不共用元素的最长不下降子序列个数
  3. 可以多次使用 a 1 a_1 a1 a n a_n an 的最长不下降子序列个数

首先第一个问题,用动态规划解决即可

第二和第三个问题需要建图,每个元素看做一个点,如果 d p [ i ] = d p [ j ] + 1 dp[i]=dp[j]+1 dp[i]=dp[j]+1(表示可以从第 j j j 个点转移到第 i i i 个点),那么就从第 j j j 个点向第 i i i 个点连一条容量是 1 的边

第三个问题,只需要把从源点向第 1 个点连的边和从最后一个点向汇点连的边容量置为 I N F INF INF 即可

怎么处理每个点只能用一次呢?和上一题一样,把每个点拆成入点和出点,连一条容量为 1 的边即可

同时需要注意特判,如果最长不下降子序列的长度为 1,后两个问题直接输出 n n n 即可

void solve()
{cin >> n;for (int i = 0; i < N; i ++ ) h[i] = -1;for (int i = 1; i <= n; i ++ ) cin >> w[i];S = n * 2 + 1, T = n * 2 + 2;int s = 0;for (int i = 1; i <= n; i ++ ){dp[i] = 1;for (int j = 1; j < i; j ++ )if (w[i] >= w[j])dp[i] = max(dp[j] + 1, dp[i]);s = max(s, dp[i]);add(i, i + n, 1);if (dp[i] == 1) add(S, i, 1);}for (int i = 1; i <= n; i ++ ){for (int j = 1; j < i; j ++ )if (w[i] >= w[j] && dp[i] == dp[j] + 1) add(j + n, i, 1);if (dp[i] == s) add(i + n, T, 1);}cout << s << '\n';if (s == 1) cout << n << '\n' << n << '\n';else{int res = dinic();cout << res << '\n';for (int i = 0; i < idx; i += 2){int a = e[i ^ 1], b = e[i];if (a == S && b == 1) f[i] = INF;else if (a == 1 && b == 1 + n) f[i] = INF;else if (a == n && b == n + n) f[i] = INF;else if (a == n + n && b == T) f[i] = INF;}cout << res + dinic() << '\n';}
}

最小割模型

直接应用

网络战争(01分数规划)

题目给出一个 n n n 个点 m m m 条边的带权无向图,求将源点 S S S 和汇点 T T T 分开的边割集 C C C,使得 ∑ e ∈ C w e ∣ C ∣ \frac{\sum_{e\in C}{w_e}}{|C|} CeCwe 最小,输出这个最小值

所有求形如 ∑ e ∈ C w e ∣ C ∣ \frac{\sum_{e\in C}{w_e}}{|C|} CeCwe 的最值问题,都可以考虑转化成 01分数规划

∑ e ∈ C w e ∣ C ∣ > λ \frac{\sum_{e\in C}{w_e}}{|C|}>\lambda CeCwe>λ,有 ∑ e ∈ C w e > λ ∣ C ∣ \sum_{e\in C}{w_e}>\lambda |C| eCwe>λC,又可推 ∑ e ∈ C w e − λ ∣ C ∣ > 0 \sum_{e\in C}{w_e}-\lambda |C|>0 eCweλC>0,由于 ∑ e ∈ C w e \sum_{e\in C}{w_e} eCwe 也是由 ∣ C ∣ |C| C 个元素组成,所以可以化简为 ∑ ( w − λ ) > 0 \sum{(w-\lambda)}>0 (wλ)>0,换成小于号仍然成立,发现其具有二段性,于是我们可以通过二分来解决这题

首先将原图中所有边的容量减去 λ \lambda λ,如果新的容量小于等于 0,那最终的边割集一定包含这条边,因为它会让最终答案更小

对于其余边,除了选择两个点集之间的边以外,还可以选择点集之内的边,但是选这些边显然只会增加所求值,所以这些边一定不会选到,因此只需要取最小割就可以了,于是最终答案就是边权为负的这些边加上最小割的边,由于最大流等于最小割,所以直接跑最大流就可以了

double work(double mid)
{double res = 0;for (int i = 0; i < idx; i += 2){if (w[i] <= mid){res += w[i] - mid;f[i] = f[i ^ 1] = 0;}else f[i] = f[i ^ 1] = w[i] - mid;}res += dinic();return res;
}void solve()
{cin >> n >> m >> S >> T;for (int i = 1; i <= n; i ++ ) h[i] = -1;for (int i = 0; i < m; i ++ ){int a, b, c;cin >> a >> b >> c;add(a, b, c);}double l = 0, r = 1e7;while (r - l > eps){double mid = (l + r) / 2;if (work(mid) < 0) r = mid;else l = mid;}printf("%.2lf\n", r);
}

最优标号

题意是给你一个无向图,每个点都有一个标号 p p p,对于某条边,定义其费用为 c o s t ( u , v ) = p [ u ] ˆ p [ v ] cost(u,v)=p[u] \^\ p[v] cost(u,v)=p[u] ˆp[v],现在已知部分点的标号,需要确定剩余点标号使所有边的费用和最小

按位考虑,如果当前考虑第 k k k 位,由于每一位只有 0 和 1 两种选择,所以我们可以将点分为两个集合,这一位对于答案的贡献可以转化为这两个集合之间有多少条边,即最小割模型

定义虚拟源点和虚拟汇点,源点在第一个集合,汇点在第二个集合,如果结点一开始就有编号,若编号在这一位上是 0 ,就连一条从源点到该点、容量为正无穷的边(保证求最小割不会选到这一条边),若编号在这一位上是 1,就连一条从该点到汇点,容量为正无穷的边(保证求最小割不会选到这一条边),其余边容量均为 1,之后求最小割计算每位贡献即可

int build(int pos)
{for (int i = 1; i <= n + 2; i ++ ) h[i] = -1;idx = 0;for (int i = 0; i < m; i ++ ) add(edge[i].first, edge[i].second, 1, 1);for (int i = 1; i <= n; i ++ ){if (pi[i] == -1) continue;else if ((pi[i] >> pos) & 1) add(i, T, INF, 0);else add(S, i, INF, 0);}int res = dinic();return (res << pos);
}void solve()
{cin >> n >> m;S = n + 1, T = n + 2;for (int i = 1; i <= n + 2; i ++ ) pi[i] = -1;for (int i = 0; i < m; i ++ ) cin >> edge[i].first >> edge[i].second;int k; cin >> k;for (int i = 0; i < k; i ++ ){int u, x;cin >> u >> x;pi[u] = x;}int res = 0;for (int i = 0; i < 31; i ++ ) res += build(i);cout << res << '\n';
}

最大权闭合子图

闭合子图:给定一个有向图 G = ( V , E ) G=(V,\ E) G=(V, E),图中的某一点集需要满足,点集内部的所有边,不能从点集内指向点集外,只能在内部消化,点集加上相关的边称为一个闭合子图

(只需要保证边不从里面指向外面即可,可以从外面指向里面)

最大权闭合子图是指权值最大的闭合子图(权值是每个点上的权值)

构造方式:原有向图 G = ( V , E ) G=(V,\ E) G=(V, E),流网络 N = ( V N , E N ) N=(V_N,\ E_N) N=(VN, EN),构造出的流网络:

  • 点:原图中的点+源点+汇点
  • 边:原图中的边(容量为 I N F INF INF)+源点向权值为正数的点连边(容量为权值)+权值为负数的点向汇点连边(容量为权值的相反数)

解决问题的大体思路就是将原问题的闭合子图对应到

在这个问题中,简单割代表割中的边要么和源点相连,要么和汇点相连,不存在既不和源点连也不和汇点连的边

最小割一定是一个简单割(因为最小割的容量是有限值,如果包括了中间不和源点汇点相连的边,容量就不是有限值了)

原问题的闭合子图可以和流网络的简单割一一对应,求闭合子图的最值,只需要求简单割的最值,又因为最小割一定是简单割,最小割的最值一定是简单割的最值,所以只需要求出所有割的最值(即为最小割),就是闭合子图的最值了

设闭合子图中点集为 V 1 V_1 V1,其余点为 V 2 V_2 V2,只会存在 V 1 V_1 V1 向汇点 t t t 连的边和源点 s s s V 2 V_2 V2 连的边,有 C [ S , T ] = C [ V 1 , t ] + C [ s , V 2 ] = ∑ v ∈ V 2 + w v + ∑ v ∈ V 1 − w v C[S,\ T]=C[V_1,\ t]+C[s,\ V_2]=\sum_{v\in V_2^{+}}{w_v}+\sum_{v\in V_1^{-}}{w_v} C[S, T]=C[V1, t]+C[s, V2]=vV2+wv+vV1wv

V 1 V_1 V1 的总权值 w ( V 1 ) = ∑ v ∈ V 1 + w v − ∑ v ∈ V 2 + w v w(V_1)=\sum_{v\in V_1^+}{w_v}-\sum_{v\in V_2^+}{w_v} w(V1)=vV1+wvvV2+wv

两式相加, C [ S , T ] + w ( V 1 ) = w ( V + ) C[S,\ T]+w(V_1)=w(V^+) C[S, T]+w(V1)=w(V+)

w ( V + ) w(V^+) w(V+) 是定值,想让 w ( V 1 ) w(V_1) w(V1) 最大,就让 C [ S , T ] C[S,\ T] C[S, T] 最小,即求流网络的最小割,最大流

w ( V 1 ) = w ( V + ) − C [ S , T ] w(V_1)=w(V^+)-C[S,\ T] w(V1)=w(V+)C[S, T]

P4174 [NOI2006] 最大获利

题目链接

题意是有 n n n 个地方可以建基站,在第 i i i 个地方建基站的花费是 p i p_i pi,有 m m m 个用户群,第 i i i 个用户群所对应的利益是 c i c_i ci,每个用户群对应两个基站,只有这两个基站都建立了才能获得该用户群对应的利益,问如何建立基站才能使得获益最大,获益为从用户群获得的利益减去建基站的花费

我们将基站和用户群都看做点,基站的点权为 − p i -p_i pi,用户群的点权为 c i c_i ci,这样问题就变成了我们选择用户群和基站,使得选择的用户群对应的基站都被选择了,要使选择的所有点权值之和最大

怎么让选择的用户群对应的基站都被选择呢,我们从用户群向对应的基站连一条有向边,这样就变成了找到有向图的最大权闭合子图了

void solve()
{cin >> n >> m;S = n + m + 1, T = n + m + 2;for (int i = 1; i < N; i ++ ) h[i] = -1;for (int i = 1; i <= n; i ++ ){int x; cin >> x;add(i, T, x, 0);}int ans = 0;for (int i = 1; i <= m; i ++ ){int a, b, c; cin >> a >> b >> c;add(n + i, a, INF, 0);add(n + i, b, INF, 0);add(S, n + i, c, 0);ans += c;}cout << ans - dinic() << '\n';
}

最大密度子图

在原图 G = ( V , E ) G=(V,\ E) G=(V, E) 的基础上选择一个点集 V ′ V' V 和一个边集 E ′ E' E,使得边集中每一条边的两个端点,都出现在点集内

最大密度子图:在以上的所有选择方案种, ∣ E ′ ∣ ∣ V ′ ∣ \frac{|E'|}{|V'|} VE 最大的图

最大化一个分式,想到 01分数规划+二分:

∣ E ′ ∣ ∣ V ′ ∣ = g \frac{|E'|}{|V'|}=g VE=g,则有 ∣ E ′ ∣ − g ∣ V ′ ∣ = 0 |E'|-g|V'|=0 EgV=0,如果 ∣ E ′ ∣ − g ∣ V ′ ∣ |E'|-g|V'| EgV 的最大值大于 0 0 0,说明 g g g 小了,反之同理

所以核心就是最大化 ∣ E ′ ∣ − g ∣ V ′ ∣ |E'|-g|V'| EgV,也就是最小化 g ∣ V ′ ∣ − ∣ E ′ ∣ g|V'|-|E'| gVE,这个式子又可以化简为 ∑ v ∈ V g − ( ∑ v ∈ V ′ d v 2 − C [ V ′ , V ′ ‾ ] 2 ) \sum_{v\in V}g-(\frac{\sum_{v\in V'}d_v}{2}-\frac{C[V',\ \overline{V'}]}{2}) vVg(2vVdv2C[V, V]) ,怎么理解这个化简呢,减号前面很好理解,减号后面也就是边数,我们可以把所有边一分为二,两个部分分别给两个端点,边数也就是度数除以 2 2 2,然后再减去割边,得到子图中的边数,整理一下式子: 1 2 × ( ∑ v ∈ V ′ ( 2 g − d v ) + C [ V ′ , V ′ ‾ ] ) \frac{1}{2}\times(\sum_{v\in V'}{(2g-d_v)}+C[V',\ \overline{V'}]) 21×(vV(2gdv)+C[V, V])到这就将原问题和最小割关联起来了

最大密度子图问题也可以转化为最大权闭合子图问题,只需要将点看成点,边也看成点,边的点权值是 1 1 1,点的点权值是 − g -g g,边 ( u , v ) (u,\ v) (u, v) 就让这条边形成的点向 u u u v v v 连有向边

费用流模型

定义

费用流:所有最大可行流中,费用的最小 / 最大值

费用:可行流中用到的边的费用乘流量之和

如果一张图有最大流,一定有最小费用最大流(但是不一定能求出来)

求法 - 基于EK算法

把EK算法中的bfs换成spfa即可

特别地,常规的最小费用最大流算法不能处理负权回路问题

u u u v v v 费用记作 w ( u , v ) w(u,\ v) w(u, v),那么 w ( v , u ) = − w ( u , v ) w(v,\ u)=-w(u,\ v) w(v, u)=w(u, v)

步骤:

  1. 找一条增广路(使用spfa找一条从源点到汇点的最短/长路)
  2. 更新当前可行流

模板 - zkw费用流

据说跑起来很快

最小/大费用最大流

class Graph
{
public:int n, m;int top, to[M << 1], fw[M << 1], ct[M << 1];vector<int> g[V];Graph() { top = 1; }void add(int x, int y, int f, int c){g[x].push_back(++top);to[top] = y, fw[top] = f, ct[top] = c;}void Add(int x, int y, int f, int c){add(x, y, f, c), add(y, x, 0, -c);}
};
class zkwMCMF : public Graph
{
public:int s, t;int fans, cans; // 最大流流量 和 最小费用int dep[V];bool vis[V];deque<int> q;bool spfa(){for (int i = 1; i <= n; i++)vis[i] = 0, dep[i] = INF;q.push_back(t), vis[t] = 1, dep[t] = 0;while (q.size()){int x = q.front();q.pop_front(), vis[x] = 0;for (auto i : g[x])if (fw[i ^ 1] && dep[to[i]] > dep[x] - ct[i]){dep[to[i]] = dep[x] - ct[i];if (!vis[to[i]]){vis[to[i]] = 1;if (q.size() && dep[to[i]] < dep[q.front()])q.push_front(to[i]);elseq.push_back(to[i]);}}}return dep[s] < INF;}int dfs(int x, int F){vis[x] = 1;if (x == t || !F)return F;int f, flow = 0;for (auto i : g[x])if (!vis[to[i]] && fw[i] && dep[x] - ct[i] == dep[to[i]] && (f = dfs(to[i], min(F, fw[i]))) > 0){cans += f * ct[i], fw[i] -= f, fw[i ^ 1] += f, flow += f, F -= f;if (!F)break;}return flow;}// 求最小费用最大流调用此函数即可// 求最大费用最大流,需要在建图时将费用取反,最后的最大费用为 -cansvoid mcmf(){while (spfa()){vis[t] = 1;while (vis[t]){for (int i = 1; i <= n; i++)vis[i] = 0;fans += dfs(s, INF);}}}
} network;

最后补一个蒋老师的最小费用可行流板子

struct MCFGraph
{struct Edge{int v, c, f;Edge(int v, int c, int f) : v(v), c(c), f(f) {}};const int n;vector<Edge> e;vector<vector<int>> g;vector<int> h, dis;vector<int> pre;bool dijkstra(int s, int t){dis.assign(n, numeric_limits<int>::max());pre.assign(n, -1);priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> que;dis[s] = 0;que.emplace(0, s);while (!que.empty()){int d = que.top().first;int u = que.top().second;que.pop();if (dis[u] < d)continue;for (int i : g[u]){int v = e[i].v;int c = e[i].c;int f = e[i].f;if (c > 0 && dis[v] > d + h[u] - h[v] + f){dis[v] = d + h[u] - h[v] + f;pre[v] = i;que.emplace(dis[v], v);}}}return dis[t] != numeric_limits<int>::max();}MCFGraph(int n) : n(n), g(n) {}void addEdge(int u, int v, int c, int f){if (f < 0){g[u].push_back(e.size());e.emplace_back(v, 0, f);g[v].push_back(e.size());e.emplace_back(u, c, -f);}else{g[u].push_back(e.size());e.emplace_back(v, c, f);g[v].push_back(e.size());e.emplace_back(u, 0, -f);}}pair<int, int> flow(int s, int t){int flow = 0;int cost = 0;h.assign(n, 0);while (dijkstra(s, t)){for (int i = 0; i < n; ++i)h[i] += dis[i];int aug = numeric_limits<int>::max();for (int i = t; i != s; i = e[pre[i] ^ 1].v)aug = min(aug, e[pre[i]].c);for (int i = t; i != s; i = e[pre[i] ^ 1].v){e[pre[i]].c -= aug;e[pre[i] ^ 1].c += aug;}flow += aug;cost += int(aug) * h[t];}return make_pair(flow, cost);}
};

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com