【模板】最大流详细证明(Dinic+当前弧优化)(附:题- How Many to Be Happy?)

初识网络流-最大流==最小割

什么是网络流

网络流(network-flows)是一种类比水流的解决问题方法,与线性规划密切相关。网络流的理论和应用在不断发展。而我们今天要讲的就是网络流里的一种常见问题——最大流问题。
如下图:
在这里插入图片描述

最大流 (Dinic)

进而引出了最大流问题:从源点出发到达汇点,所能流入最大的流量。

增广路

在这里插入图片描述
那么我们如何知道S(源点)---->T(汇点)的最大流量呢?
我们的具体步骤是:
1.先找出一条增广路径,得到这条路径权值的最小值mins,之后该路径边上的流(权值)的值减去mins。
2.得到增广路径,使所有反向边的流(权值)加上mins。
3.继续重复1,2操作,使得不再找的出增广路径。
4.则此时的每条增广路径之和 就是源点到汇点的最大流量。
具体证明为什么这样操作可以得到最大流: 视频传送门:对增广路径懵懂可以看看 加深对此概念的理解
-------------------------------------------先鸽一鸽以后补---------------------------------------------------------------------------------------

步骤证明 来了来了~

令s到t的最大流为 maxflow.
现在我们需要做的是找出s到t点最多得不相交的路径。(这里的最多不相交路径数量就是最大流,但是呢每条路径权值是1而已~)

在证明之前我先把证明的公式给你们吧~:

  • 最小割 得出:用数学表达式表示就是:maxflow<=m
  • 增广路径&不相交路径. 得出: 数学表达式: k<=maxflow.
  • 证明k=m, 就得出 maxflow=k=m。 发现了吧!最小割和最大流是一回事嗷 ~

如下图:
在这里插入图片描述
在这张图中我们显然知道s到t有两条不相交的路径,为:s-a-b-c-t 和s-d-b-t。
那么对于计算机来说他是怎么执行 得知有两条不相交路径呢?
接下来引入伟大的 Ford-Fulkerson算法(FFA)
传送门
看了和没看没啥区别~ ,还是不懂呢, 我大概总结一下他要做什么,通过找出最小割的上界(至多)和不相交路径的下界(至少)两者逼近得到最大流。
现在还是没听懂(太正常了嘞~)
且听我慢慢细说~

找出s到t的最小割

什么是最小割呢, 拿上图为例。

要找s到t的最小割。 首先我们引入两个集合x,y,集合里存放顶点。
x存放s和其他点,y存放t和其他点。 且x∩y=空 ,x∪y=全顶点。

而割是什么意思呢?

就是x集合的点到y集合点的次数。 (注意是x到y的方向,这点很重要)
比如: x={s,d} y={a,b,c,t}; 这样的割的数量就是3. 分别是s-a d-a 和d-b.
再如 x={s,d,c} y={a,b,t}; 这样的割的数量即是4.分别是 s-a,d-a,d-b,c-t.
再如 x={s, } y={a,d,b,c,t} ; 同理割为2. 即是 s-a,s-d.
等等。。。。
从这里面:我们要在对于任意一对(x,y)中找出其中割的数量最小,并记为m
在上图中任意一对(x,y),我们很明显知道s到t的最小割2. 即是我们找不出比2还小的割了。

那么我们为什么要求出最小割呢??? 其实发现了吗~ 当我求出s到t的最小割的时候,就已经知道了

s到t的不相交路径 至多就只能有2条(m条)。
这里用数学表达式表示就是:maxflow<=m (关键不等式!!!!)
认真思考一下(ps:别忘了前面的x,y集合的定义。 x中有s点,y有t点嗷).

增广路径&不相交路径

怎么理解为什么引入增广路径呢?
就是随便找出一条能从s到t的一条路径,然后将该路径的所有边反向即可。
下次再找s到t路径时,我们也可以沿着前面反向的边路过~ (即是下图右边的p1,p2,p3变化,是不是好妙啊~~)
直到找不到s到t的路径。这个时候我们记录一下 共有这样的路径共有k条。
那么k的有什么意义呢?
至少我们知道了: 至少有k条不相交的路径能从s到达t~。
数学表达式: k<=maxflow.
如下图:
左边S都是同一个点,他有很多边。 在上述中每找到一条增广路径,将此路径所有边的方向反向.
在下一次寻找S到T的路径时,如下图的过程。。。。
在这里插入图片描述

证明k =m,得出maxflow=k=m

残留网络就是:上图的右边那个图。
现在我们知道了在残留网络上我们找不到一条s到t的路径了。
所以残留网络中 t到s的割为0.
因此我们只要证明p1,p2,p3,p4恰好只穿过s-t一次。我就得到了大小是4的割。
而这我们使用反证法容易得到: 如下图,要是一条p路径多次穿过与我们的残留网络中 t到s的割为0.
矛盾,即p路径只穿过一次。
从而我们证明了 当算法终止时,有k条路径。 (即是至多有k不相交的路径).
进而得出了k即是s到t的最小割。得证k==m。(最小s-t割 = 不相交的最大数量).
即得出了maxflow=k=m。
在这里插入图片描述
至此证明结束~~。

代码该怎么想呢??

现在我们的目标是找出S到T的最大流
  • 首先加入边,权值即是该边的流, 然后再添加反向边 但是流为0 (为什么是0呢,因为反向边本来就没权值,那为什么要添加呢??因为后面增广路径需要反向边。)
  • 每一次BFS一次找出一条路径然后再DFS, DFS修改流值。
int ans=0;
 while(BFS(S,T)){
            ans += DFS(S, T, 1ll << 40);
    }

为什么要先BFS呢? 如果直接DFS寻找路径找得重复而且复杂模糊,这时就需要预处理一下,标记每个点的深度。
之后的DFS该怎么搜索就会更加清晰明确了。

传送门:【模板】 最大流

题意

给出n个点,m条边, S源点,T汇点,问S到T的能通的最大流量是多少?(m条有向边)

思路

上述已分析:
不过注意下:会卡时
还需要对Dinic算法进行优化,
具体是:

当前弧优化。

什么是当前弧优化呢?注意在DFS中用cur[x]表示当前应该从x的编号为cur[x]的边开始访问,也就是说从0到cur[x]-1的这些边都不用再访问了,相当于删掉了,达到了满流。DFS(x,a)表示当前在x节点,有流量a,到终点t的最大流。当前弧优化在DFS里的关键点在if(a==0) break;也就是说对于结点x,如果x连接的前面一些弧已经能把a这么多的流量都送到终点,就不需要再去访问后面的一些弧了,当前未满的弧和后面未访问的弧等到下次再访问结点x的时候再去增广。
举个栗子~ (有图更直观)
如下图:
每执行一次BFS之后:
我们会得到一系列各点的深度。
然后在DFS进行中:
最先我们会搜索蓝线的路径。发现到达不了t点。(也就是说明蓝线是"死路")。
回退到1点。
再是走红色的路线,当红线走时,可能会走蓝线的路径,
但是我们知道呀,蓝线我们都已经走过了并且是明白蓝线的路径是不可能到达t点。
而当前弧优化就是好在这里,不用重复探索已经探索的点(避免红线再走蓝线的路线)。
而弧优化即是优化在这,避免蓝色的线再走红色线的"死路". 达到不重复,不走死路,而节省很多时间。
红线经过一系列的寻找顺利到达t点,DFS结束。
在这里插入图片描述

模板code: (详解)

#include<iostream>
#include<algorithm>
#include<math.h>
#include<cstring>
#include<vector>
#include<queue>
#include<set>
#define ll long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=1e4+10;
const ll mod=1e9+7;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
//建图
int n, m; //n点 m边
// cnt:计数  from:存放索引  to:第cnt条边的v head:记录 u点最后的计数cnt
ll cnt, from[N], to[N], head[N];
// 流(权值)
ll flow[N];
// 弧优化所需要的"记忆"数组
int cur[N];
//添加边
void addedge(int u,int v,int w){
    from[cnt] = head[u];
    to[cnt] = v;
    flow[cnt] = w;
    head[u] = cnt++;
}
queue<int> q;  
int level[N];  //标记点的深度
bool BFS(int S,int T){
     //每次BFS都要 初始化
    while(!q.empty()){
        q.pop();
    }
    rep(i, 1, n) level[i] = 0;
    q.push(S);
    level[S] = 1;
    while(!q.empty()){
        int u = q.front();
        q.pop();
        for (int i = head[u]; i != -1;i=from[i]){
            int v = to[i];
            //判断边是否可行   v点没有被标记深度 而且这条边是有流的
            if(level[v]==0&&flow[i]){
                level[v] = level[u] + 1;
                q.push(v);
                if(v==T)
                    return true;
            }
        }
    }
    return false;
}
ll DFS(int u,int T,ll minf){
    //递归出口
    if(u==T)
        return minf;
    ll res = 0;
    // 这里的int &i 与int i 差别很大!!!!!!!!
    // int &i  这边采用地址引用,i更改cur也更改 
    // 这么理解 在下次DFS后 可能会是的cur[u] 的值改变 从而改变当前的i值。
    // cur[u]变 i也变。   有点难想到~
    for (int &i = cur[u]; i != -1;i=from[i]){
        int v = to[i];
        //判断边可行
        if (level[v] == level[u]+1 && flow[i]){
            ll ans = DFS(v, T, min(flow[i], minf));
           //回退操作  ;流改变
            flow[i] -= ans;
            flow[i ^ 1] += ans;
            res += ans;
            minf -= ans;
            if(minf==0)
                return res;
        }
    }
    return res;
}
ll maxflow(int S,int T){
    ll ans = 0;
    //层次化
    while(BFS(S,T)){
        for (int i = 1; i <= n;i++){
            cur[i] = head[i];
        }
            ans += DFS(S, T, 1ll << 40);
    }
    return ans;
}
void solve(){
    n = read();
    m = read();
    ll u, v, w;
    int S, T;
    S = read();
    T = read();
     //初始化
    memset(head, -1, sizeof(head));
    rep(i, 1, m)
    {
        u = read();
        v = read();
        w = read();
        addedge(u, v, w);
        addedge(v, u, 0);
    }
    ll sum = maxflow(S, T);
    cout << sum << endl;
    return;
}
int main (){
    solve();
    getchar();
    return 0;
}

-----------------------------------------------------------------------------分鸽线--------------------------------------------------------------------------------
以后补:~

题-How Many to Be Happy?

传送门

题意:找最小生成树 ,但是呢有要求, 需要满足一条边在最小生成树里,所需要删掉的最小多少条边数才能满足该边在最小生成树里。
然后对任意的一条边都这样执行, 对所有的 "删掉的最小边数"求和。
思路:克鲁斯算法 和 最大流问题。
code:

#include<iostream>
#include<algorithm>
#include<math.h>
#include<cstring>
#include<vector>
#include<queue>
#include<set>
#define ll long long
#define rep(i, a, b) for (int i = a; i <= b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=1105;
const ll mod=1e9+7;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
int n, m;
int cnt, head[N], from[N], to[N];
int flow[N];
struct zw {
    int u, v, w;
} a[N];
void addedge(int u,int v,int w){
    from[cnt] = head[u];
    to[cnt] = v;
    head[u] = cnt++;

    from[cnt] = head[v];
    to[cnt] = u;
    head[v] = cnt++;
}
bool cmp(zw a,zw b){
    return a.w < b.w;
}
queue<int> q;
int level[N];
//只标记一条S到T的路径   找到了就立马返回 找不到就返回0
int BFS(int S,int T){
    //初始化
    while(!q.empty())
        q.pop();
    memset(level, 0, sizeof(level));
    rep(i, 0, cnt+1) level[i] = 0;
    level[S] = 1;
    q.push(S);
    while(!q.empty()){
        int u = q.front();
        q.pop();
        for (int i = head[u]; i != -1;i=from[i]){
            int v = to[i];
            //对当前路径  判断能不能走
            if(!flow[i]||level[v])
                continue;
            q.push(v);
            level[v] = level[u] + 1;
            //找到了就立马返回
            if(v==T)
                return 1;
        }
    }
    //到达不了目标点
    return 0;
}
//DFS更新flow的情况  源点 汇点 流量
int DFS(int u,int T,int minf){
    if(u==T)
        return minf;
    int sum = 0;
    for (int i = head[u]; i != -1;i=from[i]){
        int v = to[i];
        //dfs找出一条路径,先判断一下该路径能不能走
        if (!flow[i] || level[v] != level[u] + 1)
            continue;
        //可行
        int ans = DFS(v, T, min(minf, flow[i]));
        flow[i] -= ans;
        //前面已经讲了 重边 前面的addedge重边一起加
        flow[i^1] += ans;
        minf -= ans;
        sum += ans;
        if(minf==0){
            return sum;
           
        }
    } 
   // cout << "我被打印了" << endl;
    return sum;
}
int  dinic(int S,int T){
    //初始化
    rep(i,0,cnt){
        flow[i] = 1;
    }
    int ans = 0;
    while(BFS(S,T)){
       // cout << "dada" << endl;
        ans += DFS(S, T, 10000);
    }
    return ans;
}
int main (){
    n = read();
    m = read();
    rep(i,1,m){
        a[i].u = read();
        a[i].v = read();
        a[i].w = read();
    }
    //初始化
    rep(i, 0, m + m + 10) head[i] = -1;
    sort(a + 1, a + 1 + m, cmp);
    int r = 1;
    int sum = 0;
    rep(l,1,m){
       while(a[r].w<a[l].w){
           addedge(a[r].u, a[r].v, 1);
           r++;
       }
       sum += dinic(a[l].u, a[l].v);
    }
    cout << sum << endl;
   // getchar();
    //getchar();
    return 0;
}

参考资料

千杯湖底沙:最大流算法之三——Dinic算法的优化——当前弧优化
小小小小葱:Dinic+当前弧优化
zxyoi_dreamer:【模板】最大流Dinic算法
stevensonson:网络流的最大流入门(从普通算法到dinic优化)
mlm5678:Dinic & 当前弧优化

参考视频

网络流基础:理解最大流/最小割定理 (蒋炎岩)

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

axtices

谢谢您的打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值