在实际应用中,不仅要考虑流量,还要考虑费用。
例如在网络布线工程中有很多中电缆,电缆的粗细不同,流量和费用也不同。如果全部使用较粗的电缆,则造价太高;如果全部使用较细的电缆,则流量满足不了要求。我们希望建立一个费用最小、流量最大的网络,即最小费用最大流。
求解最小费用最大流有两种思路:
(1)先找最小费用路,在该路径上增流,增加到最大流,称为最小费用路算法。
(2)先找最大流,然后找负费用圈,消减费用,减少到最小费用,称为消圈算法。
最小费用路算法,是在残余网络上寻找从源点到汇点的最小费用路,即从源点到汇点的以单位费用为权的最短路,然后沿着最小费用路增流,直到找不到最小费用路为止。
最短增广路算法中求最短增广路是去权值的最短路,而最小费用路是以单位费用为权值的最短路。
(1)创建混合网络(见《趣学算法》7.3.6节)
先初始化为零流,零流对应的混合网络中,正向边的容量为cap,流量为0,费用为cost,反向边容量为0,流量为0,费用为−cost。
(2)找最小费用路
先初始化每个结点的距离为无穷大,然后令源点的距离dist[v1]=0。在混合网络中,从源点出发,沿可行边(E[i].cap>E[i].flow)广度搜索每个邻接点,如果当前距离dist[v]>dist[u]+ E[i].cost,则更新为最短距离:dist[v]=dist[u]+E[i].cost,并记录前驱。
根据前驱数组,找到一条最短费用路,增广路径:1—2—5—6,混合网络如图所示。
(3)沿着增广路径正向增流d,反向减流d
从汇点逆向找最小可增流量d=min(d, E[i].cap−E[i].flow),增流量d=3,产生的费用为mincost+=dist[v6]*d=8×3=24,如图所示。
继续找最小费用路,增流。直到找不到最小费用路为止。
(1)时间复杂度:从算法描述中可以看出,找到一条可增广路的时间是O(E),最多会执行O(VE)次,因为关键边的总数为O(VE),因此总的时间复杂度为O(VE2),其中V为结点个数,E为边的数量。
(2)空间复杂度:使用了一些辅助数组,空间复杂度为O(V)。
#include<iostream>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
const int inf=0x3f3f3f3f;
const int N=100;
const int M=10000;
int cnt;
int head[N],dist[N],pre[N];//dist[i]表示源点到点i最短距离,pre[i]记录前驱
bool vis[N];//标记数组
int maxflow,mincost;//最大流
struct Edge{
int v,next;
int cap,flow,cost;
}E[M<<1];
void init(){//初始化
memset(head,-1,sizeof(head));
cnt=0;
}
void add(int u,int v,int c,int cost){
E[cnt].v=v;
E[cnt].cap=c;
E[cnt].flow=0;
E[cnt].cost=cost;
E[cnt].next=head[u];
head[u]=cnt++;
}
void adde(int u,int v,int c,int cost){
add(u,v,c,cost);
add(v,u,0,-cost);
}
bool SPFA(int s,int t,int n){//求最短费用路
queue<int> q; //队列
memset(vis,false,sizeof(vis));//访问标记初始化
memset(pre,-1,sizeof(pre)); //前驱初始化
memset(dist,0x3f,sizeof(dist));
vis[s]=true; //标记入队
dist[s]=0;
q.push(s);
while(!q.empty()){
int u=q.front();
q.pop();
vis[u]=false;//队头元素出队,并且消除标记
for(int i=head[u];~i;i=E[i].next){//访问u的所有邻接点
int v=E[i].v;
if(E[i].cap>E[i].flow&&dist[v]>dist[u]+E[i].cost){//松弛操作
dist[v]=dist[u]+E[i].cost;
pre[v]=i; //记录前驱边
if(!vis[v]){ //顶点v不在队内
q.push(v); //入队
vis[v]=true; //标记入队
}
}
}
}
return pre[t]!=-1;
}
int MCMF(int s,int t,int n){ //minCostmaxFlow
maxflow=mincost=0;//maxflow为最大流量,mincost为最小费用
while(SPFA(s,t,n)){//表示找到了从s到t的最短路(可增广路)
int d=inf;
for(int i=pre[t];~i;i=pre[E[i^1].v])//找最小可增流量
d=min(d,E[i].cap-E[i].flow);
for(int i=pre[t];~i;i=pre[E[i^1].v]){//最小费用路逆向操作
E[i].flow+=d;//同向边增流
E[i^1].flow-=d;//反向边减流
}
maxflow+=d; //更新最大流
mincost+=dist[t]*d; //dist[t]为该路径上单位流量费用之和,更新最小费用
}
return mincost;
}
int main(){
int n,m,u,v,w,c;
cout<<"请输入结点个数n和边数m:"<<endl;
cin>>n>>m;
init();//初始化
cout<<"请输入两个结点u,v,边(u--v)的容量w,单位容量费用c:"<<endl;
for(int i=1;i<=m;i++){
cin>>u>>v>>w>>c;
adde(u,v,w,c);
}
cout<<"网络的最小费用:"<<MCMF(1,n,n)<<endl;
cout<<"网络的最大流值:"<<maxflow<<endl;
return 0;
}
/*测试数据
6 10
1 3 4 7
1 2 3 1
2 5 4 5
2 4 6 4
2 3 1 1
3 5 3 6
3 4 5 3
4 6 7 6
5 6 3 2
5 4 3 3
*/
#include<iostream>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
const int inf=0x3f3f3f3f;
const int N=100;
const int M=10000;
int cnt;
int head[N],dist[N],pre[N];//dist[i]表示源点到点i最短距离,pre[i]记录前驱
bool vis[N];//标记数组
int maxflow,mincost;//最大流
struct Edge{
int v,next;
int cap,flow,cost;
}E[M<<1];
void init(){//初始化
memset(head,-1,sizeof(head));
cnt=0;
}
void add(int u,int v,int c,int cost){
E[cnt].v=v;
E[cnt].cap=c;
E[cnt].flow=0;
E[cnt].cost=cost;
E[cnt].next=head[u];
head[u]=cnt++;
}
void adde(int u,int v,int c,int cost){
add(u,v,c,cost);
add(v,u,0,-cost);
}
bool SPFA(int s,int t,int n){//求最短费用路的SPFA
queue<int> q; //队列,STL实现
memset(vis,false,sizeof(vis));//访问标记初始化
memset(pre,-1,sizeof(pre)); //前驱初始化
memset(dist,0x3f,sizeof(dist));
vis[s]=true; //顶点入队vis要做标记
dist[s]=0;
q.push(s);
while(!q.empty()){
int u=q.front();
q.pop();
vis[u]=false;
//队头元素出队,并且消除标记
for(int i=head[u];~i;i=E[i].next){//遍历顶点u的邻接表
int v=E[i].v;
if(E[i].cap>E[i].flow&&dist[v]>dist[u]+E[i].cost){//松弛操作
dist[v]=dist[u]+E[i].cost;
pre[v]=i; //记录前驱
if(!vis[v]){ //顶点v不在队内
q.push(v); //入队
vis[v]=true; //标记
}
}
}
}
cout<<"最短路数组"<<endl;
cout<<"dist[ ]=";
for(int i=1;i<=n;i++)
cout<<" "<<dist[i];
cout<<endl;
return pre[t]!=-1;
}
int MCMF(int s,int t,int n){ //minCostMaxFlow
maxflow=mincost=0;//maxflow 当前最大流量,mincost当前最小费用
while(SPFA(s,t,n)){//表示找到了从s到t的最短路
int d=inf;
cout<<endl;
cout<<"增广路径:"<<t;
for(int i=pre[t];~i;i=pre[E[i^1].v]){
d=min(d,E[i].cap-E[i].flow); //找最小可增流量
cout<<"--"<<E[i^1].v;
}
cout<<"增流:"<<d<<endl;
cout<<endl;
maxflow+=d; //更新最大流
for(int i=pre[t];~i;i=pre[E[i^1].v]){//沿最小费用路增减流
E[i].flow+=d;
E[i^1].flow-=d;
}
mincost+=dist[t]*d; //dist[t]为该路径上单位流量费用之和 ,最小费用更新
}
return mincost;
}
void printg(int n){//输出网络邻接表
cout<<"----------网络邻接表如下:----------"<<endl;
for(int i=1;i<=n;i++){
cout<<"v"<<i<<" ["<<head[i];
for(int j=head[i];~j;j=E[j].next)
cout<<"]--["<<E[j].v<<" "<<E[j].cap<<" "<<E[j].flow<<" "<<E[j].cost<<" "<<E[j].next;
cout<<"]"<<endl;
}
cout<<endl;
}
void printflow(int n){//输出实流边
cout<<"----------实流边如下:----------"<<endl;
for(int i=1;i<=n;i++)
for(int j=head[i];~j;j=E[j].next)
if(E[j].flow>0){
cout<<"v"<<i<<"--"<<"v"<<E[j].v<<" "<<E[j].flow<<" "<<E[j].cost;
cout<<endl;
}
}
/*
6 10
1 3 4 7
1 2 3 1
2 5 4 5
2 4 6 4
2 3 1 1
3 5 3 6
3 4 5 3
4 6 7 6
5 6 3 2
5 4 3 3
*/
int main(){
int n,m;
int u,v,w,c;
cout<<"请输入结点个数n和边数m:"<<endl;
cin>>n>>m;
init();//初始化
cout<<"请输入两个结点u,v,边(u--v)的容量w,单位容量费用c:"<<endl;
for(int i=1;i<=m;i++){
cin>>u>>v>>w>>c;
adde(u,v,w,c);
}
cout<<endl;
printg(n);//输出初始网络邻接表
cout<<"网络的最小费用:"<<MCMF(1,n,n)<<endl;
cout<<"网络的最大流值:"<<maxflow<<endl;
cout<<endl;
printg(n);//输出最终网络
printflow(n);//输出实流边
return 0;
}