网络流学习笔记【更新中】
网络流学习笔记
定义
在一个有向图上,存在一个源点
所有边从源点连出,最终连向汇点。
每条边有流量限制
题目一般会问从源点出发到汇点最多能有多少的流量。
比如下图就是一个网络流:
最大流
网络流的最基础的部分。
例题:洛谷 P3376 【模板】网络最大流。
首先最基础的建图没什么好说的。
EK(Edmonds-Karp) 算法
首先介绍 EK 算法。
我们引入增广路的概念。一条增广路是从
在该算法中,我们会对整张图直接 BFS 多次求出所有增广路。答案即为所有增广路流量的总和。
但是,直接这样是有问题的。
比如这张图:
转自 OI-Wiki。
在这张图中,直接跑增广路是不优的。
所以我们继续引入退流的思想。
该思想的主要内容是,当之前一次决策中找到的增广路实际还可以被更多的扩展,此时的答案不是最优的,我们就把这条增广路去掉,重新去跑新的增广路。(感觉比较抽象?)
具体的,在实际建图时,我们会在反着建一条流量为
之后每次找到一条增广路时将路径上的所有正边的流量减去这条增广路的流量,所有反边的流量加上这条增广路的流量。
那么在决策不是最优时,我们会再去走一遍反边。一遍正边一遍反边就相当于没走。
这样就能保证正确性了。
放张图:
转自 【学习笔记】网络流-CCComfy の Blog
由于我们需要非常快速的找到一条边的反边,所有在网络流建图的时候,大部分 OIer 会选择使用链式前向星建图。但我偏不 但我仍然使用邻接矩阵建图,只不过需要多记录一点点东西。
我们在建边的时候记录下边的编号,并从任意偶数开始(我选择从
在跑 BFS 的时候有一个小优化:当准备跑的边已经没有流量的时候,就不继续跑。看上去优化没啥用,但不加就会让你全部 TLE。
EK 的理论复杂度是
然后放下代码:Link。
继续放几道例题:
CodeForces 78E Evacuation:题解
洛谷 P2763 试题库问题
简要题意:有
我们直接把每道题目和每个标签当成一个点。
那么连边显然可以是每道题目向它所属的标签连一条流量为
然后跑网络流即可。
但是本题还需要输出方案。
我们不难发现,本题建出来的图大概长这样:
也就是说,我们在执行 Update 函数时,访问到的第一个结点是汇点,第二个结点是标签编号,第三个结点是题目编号。
那么我们就只要记录一下每一条增广路上经过的标签编号和题目编号,然后开一个 vector 记录即可。
其它都是差不多的,放一下 Update 函数的代码:
void update(){
int x = t;
queue<int> tmp;
while (x){
int o = pre[x];
Dis[o] -= dis[t];
Dis[o ^ 1] += dis[t];
tmp.push(x);
x = lst[o ^ 1];
}
while (tmp.size()){
int o = tmp.front();
tmp.pop();
if (1 <= o && o <= n){
ans[x].push_back(o);
}else if (n + 1 <= o && o <= k + n){
x = o - n;
}
}
}
Dinic 算法
首先 EK 算法在绝大多数情况下跑的很快,但在
所以当
事实上绝大多数题目直接用 Dinic 就够了。
Dinic 算法的核心是 DFS。但是它还是要用到 BFS。
其核心逻辑是:
1.首先利用 BFS 求出分层图。由于每一个点一次最多在一条增广路上,所以每个点在一次增广中的层级是不变的;
2.然后 DFS 按照分层图的顺序去求这条增广路的流量。
具体细节结合代码来看吧:
BFS 部分:
bool bfs(){
for (int i = 0; i <= n; i++){
level[i] = 0;
}
level[s] = 1;//分层,初始设为 1,vis 数组就可以顺便省了
queue<int> b;
b.push(s);
while (b.size()){
auto x = b.front();
b.pop();
for (auto v : g[x]){
int w = Dis[v.id];
if (!w || level[v.v]){
continue;
}
level[v.v] = level[x] + 1;//v.v 是 x 在分层图上的下一个点
if (v.v == t){
return 1;
}
b.push(v.v);
}
}
return 0;
}
DFS 部分:
int dinic(int x, int f){//当前在点 x,流量为 f
if (x == t){//到汇点直接返回 w
return f;
}
int rest = f, inc;//rest 是当前还剩下的流量,f - rest 就是这次增广跑出来的流量
for (auto v : g[x]){
int w = Dis[v.id];
if (!w || level[v.v] != level[x] + 1){//没有流量或者 v.v 不是 x 在分层图的下一个点
continue;
}
inc = dinic(v.v, min(rest, w));
if (!inc){//没有流量 level[v.v] 就设为 0,之后不可能再跑 v.v
level[v.v] = 0;
}
rest -= inc;
Dis[v.id] -= inc;
Dis[v.id ^ 1] += inc;//修改 rest 和边的流量
}
return f - rest;
}
主函数:
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m >> s >> t;
cnt = -1;
while (m--){
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
add(v, u, 0);
}
while (bfs()){
ans += dinic(s, 1e18);
}
cout << ans;
return 0;
}
然后 Dinic算法就讲完了,撒花✿✿ヽ(°▽°)ノ✿
这样的 Dinic 的时间复杂度其实是没有保证的。
我们考虑这种图:
(当然这张图还不够大,不过假如链再长一些,连接
此时我们的算法会每次从
这样时间复杂度会直接炸掉。
怎么去优化呢?我们在此引入一个新的方法:当前弧优化。
事实上「当前弧优化」就是「当前边优化」,只是翻译过来变了,然后就没改了。
当前边优化的内容也十分简单。
我们考虑当前面的边已经被榨干了的时候前面的边再去跑一遍是不优的。
所以我们记录当前跑到了第几条边,然后之后从下一条边开始跑就好。
代码中体现出来就是这样:
for (int &i = tmp[x]; i < g[x].size(); i++){
// Do something.
if (!rest){
break;
}
}
千万注意,在用了当前弧优化后,一定要记得判
那么此时 Dinic 的时间复杂度就为
然后
同时判断的代码最好放循环最后,详见 Link 或保存站:Link。
然后真的就讲完了。
以下是例题部分。
洛谷 P2774 方格取数问题
简要题意:
先引入一个定义:最小割。
在一个网络流图中,我们可以钦定一些边的流量变为
当钦定完后
最小割就是使得
在此介绍一个定理:最大流最小割定理。
最大流最小割定理的内容是:一个网络流图的最小割等于最大流。
提供一个感性的证明:要想要
所以最小割等于最大流。
那么接下来看到这道题。
我们注意到相邻的格子都不能多选,于是就有了一个比较常用的 Trick:黑白染色。
我们将黑色的格子与
然后跑网络流即可。
主函数:
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
cnt = -1;
t = 1e5;
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
cin >> a[i][j];
sum += a[i][j];
if ((i + j) % 2){
add(s, i * m + j, a[i][j]);
add(i * m + j, s, 0);
}else {
add(i * m + j, t, a[i][j]);
add(t, i * m + j, 0);
}
}
}
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
for (int k = 0; k < 4 && (i + j) % 2; k++){
int dx = i + fx[0][k], dy = j + fx[1][k];
if (1 <= dx && dx <= n && 1 <= dy && dy <= m){
add(i * m + j, dx * m + dy, 4e18);
add(dx * m + dy, i * m + j, 0);
}
}
}
}
while (bfs()){
int x = dinic(s, 4e18);
sum -= x;
}
cout << sum;
return 0;
}
费用流
最小费用最大流
现在一条边不仅有流量限制,还有需要的费用。
你需要保证在最大流的基础上费用最小。
事实上实现的做法和纯最大流差不多。
在 EK 中,我们把最少步数换成最小长度(费用),把 BFS 换成 SPFA(因为可能有负边权)即可。
Dinic 同理改一下即可。
然后注意一下连反边的时候边权也要取相反数,应该很好理解。
板子:洛谷 P3381 【模板】最小费用最大流。
代码放一下吧:Link。
洛谷 P2517 [HAOI2010] 订货。
题意简述:第
由于本题实在没有什么其他状态,容易想到将每个月份作为点向源点连边,同时也向汇点连边。
那么向源点连边时流量为
然后向汇点连边时将流量设为
并且由于上个月的货可以留存到下个月,那么就可以直接让点
然后跑网络流即可。
主函数代码:
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m >> S;
for (int i = 1; i <= n; i++){
int x;
cin >> x;
add(i, t, x, 0);
}
for (int i = 1; i <= n; i++){
int x;
cin >> x;
add(s, i, 1e9, x);
}
for (int i = 1; i < n; i++){
add(i, i + 1, S, m);
}
while (bfs()){
update();
}
cout << cost;
return 0;
}
洛谷 P2053 [SCOI2007] 修车
题意简述:有
咕咕咕
上下界网络流
无源汇上下界可行流
1.没有指定源点和汇点;
2.每条边有流量的上界和下界;
3.每个点流入和流出的流量守恒,包括虚拟的源点和汇点。