一.最小费用最大流(简称费用流)概念
1.什么是费用流问题
上篇文章我们讲解了最大流问题,那什么是最小费用最大流呢?听名字就可以看出,我们要在满足最大流的同时找到达成最大流的最小费用。对于一个网络流,最大流是一定的,但是组成最大流的费用是可以不同的,这里就有了在最大流网络上产生的费用流网络,就有了最小花费问题。
2.引例(洛谷 3381)
题目描述:
如题,给出一个网络图,以及其源点和汇点,每条边已知其最大流量和单位流量费用,求出其网络最大流和在最大流情况下的最小费用。
输入格式:
第一行包含四个正整数N、M、S、T,分别表示点的个数、有向边的个数、源点序号、汇点序号。
接下来M行每行包含四个正整数ui、vi、wi、fi,表示第i条有向边从ui出发,到达vi,边权为wi(即该边最大流量为wi),单位流量的费用为fi。
输出格式:
一行,包含两个整数,依次为最大流量和在最大流量情况下的最小费用。
4 5 4 3
4 2 30 2
4 3 20 3
2 3 20 1
2 1 30 9
1 3 40 5
50 280
二.算法分析
1.EK算法 + SPFA 最短路
(1)思路分析
<1>只要是网络流的增广路思想,我们都要不断去寻找增广路。寻找增广路时我们可以使用DFS,BFS。但是增广路往往是曲折而漫长的,有时候我们可能走了错误的路径,因为我们的目的是到达汇点,但是中间有些路会使我们距离汇点更远,或者走到了不通的路,从而使得路径要重新寻找,所以我们在求最大流的时候使用了BFS来找增广路,距离汇点越来越近。DFS很容易TLE QAQ
<2>在费用流中,我们仍要首先满足最大流的概念,那就必须仍要不断寻找增广路。但是为了让费用最小,那我们每次寻找的增广路都希望是花费最小的路。那么这里我们就可以使用最短路的思想来寻找增广路,每次优先使用花费最小的路径。因为图中反向建弧时,我们费用也要变成相反的(回流的时候相当于把钱拿回来),这就产生了负边问题,所以使用SPFA来求最短路。
<3>我们用每边单位流量的花费作为边权,假如一条增广路上每条边的花费分别为 c1,c2,.......ck , 那么这条边的最小流量为flow,则此增广路花费为 : c1 * flow + c2*flow + ..... + ck*flow = (c1+ c2 + c3 + .... + ck)*flow = Sum(ci)*flow
这里的Sum(ci)就是我们要求的最短路!
(2)SPFA算法求最短路(会的请略过。。QAQ)
<1>Dijkstra算法为什么无法求负权图
Dijkstra算法使用的是贪心的思想,每次在当前边中,选一条最短边并且标记该边最短距离已经确定。但是当存在负权边时,当前最短的边不一定就是最短距离,因为下一次拓展加上负权边时可能会变得更小,所以在负权图中,当前最短边不一定是到该点的最短距离,与Dijkstra的算法思想矛盾,因此Dijkstra不适用负权图。如下图(来自某博客。。忘记哪个了,博主看到轻戳我)
<2>SPFA求最短路思路
摒弃了Dijkstra算法的选择当前最短边的思路,相反来想,既然存在负权边,那么最好的当然是用负权边去更新其他边,那么被负权边更新的其他边又可以用更小的距离去更新其他其他边,所以说由于负权边的存在,我们可以不断的用被更新的边去更新其他边,直到所有边都不再改变为止。(如果有负权无向边可能就成负权环了。。只能用于有向图??)
<3>SPFA算法代码:
#include
#include
using namespace std;
#define INF 0x3f3f3f3f
const int maxn = 100 +7;
struct Edge{
int to,next,val;
}edge[maxn*100];
int head[maxn],n,m,tot,dist[maxn];
bool vis[maxn];
void addEdge(int a,int b,int c){
edge[tot].to = b;edge[tot].next = head[a];edge[tot].val = c;head[a] = tot++;
}
void SPFA(int s){
memset(vis,0,sizeof(vis));
memset(dist,INF,sizeof(dist));
queue que;
dist[s] = 0;
que.push(s);
while(!que.empty()){
int u = que.front();
que.pop();
vis[u] = false;//取消当前点标记
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].to;
if(dist[v] > dist[u] + edge[i].val){//如果连接点能更新
dist[v] = dist[u] + edge[i].val;//更新
if(!vis[v]){//如果未被标记,则标记入队,可以以更小的距离去更新其他边
vis[v] = true;
que.push(v);
}
}
}
}
}
int main()
{
while(scanf("%d%d",&n,&m)!=EOF&&(n||m)){
tot = 0;
memset(head,-1,sizeof(head));
for(int i = 0;i
(3)费用流 EK最大流 + SPFA最短路:
用SPFA求增广路 + EK算法求最大流
#include
#include
using namespace std;
#define INF 0x3f3f3f3f
const int maxn = 5000 + 7;
struct Edge{
int from,to,next,cap,flow,cost;
}edge[maxn*20];
int head[maxn],pre[maxn],dist[maxn],n,m,tot,s,t;
bool vis[maxn];
void addEdge(int a,int b,int c,int cost){//注意反向弧的负权值
edge[tot].from = a;edge[tot].to = b;edge[tot].next = head[a];edge[tot].cap = c;edge[tot].flow = 0;edge[tot].cost = cost;head[a] = tot++;
edge[tot].from = b;edge[tot].to = a;edge[tot].next = head[b];edge[tot].cap = 0;edge[tot].flow = 0;edge[tot].cost = -cost;head[b] = tot++;
}
bool SPFA(){//SPFA求增广路,dist[t]保存最小花费
memset(dist,INF,sizeof(dist));
memset(vis,0,sizeof(vis));
queue que;
dist[s] = 0;
vis[s] = 1;
que.push(s);
while(!que.empty()){
int u = que.front();
que.pop();
vis[u] = 0;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].to;
if(edge[i].cap > edge[i].flow && dist[v] > dist[u] + edge[i].cost){
dist[v] = dist[u] + edge[i].cost;
pre[v] = i;
if(!vis[v]){
vis[v] = 1;
que.push(v);
}
}
}
}
if(dist[t]!=INF)return true;
return false;
}
int CostFlow(int &flow){//EK算法
int mincost = 0;
while(SPFA()){//能找到增广路
int Min = INF;
for(int i = t;i!=s;i = edge[pre[i]].from){//寻找最小流
Min = min(Min,edge[pre[i]].cap - edge[pre[i]].flow);
}
for(int i = t;i!=s;i = edge[pre[i]].from){//处理所有边
edge[pre[i]].flow+=Min;
edge[pre[i]^1].flow-=Min;
}
flow+=Min;
mincost+=(dist[t]*Min);//累和最小花费
}
return mincost;
}
int main()
{
int T;
scanf("%d",&T);
while(T--){
tot = 0;
memset(head,-1,sizeof(head));
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i = 0;i