《数据结构与算法分析:C语言描述》复习——第九章“图论”——最大流问题(每次寻找增广路径.......)与Dinic算法

参考网站:https://www.cnblogs.com/zhuli19901106/p/3825152.html

https://www.cnblogs.com/SYCstudio/p/7260613.html

 

每次找到一条增广路径, 就给这条路径增流 , 增加的流量就可以计入到最大流内了, 因为不可能从汇点T有回流,所以只要增流的都可以计入最大流. (流网络必须满足有源点和汇点,否则算法应该跑的结果是没有意义的).——这就是增广路径的想法。

 

因此求出最大流量后,在这样的流量模型里,s作为入口,t作为出口,其他顶点的流入和流出一定会保持平衡。这仅仅是最简单的一种模型,如果允许多个入口或者多个出口的话,那么问题的复杂程度就不是几句话能说清楚的了。

 

Ford-Fulkerson方法是基于这种增广路径思想的解法,用于处理这类问题。之所以叫“方法”而不是“算法”,是因为如何寻找增广路径并没有具体说明。

  那到底怎么找呢?可以用BFS、DFS、排序等等手段来组合使用。简单的来说,就是靠搜。根据寻找增广路径的方法不同,就有了几种不同的对应算法。

 

例:若使用BFS来寻找增广路径,则属于Edmonds-Karp算法。

 

为什么添加了反向边?

可以这样想:一条边现在从u到v的流量是x。那么加上一条增广路之后可能使得这条边的流量变小,小于x,但总的从原点到汇点流量增加了。但是这条边流量最小变为0,因为这条边流向只能是u到v。所以增加的这条增广路中,在这条边上的流量最大可以是从v到u,流量为x,也就是现在从u到v的流量。一条增广路可以把这条边的流量抵消为0,但不能抵消为负数,因为不能改变网络的流向。所以要加上反向边,反向边的流量最大为x,也就是这条边最大被抵消的能力。

 

为什么要连反向边?

我们知道,当我们在寻找增广路的时候,在前面找出的不一定是最优解,如果我们在减去残量网络中正向边的同时将相对应的反向边加上对应的值,我们就相当于可以反悔从这条边流过。
比如说我们现在选择从 u 流向 v 一些流量,但是我们后面发现,如果有另外的流量从 p 流向 v,而原来 u 流过来的流量可以从 u->q 流走,这样就可以增加总流量,其效果就相当于 p->v->u->q,用图表示就是:
此处输入图片的描述
图中的蓝色边就是我们首次增广时选择的流量方案,而实际上如果是橘色边的话情况会更优,那么我们可以在 v->u 之间连一条边容量为 u->v 减去的容量,那我们在增广 p->v->u->q 的时候就相当于走了 v->u 这条 "边",而 u->v 的流量就与 v->u 的流量相抵消,就成了中间那幅图的样子了。
如果是 v->u 时的流量不能完全抵消 u->v 的,那就说明 u 还可以流一部分流量到 v,再从 v 流出,这样也是允许的。

 

一个小技巧

虽然说我们已经想明白了为什么要加反向边,但反向边如何具体实现呢?笔者在学习网络流的时候在这里困扰了好久,现在简要的总结在这里。
首先讲一下邻接矩阵的做法,对于 G[u][v],如果我们要对其反向边进行处理,直接修改 G[v][u] 即可。
但有时会出现 u->v 和 v->u 同时本来就有边的情况,一种方法是加入一个新点 p,使 u->v,而 v->u 变成 v->p,p->u。
另一种方法就是使用邻接表,我们把边从 0 开始编号,每加入一条原图中的边 u->v 时,加入边 v->u 流量设为 0,那么这时对于编号为 i 的边 u->v,我们就可以知道 i^1 就是其反向边 v->u。

 

朴素算法的低效之处

虽然说我们已经知道了增广路的实现,但是单纯地这样选择可能会陷入不好的境地,比如说这个经典的例子:
此处输入图片的描述
我们一眼可以看出最大流是 999(s->v->t)+999(s->u->t),但如果程序采取了不恰当的增广策略:s->v->u->t
此处输入图片的描述
我们发现中间会加一条 u->v 的边
此处输入图片的描述
而下一次增广时:
此处输入图片的描述
若选择了 s->u->v->t
此处输入图片的描述
然后就变成
此处输入图片的描述
这是个非常低效的过程,并且当图中的 999 变成更大的数时,这个劣势还会更加明显。
怎么办呢?
这时我们引入 Dinic 算法

Dinic 算法

为了解决我们上面遇到的低效方法,Dinic 算法引入了一个叫做分层图的概念。具体就是对于每一个点,我们根据从源点开始的 bfs 序列,为每一个点分配一个深度,然后我们进行若干遍 dfs 寻找增广路,每一次由 u 推出 v 必须保证 v 的深度必须是 u 的深度 + 1。下面给出代码
一些变量的定义

int s,t;//源点和汇点
int cnt;//边的数量,从0开始编号。
int Head[maxN];//每一个点最后一条边的编号
int Next[maxM];//指向对应点的前一条边
int V[maxM];//每一条边指向的点
int W[maxM];//每一条边的残量
int Depth[maxN];//分层图中标记深度

Dinic 主过程:

int Dinic()
{
    int Ans=0;//记录最大流量
    while (bfs())
    {
        while (int d=dfs(s,inf))
            Ans+=d;
    }
    return Ans;
}

bfs 分层图过程

bool bfs()
{
    queue<int> Q;//定义一个bfs寻找分层图时的队列
    while (!Q.empty())
        Q.pop();
    memset(Depth,0,sizeof(Depth));
    Depth[s]=1;//源点深度为1
    Q.push(s);
    do
    {
        int u=Q.front();
        Q.pop();
        for (int i=Head[u];i!=-1;i=Next[i])
            if ((W[i]>0)&&(Depth[V[i]]==0))//若该残量不为0,且V[i]还未分配深度,则给其分配深度并放入队列
            {
                Depth[V[i]]=Depth[u]+1;
                Q.push(V[i]);
            }
    }
    while (!Q.empty());
    if (Depth[t]==0)//当汇点的深度不存在时,说明不存在分层图,同时也说明不存在增广路
        return 0;
    return 1;
}

dfs 寻找增广路过程

int dfs(int u,int dist)//u是当前节点,dist是当前流量
{
    if (u==t)//当已经到达汇点,直接返回
        return dist;
    for (int i=Head[u];i!=-1;i=Next[i])
    {
        if ((Depth[V[i]]==Depth[u]+1)&&(W[i]!=0))//注意这里要满足分层图和残量不为0两个条件
        {
            int di=dfs(V[i],min(dist,W[i]));//向下增广
            if (di>0)//若增广成功
            {
                W[i]-=di;//正向边减
                W[i^1]+=di;反向边加
                return di;//向上传递
            }
        }
    }
    return 0;//否则说明没有增广路,返回0
}

把上面的内容都封装到类中:

class Graph
{
private:
    int s,t;
    int cnt;
    int Head[maxN];
    int Next[maxM];
    int V[maxM];
    int W[maxM];
    int Depth[maxN];
public:
    int n;
    void init(int nn,int ss,int tt)//初始化
        {
            n=nn;
            s=ss;
            t=tt;
            cnt=-1;
            memset(Head,-1,sizeof(Head));
            memset(Next,-1,sizeof(Next));
            return;
        }
    void _Add(int u,int v,int w)
        {
            cnt++;
            Next[cnt]=Head[u];
            V[cnt]=v;
            W[cnt]=w;
            Head[u]=cnt;
        }
    void Add_Edge(int u,int v,int w)//加边,同时加正向和反向的
        {
            _Add(u,v,w);
            _Add(v,u,0);
        }
    int dfs(int u,int dist)
        {
            //cout<<"Dfs:"<<u<<' '<<dist<<endl;
            if (u==t)
                return dist;
            for (int i=Head[u];i!=-1;i=Next[i])
            {
                if ((Depth[V[i]]==Depth[u]+1)&&(W[i]!=0))
                {
                    int di=dfs(V[i],min(dist,W[i]));
                    if (di>0)
                    {
                        W[i]-=di;
                        W[i^1]+=di;
                        return di;
                    }
                }
            }
            return 0;
        }
    int bfs()
        {
            //cout<<"Bfs.begin:"<<endl;
            queue<int> Q;
            while (!Q.empty())
                Q.pop();
            memset(Depth,0,sizeof(Depth));
            Depth[s]=1;
            Q.push(s);
            do
            {
                int u=Q.front();
                //cout<<u<<endl;
                Q.pop();
                for (int i=Head[u];i!=-1;i=Next[i])
                {
                    if ((W[i]>0)&&(Depth[V[i]]==0))
                    {
                        Depth[V[i]]=Depth[u]+1;
                        Q.push(V[i]);
                    }
                }
            }
            while (!Q.empty());
            //cout<<"Bfs.end"<<endl;
            if (Depth[t]>0)
                return 1;
            return 0;
        }
    int Dinic()
        {
            int Ans=0;
            while (bfs())
            {
                while (int d=dfs(s,inf))
                    Ans+=d;
            }
            return Ans;
        }
};

Dinic 算法的优化

Dinic 算法还有优化,这个优化被称为当前弧优化,即每一次 dfs 增广时不从第一条边开始,而是用一个数组 cur 记录点 u 之前循环到了哪一条边,以此来加速
总代码如下,修改的地方已在代码中标出:

class Graph
{
private:
    int cnt;
    int Head[maxN];
    int Next[maxM];
    int W[maxM];
    int V[maxM];
    int Depth[maxN];
    int cur[maxN];//cur就是记录当前点u循环到了哪一条边
public:
    int s,t;
    void init()
        {
            cnt=-1;
            memset(Head,-1,sizeof(Head));
            memset(Next,-1,sizeof(Next));
        }
    void _Add(int u,int v,int w)
        {
            cnt++;
            Next[cnt]=Head[u];
            Head[u]=cnt;
            V[cnt]=v;
            W[cnt]=w;
        }
    void Add_Edge(int u,int v,int w)
        {
            _Add(u,v,w);
            _Add(v,u,0);
        }
    int dfs(int u,int flow)
        {
            if (u==t)
                return flow;
            for (int& i=cur[u];i!=-1;i=Next[i])//注意这里的&符号,这样i增加的同时也能改变cur[u]的值,达到记录当前弧的目的
            {
                if ((Depth[V[i]]==Depth[u]+1)&&(W[i]!=0))
                {
                    int di=dfs(V[i],min(flow,W[i]));
                    if (di>0)
                    {
                        W[i]-=di;
                        W[i^1]+=di;
                        return di;
                    }
                }
            }
            return 0;
        }
    int bfs()
        {
            queue<int> Q;
            while (!Q.empty())
                Q.pop();
            memset(Depth,0,sizeof(Depth));
            Depth[s]=1;
            Q.push(s);
            do
            {
                int u=Q.front();
                Q.pop();
                for (int i=Head[u];i!=-1;i=Next[i])
                    if ((Depth[V[i]]==0)&&(W[i]>0))
                    {
                        Depth[V[i]]=Depth[u]+1;
                        Q.push(V[i]);
                    }
            }
            while (!Q.empty());
            if (Depth[t]>0)
                return 1;
            return 0;
        }
    int Dinic()
        {
            int Ans=0;
            while (bfs())
            {
                for (int i=1;i<=n;i++)//每一次建立完分层图后都要把cur置为每一个点的第一条边 感谢@青衫白叙指出这里之前的一个疏漏
                    cur[i]=Head[i];
                while (int d=dfs(s,inf))
                {
                    Ans+=d;
                }
            }
            return Ans;
        }
};

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章