网络最大流【附当前弧优化】

一篇巨佬的网络流建模

网络最大流问题就是给你一个有向图,告诉你一个源点与一个汇点,并给每一条边一个最大流量,需要你求出从源点最多能够发出多少单位流量到汇点(哎呀我也说不清,就是给你一些或大或小的管道(每个管道都有最大秒流量),一些中转站,一座供水塔以inf单位每秒的速度供水,问你家每秒最多得到多少单位水(中转站、供水塔、你家由管道连通))

很显然看起来我们可以从源点跑dfs,只要到下一节点的边的边权>0就跑到下一节点,一路跑过去,直到汇点,然后答案加上这一路上最小的管道流量,这一条路径上所有管道减去这个流量,然后就没了。

但是这样子是有问题的。(蓝模板怎么可能这么简单

举个栗子:(将dfs路径用红色表示)

 

 

 

然后你会发现这样的答案是2,然而正确的答案是4(s-2-t,s-3-t)

不要说你的存边是2-t比2-3优先,调个位置就卡掉了) 

为什么会这样?

因为上图中第一条dfs路径把原本应是第二条路径的一条边给“占用”了。

计算机可不是人,它无法判断到底该怎么跑dfs,所以这时候就需要我们人为地给计算机一个“反悔”的机会。

重点:怎么让计算机“反悔”?

给每条边建边权为0的反向边,当每次跑到汇点时,在回溯给dfs路径上的边权减去最小边权的时候还要给反向边加上最小边权。

先看效果:

 

然后答案就神奇地变成4了! 

为什么?

用整体的思想来看:如果我正着经过一条边,再反着经过,是不是相当于没经过?好吧,我承认这不是很好理解

这一种“抵消”的思想应用十分广泛,比如洛谷P1792 [国家集训队]种树,就是一种利用类似“抵消”的方法来实现的可反悔的贪心。

没理解也没关系,反正就是要反向建边,题做多了就理解了QAQ

使用了这种“抵消”思想的dfs,大概就是所谓的EK算法了。(即不停的寻找增广路然后操作)

然后再介绍一种优化算法:Dinic算法(本质上其实差不多,只不过是一次寻找多条增广路并更新图)

PS:增广路即能够使答案增加的dfs路径

大概步骤就是:

1.bfs——给之后寻找增广路的dfs提供一个bfs序作为扩展增广路的依据,同时判断是否还存在增广路(如果遍历不到汇点,就退出输出答案)。

2.dfs——寻找增广路(注意这里一次寻找了多条)并更新答案。

3.重复1

dinic的代码实现:(贴上我洛谷P3376 【模板】网络最大流的代码)

#include<stdio.h>
#include<string.h>
#include<iostream>
#define maxn 20010
#define maxm 200010
#define inf 0x3f3f3f3f
using namespace std;

int bg[maxn],nt[maxm],to[maxm],w[maxm],e=1;
int dep[maxn],q[maxn],n,m,s,t,ans;

void insert(int x,int y,int z) {
    nt[++e]=bg[x];
    to[e]=y;
    w[e]=z;
    bg[x]=e;
}

void bfs() {
    int i,f,l,u,v;
    f=l=1;
    q[1]=s;
    dep[s]=1;
    while (f<=l) {
        u=q[f++];
        for (i=bg[u];i;i=nt[i]) {
            v=to[i];
            if (dep[v] || !w[i]) continue;
            q[++l]=v;
            dep[v]=dep[u]+1;
        }
    }
}

int dfs(int x,int s) {
    int i,u,tmp,res=0;
    if (x==t) return s;
    if (!s) return 0;
    for (i=bg[x];i;i=nt[i]) {
        u=to[i];
        if (dep[u]==dep[x]+1) {
            tmp=dfs(u,min(s,w[i]));
            if (!tmp) continue;
            res+=tmp;
            s-=tmp;
            w[i]-=tmp;
            w[i^1]+=tmp;
            if (!s) break;
        }
    }
    return res;
}

int main() {
    int i,j,x,y,z;
    scanf("%d%d%d%d",&n,&m,&s,&t);
    for (i=1;i<=m;i++) {
        scanf("%d%d%d",&x,&y,&z);
        insert(x,y,z);
        insert(y,x,0);
    }

    while (1) {
        memset(dep,0,sizeof(dep));
        bfs();
        if (!dep[t]) break;
        ans+=dfs(s,inf);
    }
    printf("%d\n",ans);
    return 0;
}

例题链接:

洛谷P2764 最小路径覆盖问题 我的题解QwQ

洛谷P3355 骑士共存问题 暂时我没写题解QAQ

附:当前弧优化

听名字似乎是一个很高级的东西,但其实很简单,就几句话。。。

我们维护一个now数组,在每次dfs前把链式前向星的bg数组(或者是head)拷贝一份到now上,然后在dfs枚举边找下一个节点时循环枚举边的编号不再从bg[x]开始了,而从now[x]开始。而now[x]不断地更新,即i循环到哪,now[x]都更新为i。(具体实现还是看代码吧,我的语言表达能力一向不强

这样子可以大大提升程序效率。

代码:

#include<stdio.h>
#include<string.h>
#include<iostream>
#define maxn 20010
#define maxm 200010
#define inf 0x3f3f3f3f
using namespace std;

int bg[maxn],nt[maxm],to[maxm],w[maxm],e=1;
int dep[maxn],q[maxn],n,m,s,t,ans,now[maxn];

void insert(int x,int y,int z) {
    nt[++e]=bg[x];
    to[e]=y;
    w[e]=z;
    bg[x]=e;
}

void bfs() {
    int i,f,l,u,v;
    for (i=1;i<=n;i++) now[i]=bg[i];
    f=l=1;
    q[1]=s;
    dep[s]=1;
    while (f<=l) {
        u=q[f++];
        for (i=bg[u];i;i=nt[i]) {
            v=to[i];
            if (dep[v] || !w[i]) continue;
            q[++l]=v;
            dep[v]=dep[u]+1;
        }
    }
}

int dfs(int x,int s) {
    int i,u,tmp,res=0;
    if (x==t) return s;
    if (!s) return 0;
    for (i=now[x];i;i=nt[i]) {
        u=to[i];
        if (dep[u]==dep[x]+1) {
            tmp=dfs(u,min(s,w[i]));
            if (!tmp) continue;
            res+=tmp;
            s-=tmp;
            w[i]-=tmp;
            w[i^1]+=tmp;
            if (!s) break;
        }
        now[x]=i;
    }
    return res;
}

int main() {
    int i,j,x,y,z;
    scanf("%d%d%d%d",&n,&m,&s,&t);
    for (i=1;i<=m;i++) {
        scanf("%d%d%d",&x,&y,&z);
        insert(x,y,z);
        insert(y,x,0);
    }
    while (1) {
        memset(dep,0,sizeof(dep));
        bfs();
        if (!dep[t]) break;
        ans+=dfs(s,inf);
    }
    printf("%d\n",ans);
    return 0;
}

 实测效率截图O_O!:

优化之前:

之后:

 

 至于这个优化的正确性,蒟蒻引用一句很6的话(毕竟我描述不出来)——

“对于一次BFS而言,它确定的层次图中每条边若已经被走完了,那么它就不可能再带来增广,下一次就直接从这条最后没走完的边走就可以了。”

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值