摘要

今日,对最小费用最大流问题进行了一个简单的研究,并针对网上的一些已有算法进行了查找和研究。博客和资料很多,但是留下的算法很多运行失败、出错,或者意义不明。这里,个人对其中的Bellman-Ford、SPFA、改进的Dijkstra三种应用于最小费用最大流的算法进行了实现,经过测试,确保其可行性。

网络流

关于网络流,这里引入几个概念:

  • 源点:有n个点,有m条有向边,其中有一个点比较特殊,它只出不进,即入度为0。这样的点我们称为源点,一般用字母S表示。
  • 汇点:另一个点也比较特殊,只进不出,即出度为0。这样的点我们称为汇点,一般用字母T表示。
  • 容量和流量:每条有向边上有两个量,容量和流量,从i到j的容量表示为c[i,j]表示,流量则用f[i,j]表示。

通常来说,我们可以将这些边具象成道路,流量就是这条道路上的车的容量,容量就是道路可以承受的最大的车流量。
很明显,流量≤容量
而对于每个不是源点和汇点的节点而言,可以类比为没有存储功能的货物的中转站。所有进入他们的流量等于所有从它出来的流量。

  • 最大流:把源点比作工厂的话,问题就是求工厂最大可以发出多少货物,而不至于超过道路的容量限制,也即,最大流问题。

求解思路:
首先,假如所有边上额流量都不超过容量,那么我们就把这一组流量,或者说,这个流,称为一个可行流
一个最简单的可行流的例子就是零流,即所有的流量都是0的流。

  1. 我们从这个零流开始考虑,假如有这样一条路径,这条路从原点开始一直一段一段的连接,就可以到达汇点。并且,这条路径上的每一段都会满足流量<容量(注意,不是≤,而是严格<)
  2. 那么,我们就一定可以找到这条路径上的每一段的(容量-流量)的值中的最小值delta。我们把这条路上的每一段的流量都加上这个delta,就一定可以保证目前这个流依然是可行流。
  3. 这样,我们就找到了一个更大的流,它的流量是之前的流量+delta,而这条路径就称为增广路径。我们不断地从起点开始寻找增广路径,每次都对其进行增广,直到源点和汇点不再连通,也就是找不到增广路径为止。
  4. 当我们找不到增广路径时,当前的流量就是最大流

补充:

  1. 寻找增广路径的时候我们可以简单的从原点开始做BFS,并不断修改这条路上的delta量,直到找到汇点或者找不到增广路径为止。
  2. 在程序实现的过程中,我们通常只是使用一个c数组来记录容量,而不是记录流量。当流量+delta时,我们可以通过容量-delta来实现,以方便程序编写。

增加反向边的目的:
在做增广路径时可能会阻塞后来的增广路径,换计划说,做增广路径本来是有一个顺序的,只有按照有这一顺序,才能知道最大流。
但是我们在寻找时是任意的,为了修正,我们就每次讲流量加入到了反向弧中从而让后面的流能够进行自我的调整。

例子


我们第一次,可以找到1-2-3-4这条增广路径,这条路径上的delta值显然为1。

此时,我们在修改之后得到了下面这个流。其中,边上的数字代表流量。


此时,边(1,2)和边(3,4)上的边就等于容量了,我们也再也找到不到其他的增广路径,于是,当前的流量是1。

然而,这个答案并不是最大流,因为我们可以同时走1-2-4和1-3-4,这样,可以得到流量为2的最大流。

之所以出现这样的问题,是因为我们在路径寻找的过程中,没有给一个“后悔”的机会,应该有一个不走2-3-4而改走2-4的机制。

而解决这个问题的办法,就是利用一种叫做反向边的概念来解决。
即每条边(i, j)都会有一条反向边(j,i),反向边也同样有它的容量。

在第一次找到增广路径之后,在把路径上每一段容量减少delta的同时,也把每一段上的反方向的容量增加delta。

c[x,y]-=delta;
c[y,x]+=delta;

就上面这个例子,当我们找到1-2-3-4这条增广路径之后,将容量修改如下:


此时我们再去寻找增广路径,就可以得到一条:1-3-2-4,将这条路径增广之后,得到最大流为2.

这样为何有效?
实际上,当我们第二次的增广路径走3-2这条反向边时,就相当于把2-3这条正向边已经用的流量给“退”了回去,不走2-3这条路,从而改走从2点出发的其他的路径也即是2-4。

而如果这里没有2-4怎么办?
这时,假如没有2-4这条道路,那么最终这条增广路径在生成过程中也不会存在,因为最终它根本无法到达汇点。
同时,本来在3-4上的流量则是由1-3-4来“接管”。而最终2-3这条路径正向流量为1,反向流量也为1,等于没有流。

最小费用最大流

对一个费用容量网络,具有相同流量f的可行流中,总费用最小的可行流称为该费用容量网络关于流量f的最小费用流。简称为流量为f的最小费用流

什么是最小费用最大流问题:
给定网络D=(V,A,C) 每一条弧(vi,vj)上,除了已给容量Cij外,还给了一个单位流量的费用b(vi,vj)>=0. 所谓最小费用最大流问题就是求一个最大流f,使流的总输送费用最小

对于例子:

从S出发,到达T,正确路径结果,为:

  • S->1->3->T
  • S->2->4->3->T
  • S->2->4->5->T

最大流为10,最小费用为84
以下算法在该图中测试,均可得出正确结果。

贝尔曼-福特算法(Bellman-Ford algorithm)

贝尔曼-福特算法(Bellman-Ford algorithm),是求解单元最短路径的一种算法。
它的基本原理是对图进行|V| - 1次松弛操作,得到所有可能的最短路径。
它比Dijkstra算法好的部分在于,在计算最短路径的班的权值可以为负,实现起来比较简单。
缺点则是时间复杂度较高,为O(|V||E|)。不过算法已经有了一些改进方案,比如队列优化的Bellmanford算法(SPFA算法),一定程度上提高了效率。

算法原理:
贝尔曼-福特算法与迪科斯彻算法类似,都以松弛操作为基础,即估计的最短路径值渐渐地被更加准确的值替代,直至得到最优解。
在两个算法中,计算时每个边之间的估计距离值都比真实值大,并且被新找到路径的最小长度替代。
然而,迪科斯彻算法以贪心法选取未被处理的具有最小权值的节点,然后对其的出边进行松弛操作;而贝尔曼-福特算法简单地对所有边进行松弛操作,共**|V| - 1**次。
在重复地计算中,已计算得到正确的距离的边的数量不断增加,直到所有边都计算得到了正确的路径。
因为算法可以使用负权值的边,因此贝尔曼-福特算法比迪科斯彻算法适用于更多种类的输入。

优化选项:

  1. 循环的提前跳出
    在实际操作中,贝尔曼-福特算法经常会在未达到|V|-1前就给出解,|V|-1就是最大值。因此可以在循环中设置判定,在某次循环不再松弛时,直接退出循环,进行负权环判定。
  2. 队列优化
    队列优化的贝尔曼-福特算法——SPFA算法基本思路与原算法是一样的,不过该算法的提升在于它不会盲目尝试所有的节点,而是维护一个备选节点队列,并且仅有节点被松弛之后才会放入到队列中。

代码实现

#include "stdafx.h"
#include <iostream>
#include <algorithm>
#include <map>
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <vector>
#include <queue>
#include <stack>#define MAXN 5050
#define INF 0x3f3f3f3fusing namespace std;int n, m, s, t;
int u, v, c, w;
int maxFlow, minCost;struct Edge
{int from, to, flow, cap, cost;
};bool vis[MAXN];
int p[MAXN], a[MAXN], d[MAXN];
vector<int> g[MAXN];
vector<Edge> edges;void init(int n)
{for (int i = 0; i <= n; i++)g[i].clear();edges.clear();
}
void addedge(int from, int to, int cap, int cost)
{Edge temp1 = { from, to, 0, cap, cost };Edge temp2 = { to, from, 0, 0, -cost };//允许反向增广edges.push_back(temp1);edges.push_back(temp2);int len = edges.size();g[from].push_back(len - 2);g[to].push_back(len - 1);
}//贝尔曼-福特算法实现
bool bellmanford(int s, int t)
{for (int i = 0; i < MAXN; i++)d[i] = INF;d[s] = 0;memset(vis, false, sizeof(vis));memset(p, -1, sizeof(p));p[s] = -1;a[s] = INF;queue<int> que;que.push(s);vis[s] = true;while (!que.empty()){int u = que.front();que.pop();vis[u] = false;for (int i = 0; i < g[u].size(); i++){Edge& e = edges[g[u][i]];if (e.cap > e.flow&&d[e.to] > d[u] + e.cost)//进行松弛,寻找最短路径也就是最小费用{d[e.to] = d[u] + e.cost;p[e.to] = g[u][i];a[e.to] = min(a[u], e.cap - e.flow);if (!vis[e.to]){que.push(e.to);vis[e.to] = true;}}}}if (d[t] == INF)return false;maxFlow += a[t];minCost += d[t] * a[t];for (int i = t; i != s; i = edges[p[i]].from){edges[p[i]].flow += a[t];edges[p[i] ^ 1].flow -= a[t];}return true;
}void MCMF()
{while (bellmanford(s, t))continue;return;
}int _tmain(int argc, _TCHAR* argv[])
{cout << "节点数为:"; cin >> n;cout << "边数为:"; cin >> m;cout << "源点编号为:"; cin >> s;cout << "汇点编号为:"; cin >> t;cout << "输入 " << m << " 条边的信息:" << endl;while (m--){cout << "起点:"; cin >> u;cout << "终点:"; cin >> v;cout << "容量:"; cin >> c;cout << "费用:"; cin >> w;cout << "-----------------" << endl;addedge(u, v, c, w);}MCMF();cout << "最大流为:" << maxFlow << endl;cout<< "最小费用为"<<minCost << endl;cout << endl;system("pause");return 0;}

SPFA算法

算法描述:

  1. 初始化:distance数组(从源点s到各点的最小费用)全部赋值为inf,用一个队列保存所有待松弛的顶点,初始时将s点放入队列中。
  2. 队列+松弛操作:每次出队一个顶点u,对其所有的边进行松弛,如果存在某条边u->v松弛成功(dist(v)>dist(u)+w(u,v)),则将v加入队列中(当v不在队列时);重复以上操作直到队列为空或者发现负权环。
    如果网络中存在负权回路,则算法永远都不会结束,陷入死循环。
  • 判断是否存在负权环的方法:
    对任何一个顶点,每进入一次队列,意味着需要进行一次松弛,即如果某个顶点进入队列的次数超过V,说明存在负权环。

算法步骤:

  1. 建立一个队列,将源点加入队列中,建立一个数组dist记录源点到所有点的最短路径(初始为inf,源点到本身的最短路径是0)。
  2. 从队列中取出队头元素,刷新其连接的所有点的最短路径;如果刷新成功且被刷新点不在队列中,则把该点加入到队尾。
  3. 重复执行以上步骤直到队列为空或者队列中存在负权环。

代码实现

#include "stdafx.h"
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<iostream>#define MAXN 5050using namespace std;bool vis[MAXN];
int n, m, s, t;
int u, v, c, w;
int cost[MAXN], pre[MAXN], last[MAXN], flow[MAXN];
int maxFlow, minCost;
struct Edge
{int from, to, flow, cost;
}edge[MAXN];int head[MAXN], num_edge;queue <int> q;void addedge(int from, int to, int flow, int cost)
{edge[++num_edge].from = head[from];edge[num_edge].to = to;edge[num_edge].flow = flow;edge[num_edge].cost = cost;head[from] = num_edge;edge[++num_edge].from = head[to];edge[num_edge].to = from;edge[num_edge].flow = 0;edge[num_edge].cost = -cost;head[to] = num_edge;}bool SPFA(int s, int t)
{memset(cost, 0x7f, sizeof(cost));memset(flow, 0x7f, sizeof(flow));memset(vis, 0, sizeof(vis));q.push(s); vis[s] = 1; cost[s] = 0; pre[t] = -1;while (!q.empty()){int now = q.front();q.pop();vis[now] = 0;for (int i = head[now]; i != -1; i = edge[i].from){if (edge[i].flow>0 && cost[edge[i].to]>cost[now] + edge[i].cost){cost[edge[i].to] = cost[now] + edge[i].cost;pre[edge[i].to] = now;last[edge[i].to] = i;flow[edge[i].to] = min(flow[now], edge[i].flow);if (!vis[edge[i].to]){vis[edge[i].to] = 1;q.push(edge[i].to);}}}}return pre[t] != -1;
}void MCMF()
{while (SPFA(s, t)){int now = t;maxFlow += flow[t];minCost += flow[t] * cost[t];while (now != s){edge[last[now]].flow -= flow[t];edge[last[now] ^ 1].flow += flow[t];now = pre[now];}}
}int _tmain(int argc, _TCHAR* argv[])
{   memset(head, -1, sizeof(head)); num_edge = -1;//初始化 cout << "节点数为:"; cin >> n;cout << "边数为:"; cin >> m;cout << "源点编号为:"; cin >> s; cout << "汇点编号为:"; cin >> t; cout << "输入 " << m << " 条边的信息:" << endl;while (m--){cout << "起点:"; cin >> u; cout << "终点:"; cin >> v; cout << "容量:"; cin >> c; cout << "费用:"; cin >> w; cout << "-----------------" << endl;addedge(u, v, c, w);}MCMF();cout << "最大流为:" << maxFlow << endl;cout << "最小费用为:" << minCost << endl;cout << endl;system("pause");return 0;
}

改进的Dijkstra算法

算法描述:
用于求解指定两点间的最短路,或从指定点到其余个点的最短路。是目前求非负权网络最短路问题的最好方法。
基本步骤:

  1. 将所有的顶点分成两个集合,P集合和Q集合,初始时P集合只有源点,其他顶点都在Q集合中。
  2. 每次选择P集合中新加入的顶点u,用该顶点作为中转点更新Q集合中的顶点的最短路(松弛);选择Q中最短路值最小的顶点加入到集合P中。
  3. 重复步骤2直到集合Q中没有顶点。

由于最小费用最大流网络中存在负权值,Dijkstra算法不能直接求解最小费用最大流问题,如果最小费用最大流网络中的权值都非负,则可使用Dijkstra算法。引入势函数h(u)为上一次Dijkstra算法的dist(u)(表示从源点到顶点u的最短距离),对每一条边(u,v),h(v)<=h(u)+w(u,v)成立,则下一次计算中dist(v)=dist(u)+w(u,v)+h(u)-h(v),所有的dist值必然都大于等于0,则可以继续用Dijkstra算法求解最短路。

代码实现:

#include "stdafx.h"
#include <iostream>
#include <algorithm>
#include <queue>
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <functional>#define MAXN 5050
#define INF 0x3f3f3f3f
#define P pair<int,int>using namespace std;struct edge
{int to, cap, cost, rev;
};int n, m, s, t;
int u, v, c, w;
int maxFlow, minCost;vector<edge> G[MAXN];
int h[MAXN];
int dist[MAXN], prevv[MAXN], preve[MAXN];void addedge(int from, int to, int cap, int cost)
{edge temp1 = { to, cap, cost, (int)G[to].size() };edge temp2 = { from, 0, -cost, (int)G[from].size() - 1 };G[from].push_back(temp1);G[to].push_back(temp2);
}//Dijkstra算法实现
void MCMF(int s, int t, int f)
{fill(h + 1, h + 1 + n, 0);while (f > 0){priority_queue<P, vector<P>, greater<P> > D;memset(dist, INF, sizeof dist);dist[s] = 0; D.push(P(0, s));while (!D.empty()){P now = D.top(); D.pop();if (dist[now.second] < now.first) continue;int v = now.second;for (int i = 0; i<(int)G[v].size(); ++i){edge &e = G[v][i];if (e.cap > 0 && dist[e.to] > dist[v] + e.cost + h[v] - h[e.to]){dist[e.to] = dist[v] + e.cost + h[v] - h[e.to];prevv[e.to] = v;preve[e.to] = i;D.push(P(dist[e.to], e.to));}}}if (dist[t] == INF) break;for (int i = 1; i <= n; ++i) h[i] += dist[i];int d = f;for (int v = t; v != s; v = prevv[v])d = min(d, G[prevv[v]][preve[v]].cap);f -= d; maxFlow += d;minCost += d * h[t];for (int v = t; v != s; v = prevv[v]){edge &e = G[prevv[v]][preve[v]];e.cap -= d;G[v][e.rev].cap += d;}}
}int _tmain(int argc, _TCHAR* argv[])
{cout << "节点数为:"; cin >> n;cout << "边数为:"; cin >> m;cout << "源点编号为:"; cin >> s;cout << "汇点编号为:"; cin >> t;cout << "输入 " << m << " 条边的信息:" << endl;while (m--){cout << "起点:"; cin >> u;cout << "终点:"; cin >> v;cout << "容量:"; cin >> c;cout << "费用:"; cin >> w;cout << "-----------------" << endl;addedge(u, v, c, w);}MCMF(s, t, INF);cout << "最大流为:" << maxFlow << endl;cout << "最小费用为" << minCost << endl;cout << endl;system("pause");return 0;
}

算法对比

名称 特点 不足
Bellman-Ford 可以解决负权边,但不允许有负环 每次循环值均对所有元素进行松弛判断,造成许多不必要的操作。
SPFA 进阶版的BF,使用队列进行优化,每次循环值选择当前节点相邻的若干节点进行松弛。在稀疏图上十分高效 单路增广。SPFA需要维护较为复杂的标号和队列操作,同时为了修正标号,需要不止一次地访问某些节点,速度会比较慢。
改进的Dijkstra 速度普遍比SPFA要快。 无法直接处理负权边图,需要对算法进行改进。

补充

除了上述三种算法之外,还有诸如Dinic、ZKW等算法,不过个人没有研究,这里就不再赘述了。

参考文献

[1] 最小费用最大流(详解+模板)
[2] 数据结构与算法分析 - 网络流入门(Network Flow)
[3] 最小费用最大流问题
[4] 维基百科-最小费用最大流问题
[5]【最小费用最大流】知识点讲解
[6] P3381 【模板】最小费用最大流 题解

最小费用最大流问题与算法实现(Bellman-Ford、SPFA、Dijkstra)相关推荐

  1. mysql最小费用最大流问题_算法笔记_140:最小费用最大流问题(Java)

    packagecom.liuzhen.practice;importjava.util.ArrayList;importjava.util.Scanner;public classMain {publ ...

  2. matlab最小费用最大流函数,Matlab最小费用最大流算法通用程序

    下面的最小费用最大流算法采用的是"基于Floyd最短路算法的Ford和Fulkerson迭加算法",其基本思路为:把各条弧上单位流量的费用看成某种长度,用Floyd求最短路的方法确 ...

  3. 最小生成树、最大流、最小费用最大流问题精简

    最小生成树.最大流.最小费用最大流问题精简 最小生成树:   简单来说即图中一个使各点连通的N-1个边的子图,当边权和最小时为最小生成树. 经典Prim,Kruskal算法: (1)Prim:(从点出 ...

  4. matlab最小费用最大流函数,最小费用最大流算法通用Matlab程序

    下面的最小费用最大流算法采用的是"基于Floyd最短路算法的Ford和Fulkerson迭加算法",其基本思路为:把各条弧上单位流量的费用看成某种长度,用Floyd求最短路的方法确 ...

  5. mysql最小费用最大流问题_最小费用最大流问题

    复杂网络中,单源单点的最小费用最大流算法(MCMF)应用广泛. 在实际网络问题中,不仅考虑从 Vs到 Vt的流量最大,还要考虑可行流在网络传送过程中的费用问题,这就是网络的最小费用最大流问题. 最小费 ...

  6. 数学建模常用Matlab/Lingo/c代码总结系列——最小费用最大流问题

    例 19(最小费用最大流问题)(续例18)由于输油管道的长短不一或地质等原因, 使每条管道上运输费用也不相同,因此,除考虑输油管道的最大流外,还需要考虑输油 管道输送最大流的最小费用.图 8 所示是带 ...

  7. 最小费用最大流问题详解

    最小费用最大流问题 一.问题描述 在网络中求一个最大流f,使流的总输送费用最小. b(f)=∑(vi,vj)bijfijb(f) = \sum\limits_{(v_i,v_j)} b_{ij} f_ ...

  8. 网络流:最小费用最大流问题

    前置知识:最大流问题 最小费用最大流问题: 在最大流问题基础上,为每条边赋值单位流量的花费.求解保证最大流时,最小花费为多少.(因为最大流可以有多种流分配方案) 以EK算法为基础,在bfs时增加求最短 ...

  9. 最小费用最大流算法 网络流

    最小费用最大流算法 图片来源 <趣学算法> 人民邮电出版社 陈小玉 代码实现 /* 参考:<趣学算法>陈小玉 人民邮电出版社 最小费用最大流---最小费用路算法 问题分析:在实 ...

最新文章

  1. 关于.NET编译的目标平台(AnyCPU,x86,x64) (转)
  2. MySQL设置数据库的字符编码为utf8
  3. 消息摘要的编程使用(MD5、SHA、HMAC)
  4. Javascript中的单例和模块模式
  5. Android DownloadManager 的使用
  6. qj71c24n通讯实例_Q系列串行口通信模块用户参考手册QJ71C24N(基础篇).pdf
  7. html tab标签_如何用HTML写一个网页
  8. VS2019正式版注册码秘钥
  9. donet 微服务开发 学习-consul 消费端开发
  10. apkg格式怎么打开_jpg怎么转换成pdf?再不学就晚了
  11. typeorm实战之findOne()方法
  12. win7计算机不支持此接口,Win7 "explorer.exe 不支持此接口"问题
  13. HTML5期末大作业:仿唯品会购物网站设计——仿唯品会购物商城(5页) HTML+CSS+JavaScript 学生DW网页设计作业成品 商城网站设计
  14. 清华大学python_清的解释|清的意思|汉典“清”字的基本解释
  15. 解决”error: info is different in .repo/manifests/.git vs .repo/manifests.git报错
  16. css图片颜色设置为黑白
  17. 看完《指环王》说几句
  18. 网络七层协议地图,报文格式一览无遗。绝对是干货,值得收藏
  19. mysql:列类型之decimal、numeric
  20. 电信JAVA手机_手机modem开发(28)—开发电信VoLTE开关默认值设置

热门文章

  1. 射击小游戏c语言实验报告,C++实现简单射击小游戏
  2. uniapp 获取 iphone x 底部黑线高度_安卓手机越狱你得iPhone、iPad最新版教程
  3. 【翻译】Kinect v2程序设计(C++) BodyIndex篇
  4. 计组实验:logisim入门实验
  5. python爬取新浪微博热门话题保存到excel等文件
  6. python if elif else_Python的 if .else.elif语句详解
  7. zabbix编译解决ldap问题
  8. vulnstack红日-三
  9. 精准操盘公式 擒庄操盘 通达信趋势操盘抄底主图选股
  10. LinuxProbe学习笔记(八)