网络流初步

网络流初步(脑部整理)

呜呜呜,家人们也是学上网络流了。

咸鱼起手,你反应得过来吗?

英语不太好(老英不会看窝博客吧)

What is 网络流?

概述

网络network是指一个特殊的有向图 G=(V,E),其与一般有向图的不同之处在于有容量和源汇点。

  • E中的每条边 (u,v) 都有一个被称为容量 capacity的权值,记作 c(u,v)。当 (u,v)
    E时,可以假定 c(u,v)=0
  • V 中有两个特殊的点:源点 sources 和汇点 sinktst

对于网络 G=(V,E),流 flow 是一个从边集 E 到整数集或实数集的函数,其满足以下性质。

  • 容量限制:对于每条边,流经边的流量不得超过该边的容量,即 0f(u,v)c(u,v)
  • 流守恒性:除源汇点外,任意结点u 的净流量为0

其中,我们定义 u的净量为 f(u)=xVf(u,x)xVf(x,u)。对于网络G=(V,E)和其上的流 f,我们定义 f 的流量 |f|
s的净流量 f(s)。作为流守恒性的推论,这也等于 t 的净流量的相反数 f(t)。对于网络 G=(V,E),如果 S,TV 的划分(即 ST=VST=),且满足sS,tT,则我们称 {S,T}G 的一个 st 割(cut)。我们定义 stS,T 的容量为 ||S,T||=uSvTc(u,v)

--以上摘自OIWiki

靠! 什么玩意?都是中文咋看不懂?

于是我们找到了启蒙例(ti)题(jie)。然后偷税得发现,其实就是一堆的水管,每个水管有相应的容量(c),并且水管里的水只能单向流动,接着让我处理一些有关流量(f)头大有趣问题。

可以发现,对于一张网络,有这么几个重点。

  • 容量限制:f(u,v)c(u,v)。一条边上的流量不能大于水管容量(不然就要爆炸)。
  • 相反净流量:f(u,v)=f(v,u)uv的净流量必须是vu净流量的相反数。
  • 流守恒性:除非u=su=t,否则xVf(u,x)=0。一个非源非汇的点的净流量一定是0

最大流

根据名字显然可以知道最大流就是最大流量的意思

其实就是以下意思:

【模板】网络最大流

题目描述

如题,给出一个网络图,以及其源点和汇点,求出其网络最大流。

输入格式

第一行包含四个正整数 n,m,s,t,分别表示点的个数、有向边的个数、源点序号、汇点序号。

接下来 m 行每行包含三个正整数 ui,vi,wi,表示第 i 条有向边从 ui 出发,到达 vi,边权为 wi(即该边最大流量为 wi)。

输出格式

一行,包含一个正整数,即为该网络的最大流。

样例 #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 条路径:

  • 423,该路线可通过 20 的流量。
  • 43,可通过 20 的流量。
  • 4213,可通过 10 的流量(边 42 之前已经耗费了 20 的流量)。

故流量总计 20+20+10=50。输出 50


数据规模与约定

  • 对于 30% 的数据,保证 n10m25
  • 对于 100% 的数据,保证 1n2001m50000w<231

青铜 — — Ford-Fulkerson算法(增广路算法)

首先我们先找到一条从st容量不为零的路径,就是传说中的增广路,然后把一条条增广路加在一起,似乎就可以得到一个答案。

但如果选择的路径较劣就会影响最后的答案。

那怎么办呢?增广路真的一点鸟用都没有吗?

当然,部分人类凭借高级的大脑想出了处理方法——建一条反方向的边。正向边减少的同时给反向边进行增加,即利用f(u,v)=f(u,v)这样就可以实现类似于反悔贪心的操作,至于正确性。先引入一个叫做残量网络的概念。可以理解为把满流的边扔掉后,边权为剩余流量的图。(包括反向边)。当在残量网络中流过一条反向边时,相当于将原先的流压了回去,而通过列举简单的例子,可以发现流经反向边事实上是一个反悔的等价操作。

每次只需要在残量网络中找增广路即可。

局限性

但这样跑肥肠靠RP,只需要简单造造数据就可以把算法卡成 O() 的,显然是指数级算法。

那这算法有个蛋用啊?表演T飞吗?但这个算法可以打暴力通过合理优化达到高效的运行。但增广路算法是基础。

Edmonds-Karp算法(EK算法)

显然地,我们可以每次增广最短路,把最短路压榨干净后,这条路就在残量网络中OUT了。而这条跑满流的边也就是关键边

每一个增广路意味着至少有一条关键边出现,而增广过程的迭代次数也就可以看做是每条边成为关键边的次数。

这样总迭代次数为 O(VE) ,加上0/1最短路的 O(E),总复杂度也就是 O(VE2)

局限性

因为 O(E)O(V2)同阶,那复杂度也可以看做是 O(V5)

这时候就有人说了:虽然复杂度已经降到了一个多项式的程度,但还是难以让人接受,有没有更快更好的算法呢?

有的,有的,包有的兄弟。

咱先想想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
posted @   Nightmares_oi  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示