最大流算法之Dinic

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


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


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

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;
}

如此初始化完成。ui


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

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


关于DFS寻找增广路:regexp

注意深搜在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

相关文章
相关标签/搜索