算法导论 26章 最大流(一)

来源:互联网 发布:自学跳舞的软件 编辑:程序博客网 时间:2024/06/05 07:56

最大流问题
为了求一点到另一点的最短距离,我们可以把公路地图模型化为有向图。同理,我们可以将公路模型化为一个“流网络”(flow network),并运用它来解决物流相关的问题。其中最大流问题是其中一种,即:在不违背每条路(即有向图的边)的容量的情况下,可以把物质从源点送到汇点的最大速率是多少。

最大流定义和性质
流网络是一个有向图G=(V,E),其中每条边(u,v)均有一个容量c(u,v) >= 0;如果边c(u,v)不属于E,则c(u,v) = 0.流网络一般只有两个顶点:源点s和汇点t。
对于流网络G = (V,E),容量函数为c,流函数为f。该网络满足下述三个性质:
1. 容量限制:对于所有的u,v属于E,f(u,v) <= c(u,v);
2. 反对称性: 对于所有的u,v属于E,f(u,v) = -f(v,u);
3. 流守恒性:对于所有的u属于V-{s,t},有:
这里写图片描述

从一个顶点u到另一个顶点v的流f的值为f(u,v),可正可负亦可为0。对于源点s来说,从它出发的流即为整个流网络的总流,它的值为:![这里写图片描述](http://img.blog.csdn.net/20150322153643828) 而对于最大流问题,就是希望找出从源点s到汇点t的最大值流。

最大流的例子
这里写图片描述
如上图,是一个流网络,其中对于边的数值,左边为该边上的流值,右边是改变允许通过的最大流值,即容量。可以看出最大流的值为23,可以通过累加源点的输出总流或者汇点的输入总流获得。

Ford-Fulkerson方法解决最大流问题
1、残留网络。
给定一个流网络G=(V,E)和流f,在输入流f后G的残留网络为Gf = (V,Ef),其中:
这里写图片描述,即由所有剩下的容量大于0的边所组成。如下图a是从源点s输入流11和8后得到的残留网络b。
这里写图片描述
其中,最初的原图G也是最初的残留网络。b中的粗线表示一条增广路径。
2、增广路径。
增广路径是用Ford-Fulkerson解决最大流问题的关键。增广路径p是残留网络Gf中,从源点s到汇点t的一条简单路径,该路径的每条边的容量都大于0,这意味我们可以通过这条路径继续输入流。
由此我们可以得到用Ford-Fulkerson方法求取最大流问题的算法了,如下:
1、在Gf中找到一条增广路径,求取该条路径的最小容量:这里写图片描述
2、更新该条路径上所有边(u,v)的容量c(u,v)和f(u,v),同时还需要更新c(v,u)和f(v,u);
3、循环1和2直到残留网络中不再有增广路径了;

用伪代码表示如下:

Ford - Fulkerson(G, s, t){    for each edge(u, v) in E[G]    {        f[u, v] = 0;        f[v, u] = 0;    }    while (there exists an augmenting path p)    {        cf(p) = min{ cf(u, v) :(u, v) is in p };        for each edge(u, v) in p        {            f[u, v] = f[u, v] + cf(p);            f[v, u] = -f[u, v];            cf(u, v) = cf(u, v) - cf(p);            cf(v, u) = -f[v, u];        }    }}

根据以上算法,可以看出,唯一未解之处在于如何寻找该增广路径。根据寻找增广路径所使用的算法的不同,可以得到具体不同的算法。如果我们运用广度优先搜索来遍历整个Gf依次判断是否还存在增广路径,这种算法被称为Edmonds-Karp算法。

多源点多汇点最大流问题
对于该问题,我们可以引入一个超级源点,将其与各个源点相连,并且将相连边的容量设为无穷大,同理,也引入一个超级汇点,将其与各个汇点相连,且将相连边容量设为无穷,如此,该问题就成为了单源点单汇点问题了。

接下来我将给出用C++实现Edmonds-Karp算法的具体代码。首先我们约定:为了便于实现,所有的顶点将用数字1,2,3…编号,从下标为1开始存入数据,并且顶点编号和下标索引对应,即G[i]存储的是i号节点的邻接链表。

//Ford-Fulkerson方法求最大流,用广度优先搜索寻找增广路径,为Edmonds-Karp算法#include<iostream>#include<queue>#include<fstream>#include<vector>#define MAX 0x7fffffffusing namespace std;enum color{ WHITE, GRAY, BLACK };//用来标记顶点颜色struct edgeNode{//边节点    size_t adjvertex;//该边的关联的顶点    int capacity;//边当前容量    int flow;//边当前流量    edgeNode *nextEdge;//下一条边    edgeNode(size_t adj, int w) :adjvertex(adj), capacity(w), flow(0), nextEdge(nullptr){}};class AGraph{//有向图private:    vector<edgeNode*> G;    size_t nodenum;public:    AGraph(size_t n) :nodenum(n){ G.resize(n + 1); }    void initGraph();//初始化有向图    edgeNode* search(size_t, size_t);//查找边    edgeNode* addEdge(size_t, size_t, int);//向图中添加边    void deleteEdge(size_t, size_t);//有向图中删除边    bool BFS(size_t, size_t, vector<size_t>&);//广度优先搜索,返回一个前驱数组    int Edmonds_Karp(size_t, size_t);//Ford_Fulkerson方法的一种具体实现,采用广度BFS搜索增广路经    void print();    ~AGraph();};bool AGraph::BFS(size_t s, size_t t, vector<size_t> &p){    bool augment_path = false;//记录是否存在增广路径    queue<size_t> Q;    vector<size_t> dis(nodenum + 1), color(nodenum + 1);    for (size_t i = 1; i <= nodenum; ++i)    {        dis[i] = MAX;        p[i] = i;        color[i] = WHITE;    }    color[s] = GRAY;    dis[s] = 0;    p[s] = s;    Q.push(s);    while (!Q.empty())    {        size_t u = Q.front();        if (u == t) augment_path = true;//若可以遍历到汇点,则存在        Q.pop();        edgeNode *curr = G[u];        while (curr != nullptr)        {            if (color[curr->adjvertex] == WHITE && curr->capacity > 0)            {                color[curr->adjvertex] = GRAY;                dis[curr->adjvertex] = dis[u] + 1;                p[curr->adjvertex] = u;                Q.push(curr->adjvertex);            }            curr = curr->nextEdge;        }        color[u] = BLACK;    }    return augment_path;}int AGraph::Edmonds_Karp(size_t s, size_t t){//若用两个二维数组分别存储边(u,v)的flow和capacity,就可以省去search和addEdge了,    //可以改善效率(但是提高不了渐进时间),不过增加了使用空间    vector<size_t> parent(nodenum + 1);    while (BFS(s, t, parent))//用BFS寻找从s到t的增广路径,parent记录前驱    {//若存在        int increase_flow = MAX;        size_t tmp = t;        while (tmp != parent[tmp])        {//则确定该增广路径上的最小容量            edgeNode *curr = search(parent[tmp], tmp);            if (curr->capacity < increase_flow) increase_flow = curr->capacity;            tmp = parent[tmp];        }        tmp = nodenum;        while (tmp != parent[tmp])        {//然后对该路径上边增加流量,同时将减小当前容量            edgeNode *r = search(parent[tmp], tmp);            r->flow = r->flow + increase_flow;            r->capacity -= increase_flow;            //当我们增加了f(u,v),意味着逆边(v,u)增加了容量c(v,u),            //即使该边最初是不存在的,因为v可以将该流返回给u,            //addEage功能请参考该函数注释            edgeNode *p = addEdge(tmp, parent[tmp], 0);            p->flow = -r->flow;//根据流对称性获得对称边的当前流            p->capacity -= p->flow;//同时更新其当前容量            tmp = parent[tmp];        }    }    //没有增广路径时,说明已找到最大流    int maximum_flow = 0;    edgeNode *p_s = G[s];    while (p_s != nullptr)    {//统计源点的流总量即可        maximum_flow += p_s->flow;        p_s = p_s->nextEdge;    }    return maximum_flow;}void AGraph::initGraph(){    size_t start, end;    int w;    ifstream infile("F:\\maximumflow.txt");    while (infile >> start >> end >> w)        addEdge(start, end, w);}edgeNode* AGraph::search(size_t start, size_t end){    edgeNode *curr = G[start];    while (curr != nullptr && curr->adjvertex != end)        curr = curr->nextEdge;    return curr;}edgeNode* AGraph::addEdge(size_t start, size_t end, int capacity){//添加边    edgeNode *curr = search(start, end);//先查找    if (curr == nullptr)    {//若边不存在,则添加        edgeNode *p = new edgeNode(end, capacity);        p->nextEdge = G[start];        G[start] = p;        return p;    }    return curr;//否则不添加,返回当前边地址}void AGraph::deleteEdge(size_t start, size_t end){    edgeNode *curr = search(start, end);    if (curr != nullptr)    {        if (curr->adjvertex == end)        {            G[start] = curr->nextEdge;            delete curr;        }        else        {            edgeNode *pre = G[start];            while (pre->nextEdge->adjvertex != end)                pre = pre->nextEdge;            pre->nextEdge = curr->nextEdge;            delete curr;        }    }}inline void AGraph::print(){    for (size_t i = 1; i != G.size(); ++i)    {        edgeNode *curr = G[i];        cout << i;        if (curr == nullptr) cout << " --> null";        else            while (curr != nullptr)            {                cout << " --<" << curr->capacity << ">--> " << curr->adjvertex;                curr = curr->nextEdge;            }        cout << endl;    }}AGraph::~AGraph(){    for (size_t i = 1; i != G.size(); ++i)    {        edgeNode *curr = G[i], *pre;        while (curr != nullptr)        {            pre = curr;            curr = curr->nextEdge;            delete pre;        }    }}const int nodenum = 6;/*1 2 16 1 5 132 3 12 2 5 103 5 9 3 6 204 3 7 4 6 45 2 4 5 4 14*/int main(){//测试数据采用的是上面的图,其中所有的顶点按顺时针方向从源点s(1号)开始依次编号    AGraph graph(nodenum);    graph.initGraph();    graph.print();    cout << endl;    int maximum_flow = graph.Edmonds_Karp(1, nodenum);    cout << "The maximum flow is " << maximum_flow << endl;    //graph.print();    getchar();    return 0;}

运行截图
输出中<>里面是各边最初的容量,可以理解为权值,只不过这个权值是可以改变的。

这里写图片描述

最大二分匹配
对于一个无向图G=(V,E),一个匹配时一个边子集M,其包含于E,且对于所有顶点v属于V,M中至多有一条边与v相关联。如果M中某条边与v关联,则说顶点v被匹配,否则是无匹配的。最大匹配就是最大势的匹配,即对于任何一个匹配M’,有|M| > |M’|。

二分图是这样一种图:确定点被分为L和R两部分,L和R不相交,且E中的每条边一个顶点在L,另一个顶点在R。我们假设二分图中的每个顶点至少有一条边与其关联。下图展现了匹配的概念。

这里写图片描述

那么如何寻找二分图的最大匹配呢?我们知道,对于二分图中的每个顶点v,至多能和M中的一条边关联,这就相当于对于v而言,从它出发的流最多为1,而且该边的容量最大也只能为1。因此,寻找二分图的最大匹配可以转换为在各边容量均为1的多源点多汇点的流网络中寻找最大流。对于上图,我们添加一个超级源点和一个超级汇点,得到单源点单汇点的流网络,如下图:

这里写图片描述

然后运行Edmonds-Karp算法即可得到最大匹配值,为3。
0 0