引言
在最大流(一)中我们讨论了关于EK算法的原理与代码实现,此文将讨论与EK算法同级别复杂度(O(N^2M))的算法——Dinic算法。
Dinic算法用到的思想是图的分层结构,通过BFS将每一个节点标出层次后DFS得到当前增广路。然后继续在残留网络中进行BFS分层,当汇点不在层次网络时(没有连通弧了),算法结束。


Dinic算法结构
0.初始化(边表)
1.BFS分层——汇点不在层次网络中跳出
2.DFS寻找增广路
3.输出最大流


关于初始化
在最大流(一)中,在MLE的情况下我们舍弃了邻接矩阵的存储方式转而使用边表存储每一条弧的信息。存储方式与(一)中相同。
即定义结构体:

struct qi
{
int st, en, num;//首、尾、流量限制
}flow[maxm];//边表

采用读入优化读取数据。

int read()//读入优化
{
char a;
int input = 0;
a = getchar();
while(a < '0' || a > '9')
a = getchar();
while(a >= '0' && a <= '9')
{
input = input*10+a-'0';
a = getchar();
}
return input;
}

将每一条弧与序号一一对应

for(i = 0; i != m; ++i)
{
low[i].st = read(), flow[i].en = read(), flow[i].num = read();
re[flow[i].st][++num[flow[i].st]] = i;//编号与弧的一一映射
re[flow[i].en][++num[flow[i].en]] = m+i;//定义反向弧的编号与该弧的关系
}

初始化反向弧信息:

for(i = m; i != m+m; ++i)//反向弧流量限制初始为0
{
flow[i].st = flow[i-m].en;
flow[i].en = flow[i-m].st;
flow[i].num = 0;
}

如此初始化完成。


BFS分层:
概念:所谓分层就是将图的每一个节点按照某一个标准分类。在Dinic算法中,标准是每一个节点到源点的最短路径(经过几条弧),由此得到每一个节点的层级(源点层次为0,以此类推)。
目的:限制每一次寻找增广路的时候使其在寻找增广路时不会出现浪费。若i -> j,需满足lev[j] = lev[i]+1。
实现方法广度优先搜索。记录每一个与之相邻的节点的等级为cur+1,每个节点每次广搜中只遍历一次

由此可得到两种结果
1.汇点的层次是N(N > 0);
2.汇点没有层次(N == 0);
结果为2时我们结束算法,因为在残余网络中已不存在增广路了。
如果结果是1我们继续进行操作——DFS寻找增广路(如下)。


关于DFS寻找增广路:

注意深搜在if中。

int Dfs(int curr, int min_flow)//寻找增广路
{
int i, j, a = 0;
if(curr == en) return min_flow;//遍历到汇点返回
for(i = start[curr]; i != num[curr]+1; ++i)//每一个与之相连的节点
{
++start[curr];//当前弧优化
j = flow[re[curr][i]].en;//当前节点的下一个节点
if(lev[j] == lev[curr]+1 && (a = Dfs(j, min(min_flow, flow[re[curr][i]].num))))//here
{
flow[re[curr][i]].num -= a;
flow[re[curr][i]+m].num += a;
if(min_flow == 0) break;//优化,当找不到增广路时直接跳出
return a;
}
}
return 0;//遍历不到退出
}

解释一下if的条件。

    if(lev[j] == lev[curr]+1 && (a = Dfs(j, min(min_flow, flow[re[curr][i]].num))))

首先根据算法应该满足级数比当前级数大1;
其次进行深搜,实际上循环中的代码只是用来更新流量的,通过回溯更新当前增广路的流量限制。真正寻找增广路的部分在于后面括号中的代码

也就是

(a = Dfs(j, min(min_flow, flow[re[curr][i]].num))

当a != 0 时,会返回true(!0),得到0时会返回false,当返回false的时候说明当前路径不是增广路,故直接跳出。(函数最后的return 0)


关于当前弧优化:
因为每一个节点可能有多个节点与之相连,故DFS遍历的时候可能会再次访问到。
例子:
节点P**已经被遍历,而且已经遍历到与之相连的第二个节点。被再次遍历到,说明要继续遍历与之相邻的节点,因为前两个节点(在本个例子中)已经在之前已经遍历过,所以应该直接从第三个开始遍历。故用一个start[i]记录已经遍历到的相邻节点个数(也是第几个),使得在将来访问时不重复深搜已遍历节点。


Dinic:

int Dinic(int st_pos, int end_pos)
{
int i, minn, max_flow = 0;
while(Bfs(st, en))
{
memset(start, 0, sizeof start);//每次深搜将上次已遍历节点数清零
while(minn = Dfs(st, INF)) max_flow += minn;
}
return max_flow;
}

这个就是之前整个算法的结构的代码形式


完整代码:

/*
Algorithm: Dinic
Author: kongse_qi
date: 2017/04/09
*/ #include <bits/stdc++.h>
#define INF 0x3f3f3f
#define maxm 200005
#define maxn 10005
using namespace std; int n, m, st, en, num[maxn], re[maxn][maxn/10], lev[maxn], minn, start[maxn];
struct qi{int st, en, num;}flow[maxm];//边表
bool wh[maxn]; int read()//读入优化
{
char a;
int input = 0;
a = getchar();
while(a < '0' || a > '9')
a = getchar();
while(a >= '0' && a <= '9')
{
input = input*10+a-'0';
a = getchar();
}
return input;
} void Init()//初始化
{
int i;
memset(num, -1, sizeof num);
n = read(), m = read(), st = read(), en = read();
for(i = 0; i != m; ++i)
{
flow[i].st = read(), flow[i].en = read(), flow[i].num = read();
re[flow[i].st][++num[flow[i].st]] = i;//编号与弧的一一映射
re[flow[i].en][++num[flow[i].en]] = m+i;//定义反向弧的编号与该弧的关系
}
for(i = m; i != m+m; ++i)//反向弧流量限制初始为0
{
flow[i].st = flow[i-m].en;
flow[i].en = flow[i-m].st;
flow[i].num = 0;
}
return ;
} bool Bfs(int st, int en)//BFS将图分层
{
int i, j, ne, st_pos = -1, end_pos = 0, curr_pos, q[maxn], tot = 1;
bool wh_con = 0;
lev[st] = 0;//初始源点层数为0
memset(wh, 0, sizeof wh);
memset(lev, 0, sizeof lev);
wh[st] = 1;
q[0] = st;
while(st_pos != end_pos)
{
curr_pos = q[++st_pos];
for(i = 0; i != num[curr_pos]+1; ++i)
{
j = re[curr_pos][i];//当前弧
ne = flow[j].en;//当前弧的终点
if(!wh[flow[j].en] && flow[j].num > 0)//流量限制>0 && 此次未遍历
{
if(ne == en) wh_con = 1;//源点在残余网络之中
wh[ne] = 1;
q[++end_pos] = ne;
lev[ne] = lev[curr_pos]+1;
++tot;
}
if(tot == n) return 1;//优化1:整个网络完成遍历后直接退出
}
}
return wh_con;
} int Dfs(int curr, int min_flow)//寻找堵塞流
{
int i, j, a = 0;
if(curr == en || min_flow == 0) return min_flow;
for(i = start[curr]; i != num[curr]+1; ++i)
{
++start[curr];
j = flow[re[curr][i]].en;
if(lev[j] == lev[curr]+1 && (a = Dfs(j, min(min_flow, flow[re[curr][i]].num))))
{
flow[re[curr][i]].num -= a;
flow[re[curr][i]+m].num += a;
if(min_flow == 0) break;
return a;
}
}
return 0;
} int Dinic(int st_pos, int end_pos)
{
int i, minn, max_flow = 0;
while(Bfs(st, en))
{
memset(start, 0, sizeof start);
while(minn = Dfs(st, INF)) max_flow += minn;
}
return max_flow;
} int main()
{
//freopen("test.in", "r", stdin);
//freopen("test.out", "w", stdout); Init();
printf("%d", Dinic(st, en)); //fclose(stdin);
//fclose(stdout);
}

至此便完成了Dinic算法。


实测效率
还是在luogu的P3376 【模板】网络最大流中测评。

结果:耗时/内存 429ms , 56949kb
相比于EK算法的:耗时/内存 392ms , 56988kb 似乎要慢上一些,实际上关于网络流算法的时间复杂度是玄学,只能得到上限(最坏情况)无法得到每次实际的复杂度。因为是BFS+DFS,次数,节点连通情况我们都无法计算,故时间复杂度意义不大,实测结果与具体数据有关。
通常不会超时,因为面对极坑的数据(例如真的到了N^2M的情况),奈何出题人再优化他的标程也不可能按时跑出来(100000* 100000* 10000,你行你来…),会改数据的…
所以在使用这种算法的时候还是别考虑在这上面优化了千万不要忘记读入优化,比你优化别的半天强多了)。

至此Dinic算法的分析便结束了。
箜瑟_qi 2017.04.09 20:42

最大流算法之Dinic的更多相关文章

  1. 最大流算法(Edmons-Karp + Dinic 比较) + Ford-Fulkson 简要证明

    Ford-Fulkson用EK实现:483ms #include <cstdio> #include <cstring> #define min(x,y) (x>y?y: ...

  2. 算法模板——Dinic网络最大流 2

    实现功能:同Dinic网络最大流 1 这个新的想法源于Dinic费用流算法... 在费用流算法里面,每次处理一条最短路,是通过spfa的过程中就记录下来,然后顺藤摸瓜处理一路 于是在这个里面我的最大流 ...

  3. 最大流EK和Dinic算法

    最大流EK和Dinic算法 EK算法 最朴素的求最大流的算法. 做法:不停的寻找增广路,直到找不到为止 代码如下: @Frosero #include <cstdio> #include ...

  4. P3376 【模板】网络最大流( Edmonds-krap、Dinic、ISAP 算法)

    P3376 [模板]网络最大流( Edmonds-krap.Dinic.ISAP 算法) 题目描述 如题,给出一个网络图,以及其源点和汇点,求出其网络最大流. 输入格式 第一行包含四个正整数N.M.S ...

  5. ISAP算法对 Dinic算法的改进

    ISAP算法对 Dinic算法的改进: 在刘汝佳图论的开头引言里面,就指出了,算法的本身细节优化,是比较复杂的,这些高质量的图论算法是无数优秀算法设计师的智慧结晶. 如果一时半会理解不清楚,也是正常的 ...

  6. 最大流算法-ISAP

    引入 最大流算法分为两类,一种是增广路算法,一种是预留推进算法.增广路算法包括时间复杂度\(O(nm^2)\)的EK算法,上界为\(O(n^2m)\)的Dinic算法,以及一些其他的算法.EK算法直接 ...

  7. Ford-Fulkerson 最大流算法

    流网络(Flow Networks)指的是一个有向图 G = (V, E),其中每条边 (u, v) ∈ E 均有一非负容量 c(u, v) ≥ 0.如果 (u, v) ∉ E 则可以规定 c(u, ...

  8. 算法9-5:最大流算法的Java代码

    残留网络 在介绍最大流算法之前先介绍一下什么是残留网络.残余网络的概念有点类似于集合中的补集概念. 下图是残余网络的样例. 上面的网络是原始网络.以下的网络是计算出的残留网络.残留网络的作用就是用来描 ...

  9. 海量数据挖掘MMDS week3:流算法Stream Algorithms

    http://blog.csdn.net/pipisorry/article/details/49183379 海量数据挖掘Mining Massive Datasets(MMDs) -Jure Le ...

随机推荐

  1. iOS回顾笔记(07) -- UITableView的使用和性能优化

    iOS回顾笔记(07) -- UITableView的使用和性能优化 如果问iOS中最重要的最常用的UI控件是什么,我觉得UITableView当之无愧!似乎所有常规APP都使用到了UITableVi ...

  2. Visual Studio 2017 新特性

    全新的安装体检 VS2017更好的支持了按需安装的特点,用户可以仅选择需要的功能安装,节省了不少的Disk 最小的安装仅有几百兆,但也支持20多种编程语言的编辑和源码管理 支持创建自定义的离线安装包 ...

  3. windows 下编译php扩展库pecl里的扩展memcache

    Memcache是一个高性能的分布式的内存对象缓存系统,通过在内存里维护一个统一的巨大的hash表,它能够用来存储各种格式的数据,包括图像.视频.文件以及数据库检索的结果等.简单的说就是将数据调用到内 ...

  4. c#实现list,dataset,DataTable转换成josn等各种转换方法总和

    using System;using System.Collections.Generic;using System.Text;using System.Data;using System.Refle ...

  5. iOS开发之JSON & XML

    1.概述 JSON (1) 作为一种轻量级的数据交换格式,正在逐步取代XML,成为网络数据的通用格式 (2) 基于JavaScript的一个子集 (3) 易读性略差,编码手写难度大,数据量小 (4) ...

  6. IE6 margin 双倍边距解决方案

    一.什么是双边距Bug? 先来看图: 我们要让绿色盒模型在蓝色盒模型之内向左浮动,并且距蓝色盒模型左侧100像素.这个例子很常见,比如在网页布局中,侧边栏靠左侧内容栏浮动,并且要留出内容栏的宽度.要实 ...

  7. 一次安装rpcbind失败引发的思考

    问题: yum install rpcbind -y 出现如下错误: Error -.el6.x86_64 error: %pre(rpcbind--.el6.x86_64) scriptlet fa ...

  8. 自动部署Nginx和nfs并架设Nginx集群脚本

    本人经过多次尝试,简单完成了自动部署Nginx和nfs脚本,并且能够自动部署web反向代理集群,下面详细的阐述一下本人的思路.(以下脚本本人处于初学阶段,写的并不是很完善,所以需要后期进行整理和修正, ...

  9. C++—动态内存管理之深入探究new和delete

    C++中程序存储空间除栈空间和静态区外,每个程序还拥有一个内存池,这部分内存被称为自由空间(free store)或堆(heap).程序用堆来存储动态分配的对象,即,那些程序运行时分配的对象.动态对象 ...

  10. Servlet小总结(转)

    一,什么是Servlet? Servlet是一个Java编写的程序,此程序是基于Http协议的,在服务器端运行的(如tomcat), 是按照Servlet规范编写的一个Java类. 二,Servlet ...