网络流初步(脑部整理)
呜呜呜,家人们也是学上网络流了。
咸鱼起手,你反应得过来吗?
英语不太好(老英不会看窝博客吧)
What is 网络流?
概述
网络\((network)\)是指一个特殊的有向图 \(G=(V,E)\),其与一般有向图的不同之处在于有容量和源汇点。
- $E $中的每条边 $ (u, v)$ 都有一个被称为容量 \((capacity)\)的权值,记作 $ c(u, v)$。当 \((u,v)\)
$\notin E $时,可以假定 \(c(u,v)=0\)。- $V $ 中有两个特殊的点:源点 \((source)s\) 和汇点 \((sink)t(s \neq t)\)。
对于网络 $ G=(V, E)$,流 \((flow)\) 是一个从边集 E 到整数集或实数集的函数,其满足以下性质。
- 容量限制:对于每条边,流经边的流量不得超过该边的容量,即 $ 0 \leq f(u,v) \leq c(u,v)$ ;
- 流守恒性:除源汇点外,任意结点$ u$ 的净流量为$ 0$。
其中,我们定义 $u $的净量为 \(f(u) = \sum_{x \in V} f(u, x) - \sum_{x \in V} f(x, u)\)。对于网络$ G = (V, E) $和其上的流 \(f\),我们定义 $ f$ 的流量 $ |f| $为
$s $的净流量 $ f(s)$。作为流守恒性的推论,这也等于 $ t$ 的净流量的相反数 \(-f(t)\)。对于网络 \(G = (V, E)\),如果 ${S, T} $是 \(V\) 的划分(即 \(S \cup T = V 且 S \cap T = \varnothing\)),且满足$ s \in S, t \in T$,则我们称 \(\{S, T\}\) 是 \(G\) 的一个 \(s-t\) 割(cut)。我们定义 \(s-t\) 割$ {S, T}$ 的容量为 \(||S, T|| = \sum_{u \in S} \sum_{v \in T} c(u, v)\)。
--以上摘自\(OI-Wiki\)。
靠! 什么玩意?都是中文咋看不懂?
于是我们找到了启蒙例(ti)题(jie)。然后偷税得发现,其实就是一堆的水管,每个水管有相应的容量\((c)\),并且水管里的水只能单向流动,接着让我处理一些有关流量\((f)\)的头大有趣问题。
可以发现,对于一张网络,有这么几个重点。
- 容量限制:$f(u,v) \leq c(u,v) $。一条边上的流量不能大于水管容量(
不然就要爆炸)。- 相反净流量:\(f(u,v)=-f(v,u)。\)由\(u\)到\(v\)的净流量必须是\(v\)到\(u\)净流量的相反数。
- 流守恒性:除非\(u=s\)或\(u=t\),否则\(\sum_{x \in V} f(u, x)=0\)。一个非源非汇的点的净流量一定是\(0\)。
最大流
根据名字显然可以知道最大流就是最大流量的意思
其实就是以下意思:
【模板】网络最大流
题目描述
如题,给出一个网络图,以及其源点和汇点,求出其网络最大流。
输入格式
第一行包含四个正整数 \(n,m,s,t\),分别表示点的个数、有向边的个数、源点序号、汇点序号。
接下来 \(m\) 行每行包含三个正整数 \(u_i,v_i,w_i\),表示第 \(i\) 条有向边从 \(u_i\) 出发,到达 \(v_i\),边权为 \(w_i\)(即该边最大流量为 \(w_i\))。
输出格式
一行,包含一个正整数,即为该网络的最大流。
样例 #1
样例输入 #1
4 5 4 3
4 2 30
4 3 20
2 3 20
2 1 30
1 3 30
样例输出 #1
50
提示
样例输入输出 1 解释
题目中存在 \(3\) 条路径:
- \(4\to 2\to 3\),该路线可通过 \(20\) 的流量。
- \(4\to 3\),可通过 \(20\) 的流量。
- \(4\to 2\to 1\to 3\),可通过 \(10\) 的流量(边 \(4\to 2\) 之前已经耗费了 \(20\) 的流量)。
故流量总计 \(20+20+10=50\)。输出 \(50\)。
数据规模与约定
- 对于 \(30\%\) 的数据,保证 \(n\leq10\),\(m\leq25\)。
- 对于 \(100\%\) 的数据,保证 \(1 \leq n\leq200\),\(1 \leq m\leq 5000\),\(0 \leq w\lt 2^{31}\)。
青铜 — — Ford-Fulkerson算法(增广路算法)
首先我们先找到一条从\(s\)到\(t\)容量不为零的路径,就是传说中的增广路
,然后把一条条增广路搞加在一起,似乎就可以得到一个答案。
但如果选择的路径较劣就会影响最后的答案。
那怎么办呢?增广路真的一点鸟用都没有吗?
当然,部分人类凭借高级的大脑想出了处理方法——建一条反方向的钟边。正向边减少的同时给反向边进行增加,即利用\(f(u,v)=-f(u,v)\)这样就可以实现类似于反悔贪心的操作,至于正确性。先引入一个叫做残量网络
的概念。可以理解为把满流的边扔掉后,边权为剩余流量的图。(包括反向边)。当在残量网络中流过一条反向边时,相当于将原先的流压了回去,而通过列举简单的例子,可以发现流经反向边事实上是一个反悔的等价操作。
每次只需要在残量网络中找增广路即可。
局限性
但这样跑肥肠靠RP,只需要简单造造数据就可以把算法卡成 \(O(值域)\) 的,显然是指数级算法。
那这算法有个蛋用啊?表演T飞吗?但这个算法可以打暴力通过合理优化达到高效的运行。但增广路算法是基础。
Edmonds-Karp算法(EK算法)
显然地,我们可以每次增广最短路,把最短路压榨干净后,这条路就在残量网络中OUT了。而这条跑满流的边也就是关键边。
每一个增广路意味着至少有一条关键边出现,而增广过程的迭代次数也就可以看做是每条边成为关键边的次数。
这样总迭代次数为 \(O(V\cdot E)\) ,加上0/1最短路的 \(O(E)\),总复杂度也就是 \(O(V \cdot E^{2})\)。
局限性
因为 \(O(E)\) 与 \(O(V^{2})\)同阶,那复杂度也可以看做是 \(O(V^{5})\)。
这时候就有人说了:虽然复杂度已经降到了一个多项式的程度,但还是难以让人接受,有没有更快更好的算法呢?
有的,有的,包有的兄弟。
咱先想想EK为啥不够快:
我们可以很显然地艰难地发现,EK每次只会跑一条最短路,但我们最短路可能不止一条啊。
基于这一点,我们继续进行优化。
Dinic算法
没有什么是一个BFS或一个DFS解决不了的;如果有,那就两个一起。
Dinic的核心思想就是在EK的基础上进行多路增广。
这需要搞出分层图,而这里分层图的定义可以理解为:对于每个点,到达源点s的最短路大小。也可以认为是一个点的深度。
分完层之后有什么好处呢?首先层内边和反向边都会被无视掉。
其次我们可以发现,此时所有的最短路都在图上呈现。这样DFS一遍就能把这张图上的最短路全都干掉。
似乎这样可以跑得飞快。
但是的但是,我们会重复搜到同一个点,但通过这个点的流量不一定已经到达最大值,所以不能直接标记掉,但无标记的DFS直接爆炸。
so 优化了个寂寞???
No!No!No!
我们再来加两只优化(咋这么多优化)。
代码解释。
struct node{int to,c,lp;};
vector<node>g[N];
int dep[N],pos[N];
int n,m,S,T;
inline void add(int from,int to,int c)//存边包括正向边和反向边
{g[from].push_back((node){to,c,(int)g[to].size()});//正向g[to].push_back((node){from,0,(int)g[from].size()-1});//反向,g[from]已经加入新值,下标size-1
}
inline void fc(int s)//深度信息
{for(int i=0;i<=n;i++)dep[i]=-1;queue<int>q;//储存节点编号dep[s]=0;q.push(s);while(!q.empty()){int u=q.front();q.pop();for(auto v:g[u]){if(v.c>0&&dep[v.to]<0)//容量大于0且没被计算过{dep[v.to]=dep[u]+1;q.push(v.to);}}}
}
inline int dfs(int u,int t,int f)
{if(u==t||!f) return f;int ans=0;for(int &i=pos[u];i<g[u].size();i++)//当前弧优化,重复经过同一个节点时,跳过已经跑满的边,巧妙地更新pos值实现{auto &x=g[u][i];//g[u][i]的信息要被修改if(x.c>0&&dep[u]+1==dep[x.to])//有容量且层数相邻{int frz=dfs(x.to,t,min(f,x.c));if(frz>0){x.c-=frz;g[x.to][x.lp].c+=frz;f-=frz;ans+=frz;if(!f) break;//剪枝,现在找不到妹子,以后也找不到。}}}return ans;
}
inline ll dinic(int s,int t)
{ll max_flow=0;while(1){fc(s);if(dep[t]==-1)return max_flow;for(int i=0;i<=n;i++)pos[i]=0;max_flow+=dfs(s,t,inf); }
}
inline void solve()
{n=read();m=read();S=read();T=read();for(int i=1;i<=m;i++){int x,y,c;x=read();y=read();c=read();add(x,y,c);}write(dinic(S,T));
}
完整板子:
#ifdef ONLINE_JUDGE
#else
#define Qiu_Cheng
#endif
#include <bits/stdc++.h>
// #define int long long
using namespace std;
typedef long long ll;
const int N=205,mod=1e9+7,inf=INT_MAX;
// const int mod1=469762049,mod2=998244353,mod3=1004535809;
// const int G=3,Gi=332748118;
// const int M=mod1*mod2;
inline int read()
{int x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-'){f=-1;}c=getchar();}while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c-'0');c=getchar();}return x*f;
}
inline void write(ll x)
{if(x<0){putchar('-');x=-x;}if(x>9) write(x/10);putchar(x%10+'0');
}
struct node{int to,c,lp;};
vector<node>g[N];
int dep[N],pos[N];
int n,m,S,T;
inline void add(int from,int to,int c)
{g[from].push_back((node){to,c,(int)g[to].size()});g[to].push_back((node){from,0,(int)g[from].size()-1});
}
inline void fc(int s)//深度信息
{for(int i=0;i<=n;i++)dep[i]=-1;queue<int>q;//储存节点编号dep[s]=0;q.push(s);while(!q.empty()){int u=q.front();q.pop();for(auto v:g[u]){if(v.c>0&&dep[v.to]<0){dep[v.to]=dep[u]+1;q.push(v.to);}}}
}
inline int dfs(int u,int t,int f)
{if(u==t||!f) return f;int ans=0;for(int &i=pos[u];i<g[u].size();i++)//当前弧优化{auto &x=g[u][i];//g[u][i]的信息要被修改if(x.c>0&&dep[u]+1==dep[x.to])//有容量且层数相邻{int frz=dfs(x.to,t,min(f,x.c));if(frz>0){x.c-=frz;g[x.to][x.lp].c+=frz;f-=frz;ans+=frz;if(!f) break;//剪枝}}}return ans;
}
inline ll dinic(int s,int t)
{ll max_flow=0;while(1){fc(s);if(dep[t]==-1)return max_flow;for(int i=0;i<=n;i++)pos[i]=0;max_flow+=dfs(s,t,inf); }
}
inline void solve()
{n=read();m=read();S=read();T=read();for(int i=1;i<=m;i++){int x,y,c;x=read();y=read();c=read();add(x,y,c);}write(dinic(S,T));
}
signed main()
{
#ifdef Qiu_Cheng freopen("1.in","r",stdin); freopen("1.out","w",stdout);
#endif // ios::sync_with_stdio(false); //cin.tie(0); cout.tie(0); // int QwQ; // cin>>QwQ; // while(QwQ--)solve(); solve(); return 0;
}
//12 825076913 0 173167432
// 6666 66666 666666
// 6 6 6 6 6
// 6 6 6666 6
// 6 6 6 6 6
// 6666 6 6 6666666//g++ -O2 -std=c++14 -Wall "-Wl,--stack= 536870912 " cao.cpp -o cao.exe