最大流Dinic算法实现——以《算法竞赛入门经典——训练指南》为例

    今天啃掉了网络流经典问题最大流的Dinic算法,一直想搞网络流但之前看书的状态都不对,心浮气躁,没仔细看就觉得难,结果束之高阁了。人做不成事到底还是缺决心啊!回顾学习的过程,我看懂Dinic的算法过程没太费事,大部分时间都耗在代码的理解上了,因为之前没写过网络流的题,所以我连建图都不清楚,还寻思用邻接表存残量网络怎么快速定位一条边的反向边。这些坎到底是迈过来了。下面进入正题。


概述

    Dinic算法是在最大流算法中属于增广路系列的算法,并且是“最短增广路算法”。这里不加证明地给出它的复杂度:O(n^2*m)。从集训队前辈那里得知网络流算法的复杂度其实是很难准确计算的,在实测中的运行结果常常比理论估计要好得多,要知道网络流的题数据是不太好构造的,事实上图论的题数据都不好构造,大部分数据是随机数据,因此不必对最大流算法的复杂度上限感到悲观。值得一提的是,一些恰当的优化会大大提高算法的效率。网络流的算法如果不小心写挫了,效率会倒退好多好多。

算法提要

    Dinic算法的核心是通过引入分层标号来构造层次图,具体方式是BFS。所谓分层标号就是BFS从源点出发访问到各个顶点的时间,广度优先的遍历方式具有鲜明的层次性。Dinic算法增广的策略是用DFS进行多路增广,一次找多条增广路,更新多次当前最大流的值。每次多路增广前都要重新计算分层标号,因为增广后残量网络会变。

代码解析

题目:POJ1459 多源点多汇点模型

Dinic算法模板来自《算法竞赛入门经典——训练指南》,ACMer们更喜欢称之为“大白书”。

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
#include <queue>
#include <cmath>
using namespace std;

const int MAXN = 105;
const int INF = 1<<30;

struct Edge
{
    int to, cap;
};
int n,m,s,t, np,nc;
vector<Edge> edge;
vector<int>  G[MAXN];
int d[MAXN], cur[MAXN];

void AddEdge(int from, int to, int cap)
{
    edge.push_back((Edge){to, cap});//这里调用了结构体缺省的构造函数,代码很简洁,下面也是
    edge.push_back((Edge){from, 0});
    int id = edge.size();
    G[from].push_back(id-2);
    G[to].push_back(id-1);
}

//bfs函数我做了调整,用距离标号数组d代替了书中的vis数组。另外d[s]改成了1,这个不影响。
bool bfs()
{
    memset(d, 0, sizeof(d));
    queue<int> Q;
    Q.push(s);
    d[s] = 1;
    while(!Q.empty())
    {
        int x = Q.front(); Q.pop();
        if(x == t) return true;
        for(int i=0; i<G[x].size(); i++)
        {
            Edge& e = edge[G[x][i]];
            if(!d[e.to] && e.cap>0)//邻接点未访问且该边残量不为零
            {
                d[e.to] = d[x]+1;
                Q.push(e.to);
            }
        }
    }
    return false;
}

int dfs(int x, int a)//x是当前遍历的点,a是目前为止所有弧的最小残量
{
    if(x == t || a == 0) return a;
    int flow = 0, f;
    for(int& i=cur[x]; i<G[x].size(); i++)//使用引用变量在遍历x所有邻接点的同时顺便修改了cur[x]的值,实现得很妙
    {
        Edge& e = edge[G[x][i]];
        if(d[x]+1 == d[e.to] && (f=dfs(e.to, min(a, e.cap)))>0)
        {
            e.cap -= f;
            edge[G[x][i]^1].cap += f;//^1 是定位残量网络中反向边的方法
            flow += f;
            a -= f;
            if(a == 0) break;
        }
    }
    return flow;
}

int Maxflow()//dinic
{
    int flow = 0;
    while(bfs())
    {
        memset(cur, 0, sizeof(cur));
        flow += dfs(s, INF);
    }
    return flow;
}

int main()
{
    char str[20];
    int u,v,w;
    while(scanf("%d%d%d%d", &n,&np,&nc,&m)!=EOF)
    {
        s = n+1;
        t = n+2;
        for(int i=0;i<n+2;i++) G[i].clear();
        edge.clear();
        for(int i=0;i<m;i++)//读入边信息
        {
            scanf("%s", str);
            sscanf(str, "%*c%d%*c%d%*c%d", &u,&v,&w);
            AddEdge(u,v,w);
        }
        for(int i=0;i<np;i++)//读入源点信息
        {
            scanf("%s", str);
            sscanf(str, "%*c%d%*c%d", &u,&w);
            AddEdge(s,u,w);
        }
        for(int i=0;i<nc;i++)//读入汇点信息
        {
            scanf("%s", str);
            sscanf(str, "%*c%d%*c%d", &u,&w);
            AddEdge(u,t,w);
        }
        printf("%d\n", Maxflow());
    }
    return 0;
}

注意:这份代码在POJ需要用G++编译项提交,用C++ CE了,错误信息出在edge.push_back((Edge){to, cap}),不明白,果断换G++过之。

图存储结构

这份代码采用STL vector容器实现邻接表来存图,并直接用cap表示残量。值得注意的是,网络流的题经常会有重边(这道题就有)。

dfs函数的理解

多路增广是Dinic算法求解最大流的关键一步,这部分的理解对于整个算法的理解至关重要。
函数参数中的a需要好好理解,它是走到x点前经过的所有边的残量的最小值,有些书称残量最小的这条边为“瓶颈边”。
if(x == t || a == 0) return a;
x == t 说明我们已经搜索到了汇点,同时意味着找到了一条增广路,并且这条路能够增加的流量是a。
a == 0 大白书上提到:“如果不在a==0时及时终止,整个程序的效率往往会大打折扣。”为什么呢?出现这种情形说明之前传进的参数min(a, e.cap)为0,也即e.cap=0,我们刚刚走的这条边已经没容量了!因此没有必要再搜下去,直接返回。
if(d[x]+1 == d[e.to] && (f=dfs(e.to, min(a, e.cap)))>0)
Dinic的多路增广经过了分层标号的预处理,在dfs时只走层与层间的边。当更深一层的dfs返回了0的时候,说明刚刚找了条没容量的边。

最后讨论一下引入cur数组的意义。

先说说它的意义:cur[x]表示每个结点x正在考虑的弧。我们需注意,某个结点在dfs中可能会被多次访问到,因为上一层结点可能存在多条边通向x。这时候cur数组的作用就来了,它标记了以x为起点的边有哪些已经没有增广的价值了,因此我们跳过它们,直接从第一条有用的边(这也是cur[x]存储的值的意义)向下迭代加深。优化的地方就是少做了无用功。
如果用链式前向星存图,cur[x]的初值不再是0而是head[x],更新操作发生在“遍历下一条边”时,只要令cur[x] = edge[i].next即可。这里head表示头链表,edge表示边表。



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
算法竞赛入门经典--训练指南,代码仓库,有四个版本的代码仓库。 《算法竞赛入门经典——训练指南》代码仓库 例题代码 限于篇幅,书上并没有给出所有例题的代码,这里给出了所有例题的代码,并且改进了书上的一些代码。 第一章 32题 38份代码 第二章 28题 30份代码 第三章 22题 23份代码 第四章 19题 21份代码 第五章 34题 39份代码 第六章 24题 26份代码 共159题 177份代码 为了最大限度保证代码风格的一致性,所有例题代码均由刘汝佳用C++语言编写。 所有代码均通过了UVa/La的测试,但不能保证程序是正确的(比如数据可能不够强),有疑问请致信rujia.liu@gmail.com,或在googlecode中提出: http://code.google.com/p/aoapc-book/ [最新更新] 2013-04-23 增加字符串中例题10(UVa11992 Fast Matrix Operations)的另一个版本的程序,执行效率较低,但更具一般性,可读性也更好 2013-04-22 增加字符串部分“简易搜索引擎”代码,可提交到UVa10679 2013-04-13 修正Treap中优先级比较的bug(原来的代码实际上是在比较指针的大小!),加入纯名次树代码 2013-03-31 修正UVa1549标程的bug,即buf数组不够大。 增加线段树部分“动态范围最小值”的完整代码 2013-03-23 修正UVa10054标程的bug,即没有判断是否每个点的度数均为偶数。UVa数据已经更新 LA3401修正了代码和文字不一致的问题 UVa11270增加了答案缓存 2013-03-21 增加线段树部分中两个经典问题的完整代码:快速序列操作I和快速序列操作II 2013-02-28 补全所有159道例题的代码

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值