2022.12.9 闲话
想放个歌词 .
[歌词 1]
[歌词 2]
以上是歌词 .
joke 最近发了一个网络流闲话,我马上就开抄,于是就有了这个闲话 .
最大流和最小割的一些初步应用 . 更多内容:一些没用的初步应用 .
为什么 OJ 的网络流还重构结构了啊?!
没啥用的目录
0. 基本内容 Basic
流网络
一个流网络定义为一张有向图,每条边 对于每条边,有容量 和流量 .
有两个特殊点:源点 和汇点 .
图要求满足的条件:
- 如果存在边 ,则不存在边 ,无自环 .
- 对于每个节点 ,都存在一条路径 (所以说图一定连通) .
流要求满足的条件:
- 流量限制:对于任意节点 ,有 .
- 流量守恒:对于非源汇点的所有节点 ,有
称一个流 的值 为
通常来说,源点 的入度为 ,于是后面那个 也就为 .
需要解决的问题
最大流:给一个流网络 ,问 最大的流 是什么 .
最小费用最大流:一个流网络 ,每条边同时有边权(费用),要求一条流 满足:
- 最大 .
- 边权和(费用和)最小 .
上下界网络流:以上问题的流量 存在上界 / 下界限制 .
最小割(其实不太算网络流):一个流网络 ,删掉若干条边使得 不连通,同时流量和最小 .
需要注意的,最小流这种是平凡的(0),最大割则是 NPC 问题 .
一些基本转化
反向边去除
注意到流网络有一个限制是如果存在边 ,则不存在边 .
那么我们的图就是有反向边怎么办呢?
解决方案也很简单,令 是一组点满足存在边 ,且容量分别为 .
则删掉 ,建立一个虚点 ,连 ,容量均为 即可 .
易证最大流不变 .
多源汇合一
解决多源点多汇点的问题,只需要建一个超级源点和超级汇点,分别跟源汇点连容量 的边即可 .
1. 最大流 Maxflow
最大流的增广路算法
或者叫 Ford-Fulkerson 方法 .
FF, EK, Dinic
不想详细写了,丢个 教程 .
首先建反向边方便操作 .
简要概括一下:
- FF:每次 DFS 随便找一个增广路然后增广 .
- EK:先 BFS 找一下,每次增广最短的增广路 .
- Dinic:分层删掉没用的边然后按层增广(时间复杂度分析见 8.12 闲话)
- 当前弧优化:增广过的边就删掉
- 无用点优化:不会产生贡献的点删掉
- Capacity scaling:先不加反向边跑一遍,然后一次性把反向边都加进去,然后再跑一遍 .
- 26-5:二进制分组,按位 Dinic,然后合起来 .
下面的代码是 Dinic + 当前弧优化 + 无用点优化,可以通过洛谷 & LOJ 的弱化版板子 .
Dinic 代码实现
// 点数 N
int n, m, s, t;
struct dinic
{
static const ll INF = 0x3f3f3f3f3f3f3f3f;
struct Node
{
int u, v; ll w;
Node(int p, int q, ll r) : u(p), v(q), w(r){}
};
vector<int> g[N];
vector<Node> e;
inline void addedge(int u, int v, ll w)
{
int m = e.size();
e.emplace_back(Node(u, v, w)); g[u].emplace_back(m);
e.emplace_back(Node(v, u, 0)); g[v].emplace_back(m+1);
}
int cur[N], depth[N];
inline bool bfs(int s, int t)
{
memset(cur, 0, sizeof cur);
memset(depth, -1, sizeof depth);
queue<int> q; q.push(s); depth[s] = 0;
while (!q.empty())
{
int u = q.front(); q.pop();
for (auto ee : g[u])
{
int v = e[ee].v;
if (!~depth[v] && e[ee].w){q.push(v); depth[v] = depth[u] + 1;}
}
}
return ~depth[t];
}
ll dfs(int u, int t, ll flow)
{
if ((u == t) || (flow <= 0)) return flow;
ll ans = 0; int _ = g[u].size();
for (int& p = cur[u]; p < _; p++)
{
int ee = g[u][p], v = e[ee].v;
if (depth[u] + 1 != depth[v]) continue;
ll nxt = dfs(v, t, min(flow, e[ee].w));
e[ee].w -= nxt; e[ee ^ 1].w += nxt;
ans += nxt; flow -= nxt;
if (flow <= 0) break; // 这个如果写在循环条件上会在判断前做一次 p++ 导致漏一条边
}
if (ans <= 0) depth[u] = -1;
return ans;
}
ll maxflow(int s, int t)
{
ll ans = 0;
while (bfs(s, t)) ans += dfs(s, t, INF);
return ans;
}
};
ISAP
占位 .
实际上我也不打算写,占位的原因是一个 h2 下面只有一个 h3 有点奇怪 .
根据相关法律法规,网络流题是不会卡 Dinic 的,但是 ISAP 是可以卡的,惨案:->Click Here<- .
最大流问题的 HLPP 算法可以做到稳定 最大流 .
最近(可能也不算最近了)的一篇 paper 提出了 的最大流和最小费用最大流算法,可以看作是线性的网络流了,论文链接 .
例题
更难的模型我不会!
朴素例题
蜥蜴
一个 行 列的网格图,每个地方 有高度 ,一些石柱上有蜥蜴 .
每个蜥蜴每次可以跳到欧几里得距离不超过 的任何一个格上 . 每当蜥蜴跳跃时,所离开的格高度减 ,不能跳到高度为 的格子上,多个蜥蜴不能同处一格 .
蜥蜴跳到地图外即逃离,问无法逃离的蜥蜴总数的最小值 .
, .
即求能够逃离的蜥蜴的最大值,最大流即可 .
建图就是把每个点拆成入点和出点,从入点到出点连边,容量为高度,这样就限制了经过次数 .
发现源汇点很多,建虚源汇点即可,注意因为每个地方只有一个蜥蜴所以源点连的权值是 1 .
没了 .
Code
// dinic 略
dinic F;
int n, m, d;
int h[L][L];
bool can[L][L];
inline int dist(int x1, int y1, int x2, int y2){return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);}
inline int tin(int x, int y){return (x-1) * m + y;}
inline int tout(int x, int y){return tin(x, y) + n * m;}
inline bool corner(int x, int y){return min(min(x, n+1-x), min(y, m+1-y)) <= d;}
inline bool cann(int x1, int y1, int x2, int y2)
{
if ((x1 == x2) && (y1 == y2)) return false;
return h[x1][y1] && h[x2][y2] && (dist(x1, y1, x2, y2) <= d * d);
}
string tmp;
int main()
{
scanf("%d%d%d", &n, &m, &d);
for (int i=1; i<=n; i++)
{
cin >> tmp;
for (int j=0; j<m; j++) h[i][j+1] = tmp[j] - '0';
}
int s = 2 * n * m + 1, t = s + 1, ans = 0;
for (int i=1; i<=n; i++)
{
cin >> tmp;
for (int j=0; j<m; j++)
{
can[i][j+1] = (tmp[j] == 'L');
if (can[i][j+1]) F.addedge(s, tin(i, j+1), 1), ++ans;
}
}
for (int i=1; i<=n; i++)
for (int j=1; j<=m; j++)
if (h[i][j])
{
F.addedge(tin(i, j), tout(i, j), h[i][j]);
if (corner(i, j)) F.addedge(tout(i, j), t, INF);
}
for (int i1=1; i1<=n; i1++)
for (int j1=1; j1<=m; j1++)
for (int i2= max(1, i1-d); i2 <= min(n, i1+d); i2++)
for (int j2 = max(1, j1-d); j2 <= min(m, j1+d); j2++)
if (cann(i1, j1, i2, j2)) F.addedge(tout(i1, j1), tin(i2, j2), INF);
printf("%lld\n", ans - F.maxflow(s, t));
return 0;
}
后面的题就不都给代码了 .
二分图最大匹配
给一个二分图,求最大匹配 .
源点 向所有左部点连一条容量为 的边,所有右部点向汇点 也连一条容量为 的边,于是 到 的最大流显然就是答案 .
奇怪的游戏
一个 矩阵,初始 有数 ,每次可以选两个相邻数同时加一,问至少操作几次使得所有数一样,无解输出
-1
., .
题解链接,复杂度分析部分比较麻烦可以看链接的,后面就不分析复杂度了 .
骨牌题考虑黑白染色,设黑白点个数分别为 ,权值和分别为 .
每次操作肯定是恰好操作一个黑点和一个白点,于是设最后所有点都变成了 ,则有 ,也即 .
讨论:
- 当 时 .
- 当 时 .
- 其他情况无解 .
Case 1. 可以解得 .
于是只要判断 是否能作为一个解即可,具体见后 .
Case 2. 这表明黑白色块数量相等且权值和相等 .
因为黑白色块数量相等所以如果 可以作为一个解那么 必然也可以 .
也就是解有单调性,二分答案即可 . 判断 是否能作为一个解的做法如下:
考虑建立虚拟源汇点 :
- 向所有黑点 连边权为 的有向边 .
- 所有白点 向 连边权为 的有向边 .
- 所有黑点 向相邻白点连边权为 的有向边 .
因为 是需要操作的次数,于是因为黑白色块数量权值和相等,如果有解那么流肯定能从 流到黑点流到白点再流到 .
于是只要判断以 为源点, 为汇点的最大流是否等于所有黑点之权值和即可 .
网络流题的一个常见开端就是二分答案,可以看一下 紧急疏散evacuate .
输出方案
我怎么知道如何输出方案?青蛙。
2. 最小割 Mincut
最大流 - 最小割定理
就是最大流 = 最小割 .
证明略 .
最小割树 / Gomory-Hu Tree
基本原理
还以为是什么很难的算法,原来也不算很难 desu .
我们的目标是求任意两点间最小割(等价于最大流).
最小割树定义:如果一棵树 满足对于树上的所有边 满足树上去掉 这条边后产生的两个集合恰好是原图上 的最小割把原图分成的两个集合,且边 的权值等于原图上 的最小割,则 是一棵最小割树 .
在当前点集随意选取两个点 ,在原图上跑出他们之间的最小割,然后就在树上连一条从 到 ,权值为 间最小割的边,然后递归构造 .
(这个算法被称为 Gusfield 算法,实际上是构造了一个等价流树 / Equivalent Flow Tree,和最小割树差不多).
然后两点 间最小割就是树上边权最小值,这个有很多求法,就不详细展开了 .
关于正确性:OI 不需要证明 .
时间复杂度是 次最大流的复杂度,一般用 Dinic 就是 .
Pumping Stations
给一张 个点的带权无向图,求一个排列 ,最大化
, .
施 Gomory-Hu 树,则问题变成一棵树上找一个排列 ,最大化
构造的话找到最小边左右分治即可,OI 不需要证明(?
例题
可以看一下杜教的《最小割模型在信息学竞赛中的应用》.
切糕模型
切糕模型,或者叫「最小割离散变量模型」(词源 姜志豪《网络流的一些建模方法》(NOI2016 候选队论文))
切糕
经过千辛万苦小 A 得到了一块切糕,切糕的形状是长方体,小 A 打算拦腰将切糕切成两半分给小 B . 出于美观考虑,小 A 希望切面能尽量光滑且和谐 . 于是她找到你,希望你能帮她找出最好的切割方案 .
出于简便考虑,我们将切糕视作一个长 、宽 、高 的长方体点阵 . 我们将位于第 层中第 行、第 列上的点称 ,它有一个非负的不和谐值 . 一个合法的切面满足以下两个条件:
- 与每个纵轴(一共有 个纵轴)有且仅有一个交点 . 即切面是一个函数 ,对于所有 (,),我们需指定一个切割点 ,且 .
- 切面需要满足一定的光滑性要求,即相邻纵轴上的切割点不能相距太远 . 对于所有的 和 ,若 ,则 ,其中 是给定的一个非负整数 .
可能有许多切面 满足上面的条件,小 A 希望找出总的切割点上的不和谐值最小的那个 .
, .
首先把点化成边 .
如果没有高度限制那么显然直接拉 条链然后超级源汇点随便最小割一下即可,下面考虑有限制怎么做:
假设我们建出了这样的两条链:
然后如果高度差限制是 ,则我们割这样的两条边是不合法的:
我们加一条 边(如下图虚线):
那么我们发现,这种情况变得不合法了: 和 依然连通.
这种建 边来限制选取的方法就叫做「切糕模型」.
广义的,我们用链的形式来表示偏序关系形式的选取限制,然后用最小割解决问题的模型叫做「切糕模型」,也即「最小割离散变量模型」.
(例子来源:rvalue 的博客)
有一个正确性证明:dengyaotriangle,虽然我觉得这个建图方式非常明了啊,但是这个正确性证明也可以参考一下 .
建图方式代码(建议全屏观看):
Code
for (int i=1; i<=p; i++)
for (int j=1; j<=q; j++)
{
F.addedge(s, node(i, j, 1), v[i][j][1]); F.addedge(node(i, j, r), t, INF);
for (int k=2; k<=r; k++) F.addedge(node(i, j, k-1), node(i, j, k), v[i][j][k]);
}
for (int i=1; i<=p; i++)
for (int j=1; j<=q; j++)
for (int k=1; k<=r-d; k++)
{
if (inrange(i+1, 1, p)) F.addedge(node(i+1, j, k+d), node(i, j, k), INF);
if (inrange(i-1, 1, p)) F.addedge(node(i-1, j, k+d), node(i, j, k), INF);
if (inrange(j+1, 1, q)) F.addedge(node(i, j+1, k+d), node(i, j, k), INF);
if (inrange(j-1, 1, q)) F.addedge(node(i, j-1, k+d), node(i, j, k), INF);
}
比较坑的是如果读入每次读入 的循环顺序是 .
关于这个模型的一些描述:最小割离散变量模型 .
一些其他题:
最大权闭合子图
最大权闭合子图:
给一张有向图,每个点有点权,选一个子图,使得每个点的所有邻接点都在图内,问最大点权和 .
做法就是加超级源汇 , 连点权是正的点, 连点权是负的点,容量为点权绝对值,原图的边连容量 的边然后最大流即可 .
例题:NOI2006 最大获利 .
多选一模型
不太好说,放个例题(happiness):
这个是二选一的例子,考虑首先加上所有喜悦值然后减最小割 .
建源汇点 ,首先连 到每个点,容量是文科价值,连每个点到 ,容量是理科价值 .
对于每条边拆出来一个点 ,连 到 ,容量为边权,然后 对于边到两端连无穷容量的边 .
观察可得是正确的 .
一些例子:文理分科,小 M 的作物,善意的投票等 .
以下是博客签名,正文无关
本文来自博客园,作者:yspm,转载请注明原文链接:https://www.cnblogs.com/CDOI-24374/p/16968980.html
版权声明:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0)进行许可。看完如果觉得有用请点个赞吧 QwQ
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App
2020-12-09 平衡树