网络流
前排提醒,「我不知道啊!反正不考!」等话语并非无意义调侃,而是笔者实力有限没学过,而且绝大部分题目都不考察这部分内容。
网络流在理解上通常被比喻成水流,起点 \(s\) 是自来水厂,\(t\) 是你家,图中的边是水管。
有关定义。最大流是指,每个水管都有固定的宽度,一个水管的宽度为 \(w\) 表示在单位时间内,流经这个水管的水不得超过 \(w\) 单位。已知自来水厂有无穷多水,那么你家每单位时间能获得的水的数量,即为最大流。
一个特殊的便于理解的例子是,如果 \(s \rightsquigarrow t\) 是一条链,那么最大流为所有 \(w\) 的最小值。当然一般的图绝对不一定是这样的。
费用流是指,每个水管有一个额外的固定代价 \(c\),表示一个单位的水经过这个管道就需要 \(c\) 的代价。那么在保证你家能得到最多的水的前提下,最小的代价和即为最小费用最大流。
对于最大流的求解,一个朴素想法是每次我们找一条 \(s \rightsquigarrow t\) 的路径。然后找到这条路径上的 \(w\) 的最小值 \(w_\min\),表示如果水想从这条路径走,数量就不能超过 \(w_\min\)。然后我们将这条路径上的所有边权都执行 \(w \gets w - w_\min\),并把新的边权为 \(0\) 的边删掉。一个不太恰当的理解是,高速公路的六车道上有五个车道已经满了车了,那么如果接下来有辆新车,这五个车道可以直接忽略,只考虑那个没满的车道。当然,如果六车道都满了,那么这整条高速公路对于这个新车而言都没用了。
但是这样做是错误的。例子?我不知道啊!反正不考!解决问题的方法是,对于一条原有的边 \(u \xrightarrow{w(u,v)} v\),我们建一条反向边 \(v \xrightarrow{0} u\),然后在执行上面说的 \(w(u,v) \gets w(u,v) - w_\min\) 时,再补充一个操作 \(w(v,u) \gets w(v, u) + w_\min\)。可以感性理解成,我把你的过去都记录下来,当你后悔的时候找我变身成原来就行。
然后向上面那样找一条路径操作即可。以上是 EK 算法。正确性证明?我不知道啊!反正不考!一个基于 bfs 求任意一条路径的代码如下:
// P3376 【模板】网络最大流
int n, m, s, t;
int e[N][N], pre[N], flow[N], res;
// e[i][j] 表示邻接矩阵
// pre[i] 用于存储路径
// flow[i] 表示 s -> i 的路径上的边权最小值,或者理解成从自来水厂能流到 i 的水的数量
int bfs() { // 若不存任意一条路径返回 -1,否则返回某条路径的最小边权
memset(pre, -1, sizeof pre);
flow[s] = INF; // 自来水厂有无数多的水
pre[s] = 0;
queue<int> q; // 队列实现 bfs
q.push(s);
while (q.size()) {
int u = q.front();
q.pop();
if (u == t) break;
for (int v = 1; v <= n; ++ v )
if (u != v && e[u][v] && pre[v] == -1) {
pre[v] = u;
q.push(v);
flow[v] = min(flow[u], e[u][v]);
}
}
return pre[t] == -1 ? -1 : flow[t];
}
signed main() {
cin >> n >> m >> s >> t;
while (m -- ) {
int a, b, c;
cin >> a >> b >> c;
e[a][b] += c;
}
while (1) {
int k = bfs();
if (k == -1) break;
for (int u = t; u != s; u = pre[u]) {
e[pre[u]][u] -= k; // 正向边
e[u][pre[u]] += k; // 反向边
}
res += k;
}
cout << res;
return 0;
}
EK 算法复杂度下界是 \(\mathcal O(nm^2)\)。这是因为最快情况下外层循环(即寻找一条路径)的轮数是 \(m\),而处理一条路径的复杂度是 \(nm\)。当然一般情况下远小于这个值。
而 dinic 算法的思想,是在每一轮同时找到多条路径,并同时更新。
具体的,我们将图中的点与 \(s\) 的最短距离分层。每一轮处理掉 \(s \rightsquigarrow t\) 的所有最短路径。注意这里的最短路是指在边权为 \(1\) 的图,而不是其真实边权 \(w\)。然后加上一些剪枝就能做到 \(\mathcal O(n^2m)\)。同样的,一般情况下复杂度远小于这个值。
代码是这样的,注释里讲解了剪枝方法:
// P3376 【模板】网络最大流
int n, m, s, t;
int h[N], e[M], ne[M], idx, w[M], now[M];
int dep[N];
int now[N]; // 当前弧优化,在一轮里一条边不会被经过多次
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++ ;
}
bool bfs() { // 将所有点按最短路分层,即求解 dep,如果不存在路径返回 false
fill(dep + 1, dep + n + 1, 1e18);
dep[s] = 0;
queue<int> q;
q.push(s);
now[s] = h[s];
while (q.size()) {
int u = q.front();
q.pop();
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (w[i] && dep[v] > dep[u] + 1) {
now[v] = h[v];
dep[v] = dep[u] + 1;
q.push(v);
}
}
}
return dep[t] < 1e18;
}
int dfs(int u, int flow) {
// 这两个参数可以理解成,如果自来水厂在 u 建且水量为 flow,t 能得到的最大数量。显然答案为 dfs(1, INF)
if (u == t) return flow;
int res = 0;
for (int i = now[u]; ~i; i = ne[i]) {
now[u] = i; // 当前弧优化
if (w[i] && dep[v] == dep[u] + 1) { // 这条边在最短路上
int k = dfs(v, min(flow, w[i])); // 走一步
if (k) { // 如果 t 能得到水
w[i] -= k; // 正向边
w[i ^ 1] += k; // 反向边
flow -= k; // 将自来水厂的水分给 v -> t 路径 k 个单位
res += k; // 答案加 k
}
else { // 如果 t 得不到水了
dep[v] = 1e18; // 这个点再也不会被访问了
}
}
if (!flow) break; // 如果自来水厂破产了,结束
}
return res;
}
signed main() {
memset(h, -1, sizeof h);
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> s >> t;
while (m -- ) {
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
add(b, a, 0);
}
int res = 0;
while (bfs()) res += dfs(s, INF);
cout << res;
return 0;
}
实测跑得很快。复杂度证明?我不知道啊!反正不考!正确性证明?我不知道啊!反正不考!
有关费用流的求解方法。只需要将上面找的路径,强制设为一条以代价为边权的最短路,剩下的一模一样。复杂度可能会加上一个 spfa 的玄学复杂度。正确性证明?我不知道啊!反正不考!。
几道例题。会在其中讲解一些常见 trick。
给定一张无向连通图,边有边权。求两条边集不相交的 \(1 \rightsquigarrow n\) 的路径,使得其边权和最小。输出这个最小值。
我们在 \(1\) 号点建一个自来水厂,它有 \(2\) 个单位的水。我们将每条边的代价设为它的边权,宽度设为 \(1\),表示只允许一个单位水通过。然后这两个单位水分别从 \(1\) 出发到达 \(t\)。这是一个最小费用最大流问题。
相信读者感到疑惑的是,上面的讲述中我们都默认自来水厂有无数水,那么如何体现它有恰好 \(2\) 个水呢?我们可以建一个虚点,并在这个虚点上建自来水厂(即视为起点),然后将这个虚点与 \(1\) 连一条代价为 \(0\),宽度为 \(2\) 的边。这是一个 trick。
有 \(n\) 只牛,每只牛都喜欢若干种饮料和食物。已知每种饮料和食物都恰好有 \(1\) 份。一只牛满意当且仅当它吃了至少一种它喜欢的饮料和至少一种它喜欢的食物。求最多能让多少牛满意。
拆点的 trick。我们把一头牛拆成两头牛 \((\)牛 \(1,\) 牛 \(2)\)。将牛 \(1\) 与它喜欢的饮料连有向边,将牛 \(2\) 与它喜欢的食物连有向边,牛 \(1\) 牛 \(2\) 之间。建两个虚点 \(S, T\),\(S\) 与所有饮料连有向边,\(T\) 与所有食物连有向边。然后在 \(S\) 上建自来水厂,一个水走的路径 \(S \rightsquigarrow T\) 中一定包含一个饮料,牛 \(1\) 牛 \(2\),一个食物,正好对应了一种选取方案。所以直接做最大流即可。上述边的边权均为 \(1\)。
给定 \(n\) 个物品。你需要将每个物品放入 \(A\) 盒或 \(B\) 盒。每个物品放入某个盒子都会有代价,某两个物品放入不同的盒子也有代价。求最小代价和。
最小割 = 最大流。
最小割是指,给一张带边权的无向联通图,你需要割掉一些边,使得这张图被划分成恰好两个部分,且一个部分包含 \(S\),一个包含 \(T\),且被割掉的边权和最小。可以证明其值等于最大流。证明?我不知道啊!反正不考!
对于这种划分的问题,通常会从最小割或二分图考虑。
我们将 \(A\) 盒与物品连边,\(B\) 盒同理。在有特殊限制的物品间连边。我们希望为每条边构造一个边权,使得将某些边断掉后,所有物品能够被恰好分成两部分,且恰好对应一种方案。
注意到若我们直接将边权设为对应的代价,那么对于一个物品与 \(A, B\) 的连边而言,我们不可能将这两条边都删掉(原因可以举几个例子),也不可能都不删(因为这样会导致 \(A, B\) 在同一部分内)。所以一个会删恰好一个,而删哪条我们就把物品放到哪个盒子中。物品间连边的理解是显然的。