网络流入门之最大流算法(增广路算法 由Ford-Fulkerson到Edmonds-Karp到dinic) 包看懂

今天上午讲了网络流 然而P都没有听懂 甚至不知道什么叫最大流

(什么都没有看到)

进入正题

先再强调一下网络流 及最大流的通俗概念吧

(这里引用几个大佬的说法)

算了...自己戳进去看吧 懒得复制了(我不确定大佬后面写的能不能看懂 反正我是看不懂)

https://blog.csdn.net/wzw1376124061/article/details/55001639

(看一下开头对网络流 最大流的通俗概念定义)

好吧那我就假设你知道这些概念了(如果真的没搞懂 可以再去看看一两个大佬的博客开头 一下就可以懂了)

上一张百度文库里的PPT的图


Q1:增广路径是啥玩意儿

就是一条从起点,到终点的一条每条边容量-实际流量>的路

大概有点感觉了吧?

Q2:如何增广?

(我来模拟一下这个过程你就懂了)


这是初始的样子 其中0/8代表8是这条边的容量 0是流量

根据之前说的 先找增广路径 再更新它

于是我们现在先找到一条 1-2-4

然后怎么办,把这条路上的每条边流量都加上每条边容量与流量差的最小值(这个就叫增广!)

此时有两个差 一个是8 一个是6 取其最小 即6 


于是变成了这个样子

(我们都知道 一条小溪的水流量取决于最窄的地方 这就是为什么这个地方填6 可以大概理解一下)

然后继续找增广路 找到了1-2-3-4

delta=min(2,1,5) 就等于1

于是加上1

继续找增广路 1-3-4 加上3


找到这儿 已经没有增广路径了 因为没有一条1到4的路径上满足每个流量都小于容量 (比如说1-2-4,2-4的流量等于了容量)

此时的最大流就是6+4=10

--------------------------------

算了我们直接讲算法实现吧。。。

这其实是个已证明的理论 :当且仅当残量网络中不存在增广路时 此时的流是从s到t的最大流

(证明可以去看算法导论上的)

然而这只是一个错误的算法....不过离我们的正解已经很近了

为什么错误呢?因为假如出现这种情况


假如第一次找到的增广路径是1-2-3-4 加上1过后 就不存在其他的增广路径了 这样做出来最大流就是1 然而很明显 答案并不是 显然应该是2

那我们的算法哪里有问题?仔细想想,我们应该给程序一个反悔的机制,也就是相当于第一次找到的不是最优解。用回溯法吗?不太好,这样的复杂度是成指数上升的

那么Ford-Fulkerson算法神奇的利用了一个叫做反向边的概念来解决这个问题。即每条边(i,j)都有一条反向边(j,i),反向边也同样有它的容量及流量,且初始的流量值就是为正向边的容量。

在第一次找到增广路之后,在把路上每一段的流量增加delta的同时,把反向边的流量减少delta

来看看刚刚的反例(博主太懒了 只画了部分反向边)


找到1-2-4-3这个路径后 为正向边都加上delta=1

就变成了下图


那继续找增广路径 发现1-4-2-3是一条 于是加上delta=1 此时最大流就算出来了 为2

这就是反向边的操作 至于正确性的证明 去看算导吧.....

-----------------------------------------------

这就是求最大流算法的思想 那让我们区分一下Ford-Fulkerson和Edmonds-Karp呢...

Ford-Fulkerson的伪代码在这儿

FORD_FULKERSON(G,s,t)
        for each edge(u,v)∈E[G]
             do f[u,v] <— 0
                 f[v,u] <— rongliang[edge(u,v)]//按照我刚刚讲的就应该这样写 
                 //以上是初始流量 
        while there exists a path p from s to t in the residual network G
             do delta(p) <— min{ rongliang[a,b]-f[a,b]}(a,b are two points in p)
             for each edge(a,b) in p
                  do f[u,v] <— f[u,v]+delta(p)  //对于在增广路径上的正向的边,加上增加的流
                       f[v,u] <— -delta[p]     

Edmonds-Karp又是啥 其实FF是一种方法 我们可以看到FF的复杂度主要取决于第五行 即找增广路径的方法 而EK是具体实现 主要是通过bfs来找的

//我的EK写法。。。建议邻接矩阵 
//题目:洛谷p2740 草地排水 
#include<bits/stdc++.h>
using namespace std;
int m,n,cap[205][205],pre[205],flow[205][205];
bool vis[205];
bool bfs(int s,int t)
{
    memset(pre,-1,sizeof(pre));
    memset(vis,false,sizeof(vis));
    queue<int> q;
    
    q.push(s);
    vis[s]=true;
    while(!q.empty())
    {
        int now=q.front();
        q.pop();
        for(int i=1;i<=n;i++)
        {
            if(vis[i]==false&&i!=now&&cap[now][i]-flow[now][i]>0)
            {
                q.push(i);
                pre[i]=now;
                vis[i]=true;
                if(i==t)	return true;	//找到了 
            }
        }
    }
    return false;
}
int max_flow(int s,int t)
{
    int ans=0;

    while(bfs(s,t))
    {
        int delta=0x7fffffff;
        for(int i=t;i!=s;i=pre[i])
        {
            delta=min(delta,cap[pre[i]][i]-flow[pre[i]][i]);
        }
        for(int i=t;i!=s;i=pre[i])
        {
            flow[pre[i]][i]+=delta;
            flow[i][pre[i]]-=delta;
        }
        ans+=delta;	//这是一个结论 记住就行 
    }
    return ans;
}
int main()
{
    cin>>m>>n;
    for(int i=1;i<=m;i++)
    {
        int x,y,rl;
        cin>>x>>y>>rl;
        //必须是+= 超级阴人.....没有看到一处到另一处不止一条排水沟 
        cap[x][y]+=rl;
        cap[y][x]+=rl;
        flow[x][y]=0;
        flow[y][x]+=rl;
    }
    cout<<max_flow(1,n);
    return 0;
}

(我的写法也许和他们有差别 不过真的很好懂)

EK算法时间复杂度:O(nm^2)

-----------------------------------------------------

前两种最大流算法都讲完了 现在进入dinic算法

我们发现之前的算法可能会碰到这种尴尬的情况


一眼可以看出最大流是999+999 但是如果我们的程序一开始就采取了1-2-3-4这条路径...

那么就变成了下图(省去了几个反向边)


而下次增广时 选择1-3-2-4


(注意变化的数字)

这样下去就会陷在这个泥潭之中 如果999这个数字变得更大 比如1e7 那TLE简直是稳稳的

于是我们引入dinic算法

其实我认为 搞懂了EK 直接看dinic的代码是没问题的 特别是博主这种简洁的代码

dinic算法就是为整个图增加了step数组 代表每个点在bfs子树里的层数

这样的话每次从点q dfs到下一点p必须满足step[p]=step[q]+1 这样就不会出现刚刚那尴尬的情况

会有多次bfs 因为图一直在增广 在变化 来更新step数组 

这样下去总会求出最大流 证明见算法导论

值得一提的是邻接表的小细节 我写在了“记录容易出错的地方”的那篇文章里 可以去看看

代码里有注释的 可以自己画图 结合代码注释理解

#include<bits/stdc++.h>
using namespace std;
int s,t,n,m,tot,first[10005],step[10005];
const int INF=0x7fffffff;
struct Edge
{
    int to;
    int next;
    int flow;
    int cap;
    //很多人没有加上flow这个元素 用一个cap就代替了delta 然而我不喜欢 我的写法好理解一点 
}edge[400005];
void addedge(int x,int y,int flow,int cap)
{
    edge[tot].to=y;
    edge[tot].flow=flow;
    edge[tot].cap=cap;
    edge[tot].next=first[x];
    first[x]=tot++;
    //注意区分tot在前和后的区别....详见我的那篇记录易错点的博客 
}
bool bfs(int s,int t)
{
    memset(step,-1,sizeof(step));
    step[s]=1;
    queue<int> q;
    q.push(s);
    
    while(!q.empty())
    {
        int now=q.front();
        q.pop();
        for(int u=first[now];u!=-1;u=edge[u].next)
        {
            int vis=edge[u].to;
            if(edge[u].cap-edge[u].flow>0&&step[vis]==-1)//这里有一个代替vis数组的感觉
            {
                step[vis]=step[now]+1;
                q.push(vis);
            } 
        }
    } 
    return step[t]!=-1;
} 
int dfs(int s,int t,int flow)	//flow是原点到当前点的剩余流量 
{
    if(s==t)	return flow;	 
    int addflow=0;
    for(int i=first[s];i!=-1&&addflow<=flow;i=edge[i].next)
    {
        int vis=edge[i].to;
        if(step[vis]==step[s]+1&&edge[i].cap-edge[i].flow>0)
        {
        //这这条路径上流量用了,下条路径的可以流过去的就少了(一个点的几个边) 相当于是防止这个流量用完了
            int delta=dfs(vis,t,min(flow-addflow,edge[i].cap-edge[i].flow));
            addflow+=delta; 
            edge[i].flow+=delta;
            edge[i^1].flow-=delta;	//2^1=3,3^1=2 求反向边编号 
        } 
    }
    return addflow;
}
int max_flow(int s,int t)
{
    int ans=0;
    while(bfs(s,t))
    {
        ans+=dfs(s,t,INF);
    }
    return ans;
}
int main()
{
    cin>>n>>m>>s>>t;
    memset(first,-1,sizeof(first));
	for(int i=1;i<=m;i++)	edge[i].next=-1;
    for(int i=1;i<=m;i++)
    {
        int x,y,rl;
        cin>>x>>y>>rl;
        addedge(x,y,0,rl);
        addedge(y,x,rl,rl);
    }
    cout<<max_flow(s,t);
    return 0;
}

终于讲完所有的了 希望你能有所收获 花了整整一天

(oier不容易啊 作业动都没动 明天等死吧)

  • 18
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值