今日,对最小费用最大流问题进行了一个简单的研究,并针对网上的一些已有算法进行了查找和研究。博客和资料很多,但是留下的算法很多运行失败、出错,或者意义不明。这里,个人对其中的Bellman-Ford、SPFA、改进的Dijkstra三种应用于最小费用最大流的算法进行了实现,经过测试,确保其可行性。
关于网络流,这里引入几个概念:
通常来说,我们可以将这些边具象成道路,流量就是这条道路上的车的容量,容量就是道路可以承受的最大的车流量。
很明显,流量≤容量。
而对于每个不是源点和汇点的节点而言,可以类比为没有存储功能的货物的中转站。所有进入他们的流量等于所有从它出来的流量。
求解思路:
首先,假如所有边上额流量都不超过容量,那么我们就把这一组流量,或者说,这个流,称为一个可行流。
一个最简单的可行流的例子就是零流,即所有的流量都是0的流。
补充:
增加反向边的目的:
在做增广路径时可能会阻塞后来的增广路径,换计划说,做增广路径本来是有一个顺序的,只有按照有这一顺序,才能知道最大流。
但是我们在寻找时是任意的,为了修正,我们就每次讲流量加入到了反向弧中从而让后面的流能够进行自我的调整。
例子:
我们第一次,可以找到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,使流的总输送费用最小。
最大流为10,最小费用为84。
以下算法在该图中测试,均可得出正确结果。
贝尔曼-福特算法(Bellman-Ford algorithm),是求解单元最短路径的一种算法。
它的基本原理是对图进行|V| - 1次松弛操作,得到所有可能的最短路径。
它比Dijkstra算法好的部分在于,在计算最短路径的班的权值可以为负,实现起来比较简单。
缺点则是时间复杂度较高,为O(|V||E|)。不过算法已经有了一些改进方案,比如队列优化的Bellmanford算法(SPFA算法),一定程度上提高了效率。
算法原理:
贝尔曼-福特算法与迪科斯彻算法类似,都以松弛操作为基础,即估计的最短路径值渐渐地被更加准确的值替代,直至得到最优解。
在两个算法中,计算时每个边之间的估计距离值都比真实值大,并且被新找到路径的最小长度替代。
然而,迪科斯彻算法以贪心法选取未被处理的具有最小权值的节点,然后对其的出边进行松弛操作;而贝尔曼-福特算法简单地对所有边进行松弛操作,共**|V| - 1**次。
在重复地计算中,已计算得到正确的距离的边的数量不断增加,直到所有边都计算得到了正确的路径。
因为算法可以使用负权值的边,因此贝尔曼-福特算法比迪科斯彻算法适用于更多种类的输入。
优化选项:
代码实现:
#include "stdafx.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXN 5050
#define INF 0x3f3f3f3f
using 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;
}
算法描述:
算法步骤:
代码实现:
#include "stdafx.h"
#include
#include
#include
#include
#include
#define MAXN 5050
using 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算法不能直接求解最小费用最大流问题,如果最小费用最大流网络中的权值都非负,则可使用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
#include
#include
#include
#include
#include
#include
#include
#define MAXN 5050
#define INF 0x3f3f3f3f
#define P pair
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 【模板】最小费用最大流 题解