算法进阶课网络流部分总结
基本概念
流网络
由点集和边集组成的有向图,其中包含一个源点 s 和一个汇点 t,边上的权值表示
可以将其理解为水流和管道,从源点流出水流,流入汇点,边上的权值表示管道可以流过的水量
可行流
满足以下条件的叫做可行流:
- 容量限制:一条边上流过的流量不能超过其最大限制,即 0 ≤ f ( u , v ) ≤ c ( u , v ) 0\le f(u, v)\le c(u, v) 0≤f(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)=∑u∈X∑v∈Yf(u,v)−∑u∈X∑v∈Yf(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,X∪Y)=f(Z,X)+f(Z,Y)
性质
- 对于任意的割,割的流量 < 割的容量
- 对于任意的割和可行流,割的流量 = 可行流的流量
- 对于任意的割和可行流,可行流的流量 < 割的容量
最小割
最小割:最小容量割
最大流最小割定理
对于某一个流网络 G 来说,以下三个条件可以相互等价:
- 一个可行流 f f f 是最大可行流
- 可行流 f f f 的残留网络中不存在增广路
- 存在一个割 [ S , T ] [S,T] [S,T] 使得可行流的流量等于割的容量
最大流等于最小割
算法
思想:维护残留网络,在残留网络里不断找增广路,将增广路删去,一直重复直到找不到增广路为止
EK算法
时间复杂度: O ( n m 2 ) O(nm^2) O(nm2)
步骤:
- 找增广路
- 更新残留网络(正向边容量 - 流量,反向边容量 + 流量)
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) 0≤f(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_{出} c入−c出,怎么处理这个少流入的流量呢,我们连一条边从源点到 x x x,流量是 c 入 − c 出 c_{入}-c_{出} c入−c出 就可以了(如果少流出了,就连一条 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 i−1 天到达第 j j j 个点的人可以停在这个点不动,所以连一条从第 i − 1 i-1 i−1 天的第 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 最长不下降子序列问题
题意是给出一个序列,回答三个问题:
- 最长不下降子序列的元素个数 s s s
- 长度为 s s s 且不共用元素的最长不下降子序列个数
- 可以多次使用 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|} ∣C∣∑e∈Cwe 最小,输出这个最小值
所有求形如 ∑ e ∈ C w e ∣ C ∣ \frac{\sum_{e\in C}{w_e}}{|C|} ∣C∣∑e∈Cwe 的最值问题,都可以考虑转化成 01分数规划
设 ∑ e ∈ C w e ∣ C ∣ > λ \frac{\sum_{e\in C}{w_e}}{|C|}>\lambda ∣C∣∑e∈Cwe>λ,有 ∑ e ∈ C w e > λ ∣ C ∣ \sum_{e\in C}{w_e}>\lambda |C| ∑e∈Cwe>λ∣C∣,又可推 ∑ e ∈ C w e − λ ∣ C ∣ > 0 \sum_{e\in C}{w_e}-\lambda |C|>0 ∑e∈Cwe−λ∣C∣>0,由于 ∑ e ∈ C w e \sum_{e\in C}{w_e} ∑e∈Cwe 也是由 ∣ 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]=∑v∈V2+wv+∑v∈V1−wv
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)=∑v∈V1+wv−∑v∈V2+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'|} ∣V′∣∣E′∣ 最大的图
最大化一个分式,想到 01分数规划+二分:
设 ∣ E ′ ∣ ∣ V ′ ∣ = g \frac{|E'|}{|V'|}=g ∣V′∣∣E′∣=g,则有 ∣ E ′ ∣ − g ∣ V ′ ∣ = 0 |E'|-g|V'|=0 ∣E′∣−g∣V′∣=0,如果 ∣ E ′ ∣ − g ∣ V ′ ∣ |E'|-g|V'| ∣E′∣−g∣V′∣ 的最大值大于 0 0 0,说明 g g g 小了,反之同理
所以核心就是最大化 ∣ E ′ ∣ − g ∣ V ′ ∣ |E'|-g|V'| ∣E′∣−g∣V′∣,也就是最小化 g ∣ V ′ ∣ − ∣ E ′ ∣ g|V'|-|E'| g∣V′∣−∣E′∣,这个式子又可以化简为 ∑ 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}) ∑v∈Vg−(2∑v∈V′dv−2C[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×(∑v∈V′(2g−dv)+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)
步骤:
- 找一条增广路(使用spfa找一条从源点到汇点的最短/长路)
- 更新当前可行流
模板 - 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]);
else
q.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;
}
// 求最小费用最大流调用此函数即可
// 求最大费用最大流,需要在建图时将费用取反,最后的最大费用为 -cans
void 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);
}
};