【最大流问题】增广路径——看这一篇就够了!

问题描述:求一个有向图从起点(s)到终点(t)流量的最大值:
如图所示:
在这里插入图片描述
解题算法:Ford-Fulkerson算法:
1.从零流开始
2.当剩余网络中有增广路径时(BFS),
3.找一条增广路径(DFS),增大流,
4.输出流量

算法步骤:
1.输入数据:
这个题目数据输入是一个很关键的点,我们用结构体来存储数据,但这里应该怎么存储图是一个值得思考的问题。经过尝试,我们发现,用结构体来存储边且用next来表示该始点的下一条边是一个很好的做法:

int h[MAX];//用于存储每一个顶点最后连接的是哪一个顶点;
int num = 0;//用于记录边的数量,记住,每一对平行边的序号一定相邻;
int s,t;
int p[105][105];//用于记录两点的相邻与否;

struct node{
    int from;//该边的始点
    int to;//该边的终点
    int weight;//该边的权值
    int next;//该边始点下一个连接的边
}Edge[MAX];
void AddEdge(int f, int t, int w)
{
    Edge[++num].from = f;
    Edge[num].to = t;
    Edge[num].weight = w;
    Edge[num].next = h[f];//第一个存储的next值就取0,以便于后面判定是否已经把这个点相邻的点找完(行45,行66)
    h[f] = num;//用于下一次再有以f为始点的边记录
}
int main()
{
    int N,i;
    scanf("%d", &N);//点的总数;
    s = 1;
    t = N;
    int a, b, c;
    memset(h, 0, sizeof(h));
    memset(p, 0, sizeof(p));
    while(scanf("%d%d%d", &a, &b, &c) != EOF)
    {
        if(p[a][b] == 0)
        {
            AddEdge(a,b,c);
            p[a][b] = num;
            AddEdge(b,a,0);
            p[b][a] = num;
        }
        else 
        {
            Edge[p[a][b]].weight += c;
        }
    }
    // for(i = 1; i <= num; i++)
    //     printf("from = %d to = %d weight = %d next = %d\n", Edge[i].from, Edge[i].to, Edge[i].weight, Edge[i].next);
    int ans = 0;
    while(BFS())
    {
        ans += DFS(s, INF);
        printf("%d\n", ans);
    }
    printf("%d\n", ans);
    system("pause");
    return 0;
}

2.在剩余网络中找增广路径:
这部过程尤其容易弄混淆,因为我们只需要找出一条增广路径即可,如果有的话就直接返回1,如果没有就返回0。但是!!!这里有一个重要的东西需要我们去解决,那就是找到每一个点接下来应该接哪一个点,虽然在原图中这个点可能可以接很多很多其他的点,但是,在我们要找的增广路径中,这个点的后面就只能跟某个点(或某些点),这里我们引入一个数组d来记录这个点沿着可能的增广路径到s相隔几个点(因为一些bug,我们定义s到s为1,s后面一个点到s为2…)

int d[MAX];//用于记录到起点的距离,也可以当做在遍历时判定上一个点是否已经走过了的条件
bool BFS()
{
    memset(d,0,sizeof(d));//每一次重新找剩余网络都需要更新d数组;
    d[s] = 1;//其实在一般情况下,这里的起始点的d值应该为0才说得过去,但由于后面判定这个点是否已经走过是由d[i]是否为0来判定的,所以我们起始点用1
    std::queue<int> Q;
    Q.push(s);
    while(!Q.empty())
    {
        int q = Q.front();
        Q.pop();
        for(int i = h[q]; i != 0; i = Edge[i].next)
        {
            int to = Edge[i].to;
            if(d[to] == 0 && Edge[i].weight != 0)
            {
                d[to] = d[q] + 1;
                if(to == t)
                {
                    for(int j = s; j <= t; j ++)
                        printf("%d ", d[j]);
                    printf("\n");
                    return true;
                }
                Q.push(to);
            }
        }
    }
    return d[t] != 0;
}

那第一轮BFS之后,我们的各个点的d值就出来了(如下图红色部分),这个d值就决定了待会进行DFS的时候,哪些点可以沿哪些塔顶的轨迹行走:
在这里插入图片描述

3.找到那条增广路径并增大流:

int DFS(int from, int flow)//from是当前进行遍历的点的编号,flow是当前流进from点的流量;
{
    if(from == t)
        return flow;
    int sum = 0;//sum用来记录这一点from到底能够流多少流量;
    int r = flow;//r在接下来用于记录这一点from流了一定的流量之后,还剩下多少流量可流出;
    for(int i = h[from]; i && r; i = Edge[i].next)
    {
        int to = Edge[i].to;
        if(d[to] == d[from] + 1 && Edge[i].weight != 0)
        {
            int key = DFS(to, Min(r, Edge[i].weight));//递归找到以这个点为起点能流过多少流量,流进的流量为r和权值的最小值;
            if(key == 0)
                d[to] = 0;//如果以这个点起不能再流进,那么这个点作废;
            Edge[i].weight -= key;
            Edge[i + 1].weight += key;//[i + 1]就是边[i]的平行边;
            r -= key;
            sum += key;
        }
    }
    return sum;
}

每找到一条增光路径并增大流以后,原有路径会发生变化,原路径减去该增广路径的流量,原路径的反路径加上该增广路径的流量(至于正确性,那就是另外一个话题了,详情可以见《算法导论》),那么上面的图经过第一轮增大流之后变成了什么样子呢?在这里插入图片描述
将0边删除后就是这个样子:
在这里插入图片描述
这一轮过后我们找到了总流量为35,接下来我们继续进行BFS和DFS,这个图接下来还有两部循环,分别如下:
在这里插入图片描述
第二次DFS后流量加了1
在这里插入图片描述
最后一次DFS后还是加了1,因此最后结果为37.

在这里插入图片描述
最后附上完整代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <queue>
#define MAX 100010
#define INF 0x3f3f3f3f

int h[MAX];//用于存储每一个顶点最后连接的是哪一个顶点;
int num = 0;//用于记录边的数量,记住,每一对平行边的序号一定相邻;
int s,t;
int p[105][105];//用于记录两点的相邻与否;

struct node{
    int from;//该边的始点
    int to;//该边的终点
    int weight;//该边的权值
    int next;//该边始点下一个连接的边
}Edge[MAX];

int Min(int a, int b)
{
    return a < b ? a : b;
}

void AddEdge(int f, int t, int w)
{
    Edge[++num].from = f;
    Edge[num].to = t;
    Edge[num].weight = w;
    Edge[num].next = h[f];//第一个存储的next值就取0,以便于后面判定是否已经把这个点相邻的点找完(行45,行66)
    h[f] = num;//用于下一次再有以f为始点的边记录
}

int d[MAX];//用于记录到起点的距离,也可以当做在遍历时判定上一个点是否已经走过了的条件
bool BFS()
{
    memset(d,0,sizeof(d));//每一次重新找剩余网络都需要更新d数组;
    d[s] = 1;//其实在一般情况下,这里的起始点的d值应该为0才说得过去,但由于后面判定这个点是否已经走过是由d[i]是否为0来判定的,所以我们起始点用1
    std::queue<int> Q;
    Q.push(s);
    while(!Q.empty())
    {
        int q = Q.front();
        Q.pop();
        for(int i = h[q]; i != 0; i = Edge[i].next)
        {
            int to = Edge[i].to;
            if(d[to] == 0 && Edge[i].weight != 0)
            {
                d[to] = d[q] + 1;
                if(to == t)
                {
                    for(int j = s; j <= t; j ++)
                        printf("%d ", d[j]);
                    printf("\n");
                    return true;
                }
                Q.push(to);
            }
        }
    }
    return d[t] != 0;
}

int DFS(int from, int flow)//from是当前进行遍历的点的编号,flow是当前流进from点的流量;
{
    if(from == t)
        return flow;
    int sum = 0;//sum用来记录这一点from到底能够流多少流量;
    int r = flow;//r在接下来用于记录这一点from流了一定的流量之后,还剩下多少流量可流出;
    for(int i = h[from]; i && r; i = Edge[i].next)
    {
        int to = Edge[i].to;
        if(d[to] == d[from] + 1 && Edge[i].weight != 0)
        {
            int key = DFS(to, Min(r, Edge[i].weight));//递归找到以这个点为起点能流过多少流量,流进的流量为r和权值的最小值;
            if(key == 0)
                d[to] = 0;//如果以这个点起不能再流进,那么这个点作废;
            Edge[i].weight -= key;
            Edge[i + 1].weight += key;//[i + 1]就是边[i]的平行边;
            r -= key;
            sum += key;
        }
    }
    return sum;
}

int main()
{
    int N,i;
    scanf("%d", &N);//点的总数;
    s = 1;
    t = N;
    int a, b, c;
    memset(h, 0, sizeof(h));
    memset(p, 0, sizeof(p));
    while(scanf("%d%d%d", &a, &b, &c) != EOF)
    {
        if(p[a][b] == 0)
        {
            AddEdge(a,b,c);
            p[a][b] = num;
            AddEdge(b,a,0);
            p[b][a] = num;
        }
        else 
        {
            Edge[p[a][b]].weight += c;
        }
    }
    // for(i = 1; i <= num; i++)
    //     printf("from = %d to = %d weight = %d next = %d\n", Edge[i].from, Edge[i].to, Edge[i].weight, Edge[i].next);
    int ans = 0;
    while(BFS())
    {
        ans += DFS(s, INF);
        printf("%d\n", ans);
    }
    printf("%d\n", ans);
    system("pause");
    return 0;
}
参考资源链接:[Dinic算详解:层次图与非递归实现详解](https://wenku.csdn.net/doc/n9mz9ufjzn?utm_source=wenku_answer2doc_content) Dinic算在求解网络最大流问题时,主要通过构建层次图来优化增广路径的搜索过程。层次图是一种特殊结构,它将原图中的节点根据距离源点(s)的远近划分为不同的层次,这样的划分有助于快速定位到可能的增广路径。下面是层次图构建和增广路径查找过程的具体说明: 首先,层次图的构建基于DFS实现,从源点(s)开始,递归地遍历所有可达的节点,并为每个节点分配一个层次号。在遍历过程中,记录当前节点的前驱节点以及与之相连的边的残余容量。通过这个过程,我们能够构建出一个新的图,其中只包含那些能够对流进行有效增加的边,这样就避免了无效的搜索,提高了算的效率。 接着,在增广路径查找中,算利用已构建的层次图,在每个层次中从源点开始寻找能够向后一层次的节点推进的路径,即增广路径。这里可以使用DFS或者BFS,但是在Dinic算中通常采用BFS,因为它可以在O(n+m)的时间内找到一条从源点到汇点的路径,这样的路径被称为增广路径。找到增广路径后,算会尝试通过这条路径发送流量,更新残量网络,并修改层次图中的节点层次信息。 在查找增广路径的过程中,如果使用了BFS,那么就能保证在每一层内只选择距离最小的路径,这样可以更快地找到最短的增广路径,从而提高整个算的效率。这也是Dinic算比其他如Ford-Fulkerson算在时间复杂度上有优势的原因之一。 总的来说,层次图的构建和增广路径的查找是Dinic算提高效率的关键所在。通过限制搜索范围,并在每一层内快速找到最短的增广路径,Dinic算能够以较为合理的时间复杂度达到最大流的计算结果。 建议想要深入理解层次图构建和增广路径查找过程的读者,参考《Dinic算详解:层次图与非递归实现详解》一书。本书详细介绍了Dinic算的每一个步骤,包括层次图的构建、增广路径的查找和非递归实现等多个方面,是理解Dinic算精髓的实用资料。 参考资源链接:[Dinic算详解:层次图与非递归实现详解](https://wenku.csdn.net/doc/n9mz9ufjzn?utm_source=wenku_answer2doc_content)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值