网络流学习笔记
网络流
何为网络流
想要弄清楚网络流,首先要知道网络的概念,通常在运筹学中,网络是指一个有向图$G\ =\ (V,E)$ 。其每条边$(u,v)\in E$都有一个权值$c(u,v)$,称为这条边的流量(Capacity),还有两个特殊的点,一个是源点(Source),一个是汇点(Sink)在图论中,网络流(英语:Network flow)在作为网络的有向图中分配流,使一条边的流量不会超过它的容量。
网络流的性质
1.容量限制
$\ \ \ \ $ 对于有向图\(G\)中的每一条边上所流经的流量不得超过其容量,即 \(f(u,v) \ ≤ \ c(u,v)\) 。
2.斜对称性
$\ \ \ \ $ 每条边的流量与相反边的流量之和为0,即 \(f(u,v) \ = \ -f(u,v)\)。
3.流守恒性
$\ \ \ \ $ 从源点流出的流量等于汇点流入的流量,即 \(\forall x\in V-\{s,t\},\sum_{(u,x)\in E}f(u,x)=\sum_{(x,v)\in E}f(x,v)\)
$\ \ \ \ $ 现在想象下面一个情景,工厂里有一个运输液体的传输管道,工厂在每条管道的都设置了防止倒流的装置,因为压强问题,所以每条管道在单位时间内的容量有限,这就是一个网络流模型了。
事实上,网络流的应用已遍及通讯、运输、电力、工程规划、任务分派、设备更新以及计算机辅助设计等众多领域。
网络流的常见问题
$\ \ \ \ $ 网络流问题中常见的有以下三种:最大流,最小割,费用流。
最大流
$\ \ \ \ $ 顾名思义,就是求从源点到汇点的最大流量。下面介绍几种常见的方法,其中Dinic算法几乎能完成所有与最大流相关的问题,因为他的复杂度比较优秀。
Ford-Fulkerson增广
$\ \ \ \ $ Ford-Fulkerson增广是计算最大流算法的一类统称,核心思想是贪心,通过寻找增广路来更新并求解最大流。其中,寻找增广路其实就是寻找从源点到汇点的剩余容量非空的路径,对于一条增广路,我们给每一条\((u,v)\)都加上等量的流量,以令整个网络的流量增加,这一过程被称为增广(Augment)。,但是,在执行过程中,会发现这样的思路有可能导致增广了一些原来不应存在于最大流的路径,怎么办?
$\ \ \ \ $ 我们可以利用网络流的斜对称性,在增广时进行退流操作,如果增广出了\((u,v)\)之间的双向流量实际上可以将经过\((v,u)\)的流量交换来抵消成单向流量。
退流操作带来的「抵消」效果使得我们无需担心我们按照「错误」的顺序选择了增广路。
接下类就是对Ford-Fulkerson增广算法的实现了,对于 Ford–Fulkerson 增广的不同实现,时间复杂度也各不相同。其中较主流的实现有 Edmonds–Karp, Dinic, SAP, ISAP 等算法,我会将自己理解了一些的算法介绍出来,至于其他的,下次一定(doge。
EK算法(Edmonds–Karp)
算法思想
EK算法就是通过BFS不断寻找增广路并不断更新最大流,直至在网络中再也找不到增广路为止。上面讲过,仅仅进行增广操作的话,最后得到的答案是错误的,因此需要退流,而为了追求速度,我们希望能在最短时间找到反向边,我们发现,利用邻接矩阵可以快速的找到反向边,因为邻阶矩阵就具有对称性,但是因为邻接矩阵相当于一个二维数组,无论是空间上还是时间上都不是很好,因此在网络流中的通常使用链式前向星来存储。其中,一个常用的技巧是,我们令边从偶数(通常为 0)开始编号,并在加边时总是紧接着加入其反向边使得它们的编号相邻。由此,我们可以令编号为 \(i\) 的边和编号为 $i \oplus 1 $的边始终保持互为反向边的关系。
存边代码如下:
inline void addedge(int u,int v,int c){
edge[++tot].c = c;
edge[tot].to = v;
edge[tot].from = head[u];
head[u] = tot;
edge[++tot].c = 0;
edge[tot].to = u;
edge[tot].from = head[v];
head[v] = tot;
}
更新代码如下:
inline void update(){
int x = t;
while (x != s){
int v = pre[x];
edge[v].c -= dis[t]; //c是剩余容量
edge[v^1].c += dis[t];
x = edge[v^1].to;
}
ans += dis[t];
}
适用范围
时间复杂度为\(O(nm^2)\),一般能处理\(10^3 ~ 10^4\)规模的网络。
完整代码 \(Code\)
#include <bits/stdc++.h>
#define MAXN 505050
#define int long long
using namespace std;
inline int read(){
int x = 0,f = 1;
char c = getchar();
while (!isdigit(c)){
if (c == '-'){
f = -1;
}
c = getchar();
}
while (isdigit(c)){
x = x*10+c-'0';
c = getchar();
}
return x*f;
}
int n,m,s,t;
int tot = 1,vis[MAXN],pre[MAXN],head[MAXN],flag[2500][2500];
int dis[MAXN];
int ans;
struct node {
int from,to,c,flow;
}edge[MAXN];
inline void addedge(int u,int v,int c){
edge[++tot].to = v;
edge[tot].from = head[u];
edge[tot].c = c;
head[u] = tot;
edge[++tot].to = u;
edge[tot].from = head[v];
edge[tot].c = 0;
head[v] = tot;
}
inline bool bfs(){
for (int i=1;i<=n;i++){
vis[i] = 0;
}
queue<int> q;
q.push(s);
vis[s] = 1;
dis[s] = 2005020600;
while (!q.empty()){
int x = q.front();
q.pop();
for (int i=head[x];i;i=edge[i].from){
int v = edge[i].to;
if (!edge[i].c) continue;
if (vis[v]) continue;
dis[v] = min(dis[x],edge[i].c);
pre[v] = i;
q.push(v);
vis[v] = 1;
if (v == t)return 1;
}
}
return 0;
}
inline void update(){
int x = t;
while (x!=s){
int v = pre[x];
edge[v].c-=dis[t];
edge[v^1].c+=dis[t];
x = edge[v^1].to;
}
ans+=dis[t];
}
signed main(){
n = read(),m = read(),s = read(),t = read();
for (int i=1;i<=m;i++){
int u = read(),v = read(),w = read();
if (flag[u][v] == 0){
addedge(u,v,w);
flag[u][v] = tot;
}
else {
edge[flag[u][v]-1].c+=w;
}
}
while (bfs()!=0){
update();
}
cout<<ans<<'\n';
return 0;
}
Dinic算法
Dinic算法在大部分图上的效率都十分优秀,因此也算是处理网络流问题中较为常见的,其时间复杂度是\(O(|V|^2|E|)\) 因为本人太菜不会证明,故不列出证明(
算法思想
EK算法可能会遍历整个残量网络(所有边均为其残量的 \(G_f(V,E_f)\)),而只为寻找一条增广路,显然只开了单线程,那么我们能不能找到一种方法,像开多线程一样,一次寻找多条增广路呢?
这时候就要我们的Dinic算法出马了。
分层图&\(DFS\)
考虑在进行增广前先对网络进行\(BFS\)分层,即根据节点到源点的的距离(dis)把节点分成若干层。对于每一个节点\(u\),我们用\(d(u)\)来表示他的层次,即\(s\)到\(x\)的最少边数,在残量网络中,满足\(d(u) = d(u)+1\)的边\((u,v)\)构成的子图被称为分层图,而分层图显然是一张有向无环图(Directed acyclic graph),为什么要构建分层图呢?在解释原因前,要先了解Dinic算法还要通过\(DFS\)进行增广,在\(DFS\)中,从\(S\)开始,每次我们从下一层的随机一个点开始跑,就这样一直跑到汇点,然后再一层层回溯回去,继续找这一层的另外的点再往下搜索,显然的,这样操作可以保证同时多增广的需求,然后对于每一层的最大流我们再进行合并,就能求出该网络图的最大流了。
算法框架
1.在残量网络上BFS求出节点的层次,构造分层图
2.在分层图上DFS寻找增广路,在回溯时同时更新边权
适用范围
时间复杂度为\(O(n^2m)\),一般能处理\(10^4 ~ 10^5\)的网络。
同时,Dinic算法还能用来求二分图,复杂度比匈牙利算法低,但还没学,所以在这就不介绍了。
代码 \(Code\)
#include <bits/stdc++.h>
using namespace std;
const long long inf=2005020600;
int n,m,s,t,u,v;
long long w,ans,dis[520010];
int tot=1,now[520010],head[520010];
struct node {
int to,net;
long long val;
} e[520010];
inline void add(int u,int v,long long w) {
e[++tot].to=v;
e[tot].val=w;
e[tot].net=head[u];
head[u]=tot;
e[++tot].to=u;
e[tot].val=0;
e[tot].net=head[v];
head[v]=tot;
}
inline int bfs() { //在惨量网络中构造分层图
for(register int i=1;i<=n;i++) dis[i]=inf;
queue<int> q;
q.push(s);
dis[s]=0;
now[s]=head[s];
while(!q.empty()) {
int x=q.front();
q.pop();
for(register int i=head[x];i;i=e[i].net) {
int v=e[i].to;
if(e[i].val>0&&dis[v]==inf) {
q.push(v);
now[v]=head[v];
dis[v]=dis[x]+1;
if(v==t) return 1;
}
}
}
return 0;
}
inline int dfs(int x,long long sum) { //sum是整条增广路对最大流的贡献
if(x==t) return sum;
long long k,res=0; //k是当前最小的剩余容量
for(register int i=now[x];i&∑i=e[i].net) {
now[x]=i; //当前弧优化
int v=e[i].to;
if(e[i].val>0&&(dis[v]==dis[x]+1)) {
k=dfs(v,min(sum,e[i].val));
if(k==0) dis[v]=inf; //剪枝,去掉增广完毕的点
e[i].val-=k;
e[i^1].val+=k;
res+=k; //res表示经过该点的所有流量和(相当于流出的总量)
sum-=k; //sum表示经过该点的剩余流量
}
}
return res;
}
int main() {
scanf("%d%d%d%d",&n,&m,&s,&t);
for(register int i=1;i<=m;i++) {
scanf("%d%d%lld",&u,&v,&w);
add(u,v,w);
}
while(bfs()) {
ans+=dfs(s,inf); //流量守恒(流入=流出)
}
printf("%lld",ans);
return 0;
}
费用流
费用流就是最小费用最大流,意思就是在之前的情景下,对于每一根管道,我们都给他加上一个代价,就是每经过一条管道都会花费相应的费用,然后你要在这其中找到一条保证最小花费的最大流。
聪明的读者是不是已经大概知道怎么做了呢