最小费用最大流

算法思想

在最小费用最大流中,网络中需要考虑的不仅是流量了,还涉及到单位流量流过的费用,网络流的费用成为了每条边的流量×单位流量的费用,即 C o s t ( f l o w ) = ∑ < x , y > ∈ E c o s t ( x , y ) × f l o w ( x , y ) Cost(flow)=\sum_{<x,y>\in E}cost(x,y)×flow(x,y) Cost(flow)=<x,y>Ecost(x,y)×flow(x,y),求解最小费用最大流有两个思路

  1. 先找最小单位费用路,之后在该路径上增流并增加到最大流,最小费用路算法
  2. 先找最大流,然后找负费用圈,消减费用,减少到最小费用,消圈算法

最小费用路算法

最小费用路算法是寻找源点到汇点的最小费用路,也就是以费用为边权,找最短路,然后对找到的最短路进行增流,以此反复,需要注意的是,在求解最大流时,最短增广路算法中的最短增广路是去权值的最短路,而最小费用路是以单位费用为权值的最短路,一般分成求出最小费用路和沿最小费用路增流,可以选用SPFA+EK=MCMF的方式,这种方式便于处理负权边,也可以采用Dijkstra+Dinic/ISAP,但一般由于负权边的关系,采用MCMF较多,下面给出模板

代码(MCMF)

bool SPFA(int s,int t) {
    queue<int>q;
    memset(vis,0,sizeof(vis));
    memset(pre,-1,sizeof(pre));
    memset(dis,0x3f,sizeof(dis));
    dis[s]=0;
    vis[s]=1;
    q.push(s);
    while(!q.empty()) {
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(int i=head[u]; ~i; i=e[i].next) {
            int v=e[i].to;
            if(e[i].cap>e[i].flow&&dis[v]>dis[u]+e[i].pay) {
                dis[v]=dis[u]+e[i].pay;
                pre[v]=i;
                if(vis[v])continue;
                vis[v]=1;
                q.push(v);
            }
        }
    }
    return pre[t]!=-1;
}
int MCMF(int s,int t) {
    int mincost=0;
    while(SPFA(s,t)) {
        int d=inf,v=t;
        while(v!=s) {
            int i=pre[v];
            d=min(d,e[i].cap-e[i].flow);//可增加的流量
            v=e[i^1].to;//换点
        }
        v=t;
        while(v!=s) {
            int i=pre[v];
            e[i].flow+=d;
            e[i^1].flow-=d;//实流
            v=e[i^1].to;
        }
        mincost+=dis[t]*d;
    }
    return mincost;
}

消圈算法

MCMF方便求最优解,但是如果求的不是最优解,或者只是求出比当前方案更好的更优解,消圈算法就更好一些,对一个网络来说,如果已经求得最小费用路,那么其对应的缓和网络中是没有负费用圈的,如果有负费用圈,那么一定可以沿着这个圈继续增广,得到相同费用下更小的流,即最小费用流等价残余网络中无负费用圈
可以采用SPFA先判断是否有费用圈,之后再通过回溯找到负费用圈,然后增流

代码

int negative_loop(int s,int t) {
    int f,r,top=-1;
    f=0;
    r=-1;
    memset(num,0,sizeof(num));
    memset(dist,0,sizeof(dist));
    memset(vis,true,sizeof(vis));//inq是bool类型的数组,每个元素占1字节,所以可以这样
    memset(pre,-1,sizeof(pre));
    for(int i=s; i<=t; i++)
        q[++r]=i;
    while(f<=r) {
        int u=q[r--];//栈式写法,r--改成f++就是队列了,队列要将数组扩大。
        vis[u]=false;//实际测试中,用栈比队列判断负圈效率更高
        for(int i=head[u]; ~i; i=E[i].next) {
            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[++r]=v;
                    vis[v]=true; //标记
                    if(++num[v]>t+1)
                        return v;
                }
            }
        }
    }
    return -1;
}
 for(int i=pre[k]; !vis[E[i].v]; i=pre[E[i^1].v]) { //往前找负环
            vis[E[i].v]=true;
            k=E[i].v;
        }
        for(int i=pre[k];; i=pre[E[i^1].v]) { //在负环中增加1个流量
            E[i].flow++;
            E[i^1].flow--;
            if(E[i^1].v==k) break;
        }

训练

POJ2135

题目大意: N N N个节点的无向图,无环,无重边,编号 1 1 1 ~ N N N,起点为1,中转站为N,有M条边,有边权,现在要从起点到中转站再到起点,求保证不会经过重复边的前提下的所走的最小长度

思路:设置边的容量为1,由于每条边只能走一次,但是需要从1到N再回来,可以看做存在从1到N有两条不同的路径,也就是流量为2,将边权看做费用,那么问题就转换为求最小费用最大流,添加源点和汇点,对每一条边,由于两边都可以走,没有指向,所以要加双向边,加残余网络边时费用为负值,因此不适合用Dijkstra+Dinic/ISAP

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
const int inf=0x3f3f3f3f;
const int maxn=1e3+10;
int head[maxn],cnt,dis[maxn],n,m,pre[maxn];
bool vis[maxn];
struct node {
    int to,next,cap,flow,pay;
} e[maxn*80];
void Add(int from,int to,int cap,int flow,int pay) {
    e[cnt].to=to;
    e[cnt].next=head[from];
    e[cnt].cap=cap;
    e[cnt].flow=flow;
    e[cnt].pay=pay;
    head[from]=cnt++;
}
bool SPFA(int s,int t) {
    queue<int>q;
    memset(vis,0,sizeof(vis));
    memset(pre,-1,sizeof(pre));
    memset(dis,0x3f,sizeof(dis));
    dis[s]=0;
    vis[s]=1;
    q.push(s);
    while(!q.empty()) {
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(int i=head[u]; ~i; i=e[i].next) {
            int v=e[i].to;
            if(e[i].cap>e[i].flow&&dis[v]>dis[u]+e[i].pay) {
                dis[v]=dis[u]+e[i].pay;
                pre[v]=i;
                if(vis[v])continue;
                vis[v]=1;
                q.push(v);
            }
        }
    }
    return pre[t]!=-1;
}
int MCMF(int s,int t) {
    int mincost=0;
    while(SPFA(s,t)) {
        int d=inf,v=t;
        while(v!=s) {
            int i=pre[v];
            d=min(d,e[i].cap-e[i].flow);//可增加的流量
            v=e[i^1].to;//换点
        }
        v=t;
        while(v!=s) {
            int i=pre[v];
            e[i].flow+=d;
            e[i^1].flow-=d;//实流
            v=e[i^1].to;
        }
        mincost+=dis[t]*d;
    }
    return mincost;
}

using namespace std;
int main() {
    scanf("%d%d",&n,&m);
    int s=0,t=n+1;
    memset(head,-1,sizeof(head));
    while(m--) {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        Add(u,v,1,0,w);//建双向边,两边都可以走
        Add(v,u,0,0,-w);
        Add(v,u,1,0,w);
        Add(u,v,0,0,-w);
    }
    Add(s,1,2,0,0);//连接源点
    Add(1,s,0,0,0);
    Add(n,n+1,2,0,0);//连接汇点
    Add(n+1,n,0,0,0);
    cout <<MCMF(s,t);
    return 0;
}

LuoguP2770

题目大意:略

思路:这一道题和POJ2135有些类似,但题目中有很关键的话:从西向东从东到西,也就是说,在走的过程中,除了有所有城市只能访问一次(除起点),还有一个约束,从起点到中转的路径必须是递增,从中转到起点的路径,必须是递减,这一点就约束了不能建图时无脑建双边,将点拆成边,源点和汇点的边容量为2,假定边的方向是编号低向编号高,由于需要有来有回,问题被转换成源点到汇点的最小费用最大流问题,需要满足最大流为2,求一个来回的问题被转换成求两个路径
在建图时,如果给定边正好是源点和汇点,该边的边容量为2,为了获得最多城市,只需要将费用1取反,求最短路,则必然负数最多,对应城市最多
输出答案,此时网络中应该存在两条路径,从源点出发,沿着增流且花费为负的方向走,并标记已经经过的点,到终点后,代表已经有一条路径找到了,之后继续向起点走,沿着减流且花费为正的方向走,因为减流且花费为负代表其正向边增流且花费为负,正好对应另一条路径

代码

#include <bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
const int maxn=2e4+10;
int n,m,head[maxn],cnt,dis[maxn],pre[maxn],s,t,mincost;
bool vis[maxn];
unordered_map<string,int>Umap;
string Dic[maxn];
struct node {
    int next,to,cap,flow,pay;
} e[maxn];
void Add(int from,int to,int cap,int flow,int pay) {
    e[cnt].cap=cap;
    e[cnt].flow=flow;
    e[cnt].to=to;
    e[cnt].pay=pay;
    e[cnt].next=head[from];
    head[from]=cnt++;
}
bool SPFA(int s,int t) {
    queue<int>q;
    memset(vis,0,sizeof(vis));
    memset(pre,-1,sizeof(pre));
    memset(dis,0x3f,sizeof(dis));
    dis[s]=0;
    vis[s]=1;
    q.push(s);
    while(!q.empty()) {
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(int i=head[u]; ~i; i=e[i].next) {
            int v=e[i].to;
            if(e[i].cap>e[i].flow&&dis[v]>dis[u]+e[i].pay) {
                dis[v]=dis[u]+e[i].pay;
                pre[v]=i;
                if(vis[v])continue;
                vis[v]=1;
                q.push(v);
            }
        }
    }
    return pre[t]!=-1;
}
int MCMF(int s,int t) {
    int maxflow=0;
    while(SPFA(s,t)) {
        int d=inf,v=t;
        while(v!=s) {
            int i=pre[v];
            d=min(d,e[i].cap-e[i].flow);//可增加的流量
            v=e[i^1].to;//换点
        }
        v=t;
        maxflow+=d;
        while(v!=s) {
            int i=pre[v];
            e[i].flow+=d;
            e[i^1].flow-=d;//实流
            v=e[i^1].to;
        }
        mincost+=dis[t]*d;
    }
    return maxflow;
}
void DFS(int u) {
    vis[u]=1;
    for(int i=head[u]; ~i; i=e[i].next) {
        int v=e[i].to;
        if(vis[v])continue;
        if((e[i].flow>0&&e[i].pay<=0)||(e[i].flow<0&&e[i].pay>=0)) {
            DFS(v);//先搜后输出,输出的顺序和搜索是相反的,但是根据题意也符合
            if(v<=n)cout <<Dic[v]<<endl;
        }
    }
}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >>n>>m;
    s=1,t=2*n;
    memset(head,-1,sizeof(head));
    for(int i=1; i<=n; i++)//拆点
        if(i==1||i==n) {//如果是源点或者汇点,边容量为2
            Add(i,i+n,2,0,0);
            Add(i+n,i,0,0,0);
        } else {
            Add(i,i+n,1,0,0);
            Add(i+n,i,0,0,0);
        }
    for(int i=1; i<=n; i++) {
        string str;
        cin >>str;
        Umap[str]=i;//哈希
        Dic[i]=str;//索引
    }
    while(m--) {
        string x,y;
        cin >>x>>y;
        int xx=Umap[x],yy=Umap[y];
        if(xx<yy) {
            if(xx==1&&yy==n)//如果正好是源点和汇点,边容量就为2
                Add(xx+n,yy,2,0,-1);//求点最多,费用为负,便于求最短路
            else
                Add(xx+n,yy,1,0,-1);
            Add(yy,xx+n,0,0,1);
        } else {
            if(yy==1&&xx==n) Add(yy+n,xx,2,0,-1);
            else Add(yy+n,xx,1,0,-1);
            Add(xx,yy+n,0,0,1);
        }
    }
    if(MCMF(s,t)==2) {
        cout <<-mincost<<endl;
        cout <<Dic[1]<<endl;
        memset(vis,0,sizeof(vis));
        DFS(1);
        cout <<Dic[1]<<endl;
    } else
        cout <<"No Solution!"<<endl;
    return 0;
}

POJ3680

题目大意:给出n个加权开区间,第 i i i个区间覆盖 ( a i , b i ) (a_i,b_i) (ai,bi),权值为 w i w_i wi,选择一些区间使得整个数轴没有点的覆盖数超过 k k k的情况下,使总权值最大

思路:总权值最大,那么费用用负值,对于所有的区间短点,需要先离散化,然后设置源点汇点,对于每个区间,左端点到右端点连一条边,容量为1,代表只能选一次,相邻的端点间 i i i i + 1 i+1 i+1连接一条边,容量为k,求最小费用最大流即可

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
#include <map>
using namespace std;
const int inf=0x3f3f3f3f;
const int maxn=4e5+10;
int n,k,T,s,t,cnt,head[405],l[205],r[205],w[205],ans,num[405],dis[405],pre[405];
bool vis[405];
struct node {
    int next,to,cap,flow,pay;
} e[maxn];
void Add(int from,int to,int cap,int flow,int pay) {
    e[cnt].cap=cap;
    e[cnt].to=to;
    e[cnt].flow=flow;
    e[cnt].next=head[from];
    e[cnt].pay=pay;
    head[from]=cnt++;
}
bool SPFA(int s,int t) {
    queue<int>q;
    memset(vis,0,sizeof(vis));
    memset(pre,-1,sizeof(pre));
    memset(dis,0x3f,sizeof(dis));
    dis[s]=0;
    vis[s]=1;
    q.push(s);
    while(!q.empty()) {
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(int i=head[u]; ~i; i=e[i].next) {
            int v=e[i].to;
            if(e[i].cap>e[i].flow&&dis[v]>dis[u]+e[i].pay) {
                dis[v]=dis[u]+e[i].pay;
                pre[v]=i;
                if(vis[v])continue;
                vis[v]=1;
                q.push(v);
            }
        }
    }
    return pre[t]!=-1;
}
int MCMF(int s,int t) {
    int mincost=0;
    while(SPFA(s,t)) {
        int d=inf,v=t;
        while(v!=s) {
            int i=pre[v];
            d=min(d,e[i].cap-e[i].flow);//可增加的流量
            v=e[i^1].to;//换点
        }
        v=t;
        while(v!=s) {
            int i=pre[v];
            e[i].flow+=d;
            e[i^1].flow-=d;//实流
            v=e[i^1].to;
        }
        mincost+=dis[t]*d;
    }
    return mincost;
}
int main() {
    scanf("%d",&T);
    while(T--) {
        scanf("%d%d",&n,&k);
        cnt=ans=0;
        memset(head,-1,sizeof(head));
        for(int i=1; i<=n; i++) {//记录区间,并存储端点用于离散化
            scanf("%d%d%d",l+i,r+i,w+i);
            num[++ans]=l[i],num[++ans]=r[i];
        }
        sort(num+1,num+ans+1);//排序
        ans=unique(num+1,num+ans+1)-num-1;//去重
        s=0,t=ans+1;
        for(int i=0; i<=ans; i++) {//设置容量,代表点最多经过k次
            Add(i,i+1,k,0,0);
            Add(i+1,i,0,0,0);
        }
        for(int i=1; i<=n; i++) {
            int u=lower_bound(num+1,num+ans+1,l[i])-num;//获得离散化坐标
            int v=lower_bound(num+1,num+ans+1,r[i])-num;
            Add(u,v,1,0,-w[i]);
            Add(v,u,0,0,w[i]);
        }
        cout <<-MCMF(s,t)<<endl;
    }
    return 0;
}

POJ2175

题目大意:略

思路:如果一开始盲目的求最优解的话一定会超时,可以计算一下复杂度, M C M F MCMF MCMF的复杂度为 O ( V E 2 ) O(VE^2) O(VE2),而在本题目中,复杂度的表达式为 O ( ( N + 2 ) × ( N × M + N + M ) 2 ) ≈ O ( N 5 ) O((N+2)×(N×M+N+M)^2)≈O(N^5) O((N+2)×(N×M+N+M)2)O(N5),带入100,显然时间过大,那么,我们需要在给出解的基础上判断是否存在更优的解即可,那么先构造出原题给出的方案,如果直接跑MCMF是不行的,因为存在负环,SPFA会死循环,这个时候就需要消圈算法,如果给出的解是最优解,那么构造出的混合网络就没有负费用圈,如果有,则沿着负费用圈增广,就可以得到费用更小的流,用SPFA判断是否存在负环,然后找到通过次数过多的点,但是该点未补在负环中,负环在这个点之前,找到负环即可

代码

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<queue>
#include<algorithm>
using namespace std;
const int inf=0x3f3f3f3f;
const int N=205;
const int M=40410;
int cnt;
int head[N],dist[N],pre[N];//dist[i]表示源点到点i最短距离,pre[i]记录前驱
bool vis[N];//标记数组
int G[N][N],q[N],num[N],shelter[N];
struct node {
    int x,y,p;
} a[N],b[N];

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,int flow) {
    E[cnt].v=v;
    E[cnt].cap=c;
    E[cnt].flow=flow;
    E[cnt].cost=cost;
    E[cnt].next=head[u];
    head[u]=cnt++;
}

void adde(int u,int v,int c,int cost,int flow) {
    add(u,v,c,cost,flow);
    add(v,u,0,-cost,-flow);
}

int negative_loop(int s,int t) {
    int f,r,top=-1;
    f=0;
    r=-1;
    memset(num,0,sizeof(num));
    memset(dist,0,sizeof(dist));
    memset(vis,true,sizeof(vis));//inq是bool类型的数组,每个元素占1字节,所以可以这样
    memset(pre,-1,sizeof(pre));
    for(int i=s; i<=t; i++)
        q[++r]=i;
    while(f<=r) {
        int u=q[r--];//栈式写法,r--改成f++就是队列了,队列要将数组扩大。
        vis[u]=false;//实际测试中,用栈比队列判断负圈效率更高
        for(int i=head[u]; ~i; i=E[i].next) {
            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[++r]=v;
                    vis[v]=true; //标记
                    if(++num[v]>t+1)
                        return v;
                }
            }
        }
    }
    return -1;
}

int dis(node p1,node p2) { //曼哈顿距离+1
    return abs(p1.x-p2.x)+abs(p1.y-p2.y)+1;
}

int main() {
    int n,m,s,t,p;
    scanf("%d%d",&n,&m);
    s=0,t=n+m+1;
    init();
    for(int i=0; i<n; i++) { //读入大楼位置人数
        scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].p);
        adde(s,i+1,a[i].p,0,a[i].p);//源点到大楼,容量a[i].p,费用0,流量a[i].p
    }
    for(int j=0; j<m; j++) //读入防空洞位置人数
        scanf("%d%d%d",&b[j].x,&b[j].y,&b[j].p);
    memset(shelter,0,sizeof(shelter));
    for(int i=0; i<n; i++)
        for(int j=0; j<m; j++) {
            scanf("%d",&p);
            shelter[j]+=p;
            adde(i+1,n+j+1,inf,dis(a[i],b[j]),p);//大楼到防空洞,容量inf,费用两者曼哈顿距离+1,流量p
        }
    for(int j=0; j<m; j++)
        adde(n+j+1,t,b[j].p,0,shelter[j]);//防空洞到汇点,容量b[i].p,费用0,流量shelter[i]
    int k=negative_loop(s,t);
    if(k!=-1) {
        printf("SUBOPTIMAL\n");
        memset(vis,0,sizeof(vis));
        memset(G,0,sizeof(G));
        for(int i=pre[k]; !vis[E[i].v]; i=pre[E[i^1].v]) { //往前找负环
            vis[E[i].v]=true;
            k=E[i].v;
        }//因为是环,会找到一个点被访问两次
        for(int i=pre[k];; i=pre[E[i^1].v]) { //在负环中增加1个流量
            E[i].flow++;
            E[i^1].flow--;
            if(E[i^1].v==k) break;
        }
        for(int i=0; i<cnt; i++) 
            if(E[i^1].v>0&&E[i^1].v<=n&&E[i].v>n&&E[i].v<=n+m)
                G[E[i^1].v-1][E[i].v-n-1]=E[i].flow;
        for(int i=0; i<n; i++) { //输出结果
            for(int j=0; j<m; j++) {
                if(j) printf(" ");
                printf("%d",G[i][j]);
            }
            printf("\n");
        }
    } else
        printf("OPTIMAL\n");
    return 0;
}

代码(求最优解)

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
#include <map>
using namespace std;
const int inf=0x3f3f3f3f;
const int maxn=2e4+10;
int n,m,s,t,cnt,head[121],dis[121],pre[121],ans,graph[121][121];
bool vis[121];
struct build {
    int x,y,c;
} b[212];
int Dist(build a,build b) {
    return abs(a.x-b.x)+abs(a.y-b.y)+1;
}
struct node {
    int next,to,cap,flow,pay;
} e[maxn];
void Add(int from,int to,int cap,int flow,int pay) {
    e[cnt].cap=cap;
    e[cnt].to=to;
    e[cnt].flow=flow;
    e[cnt].next=head[from];
    e[cnt].pay=pay;
    head[from]=cnt++;
}
bool SPFA(int s,int t) {
    queue<int>q;
    memset(vis,0,sizeof(vis));
    memset(pre,-1,sizeof(pre));
    memset(dis,0x3f,sizeof(dis));
    dis[s]=0;
    vis[s]=1;
    q.push(s);
    while(!q.empty()) {
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(int i=head[u]; ~i; i=e[i].next) {
            int v=e[i].to;
            if(e[i].cap>e[i].flow&&dis[v]>dis[u]+e[i].pay) {
                dis[v]=dis[u]+e[i].pay;
                pre[v]=i;
                if(vis[v])continue;
                vis[v]=1;
                q.push(v);
            }
        }
    }
    return pre[t]!=-1;
}
int MCMF(int s,int t) {
    int mincost=0;
    while(SPFA(s,t)) {
        int d=inf,v=t;
        while(v!=s) {
            int i=pre[v];
            d=min(d,e[i].cap-e[i].flow);//可增加的流量
            v=e[i^1].to;//换点
        }
        v=t;
        while(v!=s) {
            int i=pre[v];
            e[i].flow+=d;
            e[i^1].flow-=d;//实流
            v=e[i^1].to;
        }
        mincost+=dis[t]*d;
    }
    return mincost;
}
int main() {
    int cost=0;
    scanf("%d%d",&n,&m);
    s=0,t=n+m+1;
    memset(head,-1,sizeof(head));
    for(int i=1; i<=n; i++)
        scanf("%d%d%d",&b[i].x,&b[i].y,&b[i].c);
    for(int i=n+1; i<=n+m; i++)
        scanf("%d%d%d",&b[i].x,&b[i].y,&b[i].c);
    for(int i=1; i<=n; i++)
        for(int j=n+1; j<=n+m; j++) {
            int num=0;
            scanf("%d",&num);
            cost+=Dist(b[i],b[j])*num;
        }
    for(int i=1; i<=n; i++)
        for(int j=n+1; j<=n+m; j++) {
            Add(i,j,inf,0,Dist(b[i],b[j]));
            Add(j,i,0,0,-Dist(b[i],b[j]));
        }
    for(int i=1; i<=n; i++) {
        Add(s,i,b[i].c,0,0);
        Add(i,s,0,0,0);
    }
    for(int i=n+1; i<=n+m; i++) {
        Add(i,t,b[i].c,0,0);
        Add(t,i,0,0,0);
    }
    if(MCMF(s,t)==cost)
        printf("OPTIMAL\n");
    else {
        printf("SUBOPTIMAL\n");
        for(int u=1; u<=n; u++)
            for(int i=head[u]; ~i; i=e[i].next) {
                int v=e[i].to;
                if(e[i].flow>=0&&v!=s)
                    graph[u][v-n]=e[i].flow;
            }
        for(int i=1; i<=n; i++) {
            for(int j=1; j<=m; j++)
                printf("%d%c",graph[i][j],j==m?'\0':' ');
            putchar('\n');
        }
    }
    return 0;
}

UVA11613

题目大意:略

思路:首先,本题需要注意到一点,每个月有最大生产量,但是,并不是一定要生产这么多,如果计算出来会亏本,那不如不生产,其次,本题需要拆点,一个作为生产专用,一个作为销售专用,源点与对应点相连,容量为产量,费用为成本,对应拆点与汇点连接,容量为销售量,费用为售价负值,最后,由于存在保质期,那么每个点与对应的保质期内的销售节点相连,容量为产量,费用为储存值×月数

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=4e4+5;
const int inf=0x3f3f3f3f;
const int N=405;
int T,m,load,s,t,pre[N],head[N],cnt;
int price[N],month[N],dis[N],mxp[N];
bool vis[N];
struct node {
    int to,next,cap,flow,pay;
} e[maxn];
void Add(int from,int to,int cap,int flow,int pay) {
    e[cnt].to=to;
    e[cnt].cap=cap;
    e[cnt].flow=flow;
    e[cnt].pay=pay;
    e[cnt].next=head[from];
    head[from]=cnt++;
}
bool SPFA(int s,int t) {
    queue<int>q;
    memset(vis,0,sizeof(vis));
    memset(pre,-1,sizeof(pre));
    memset(dis,0x3f,sizeof(dis));
    dis[s]=0;
    vis[s]=1;
    q.push(s);
    while(!q.empty()) {
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(int i=head[u]; ~i; i=e[i].next) {
            int v=e[i].to;
            if(e[i].cap>e[i].flow&&dis[v]>dis[u]+e[i].pay) {
                dis[v]=dis[u]+e[i].pay;
                pre[v]=i;
                if(vis[v])continue;
                vis[v]=1;
                q.push(v);
            }
        }
    }
    if(dis[t]>0)return 0;//这一句很重要,如果费用大于0,代表已经亏本了
    return pre[t]!=-1;
}
int MCMF(int s,int t) {
    int mincost=0;
    while(SPFA(s,t)) {
        int d=inf,v=t;
        while(v!=s) {
            int i=pre[v];
            d=min(d,e[i].cap-e[i].flow);//可增加的流量
            v=e[i^1].to;//换点
        }
        v=t;
        while(v!=s) {
            int i=pre[v];
            e[i].flow+=d;
            e[i^1].flow-=d;//实流
            v=e[i^1].to;
        }
        mincost+=dis[t]*d;
    }
    return mincost;
}
signed main() {
    scanf("%lld",&T);
    for(int k=1; k<=T; k++) {
        memset(head,-1,sizeof(head));
        scanf("%lld%lld",&m,&load);
        s=0,t=2*m+1;
        cnt=0;
        for(int i=1; i<=m; i++) {
            int local,mxs;
            scanf("%lld%lld%lld%lld%lld",&local,&mxp[i],&price[i],&mxs,&month[i]);
            Add(s,i,mxp[i],0,local);//源点到i,容量为产量,费用为成本
            Add(i,s,0,0,-local);
            Add(i+m,t,mxs,0,-price[i]);//拆点到汇点,容量为销售量,费用为负售价(求最小)
            Add(t,i+m,0,0,price[i]);
        }
        for(int i=1; i<=m; i++)
            for(int j=0; j<=month[i]; j++) {//保质期前销售
                Add(i,i+m+j,mxp[i],0,j*load);
                Add(i+m+j,i,0,0,-j*load);
            }
        printf("Case %lld: %lld\n",k,-MCMF(s,t));
    }
    return 0;
}

LuoguP4553

题目大意:略

思路:可以用上下界网络流,但还是最小费用最大流更简单一些
拆每个点,但是拆点间不相连,添加源点到拆点入点的容量v费用0的边,出点到汇点连容量v费用0的边
由于没有给拆点间连接,那么出点的流量只能来自别的入点,对于给出的每条航线,将入点与对应出点连接,容量无穷费用为p
最后额外建一个点,源点与其相连,容量m费用0,然后这个点与所有出点连接,容量无穷费用0,代表m个人可以随意选出发点

代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e4+5;
const int N=1212;
const int inf=0x3f3f3f3f;
int s,t,sx,cnt,head[N],n,m,dis[N],pre[N];
bool vis[N];
struct node {
    int next,to,cap,flow,pay;
} e[maxn<<1];
void Add(int from,int to,int cap,int flow,int pay) {
    e[cnt].to=to;
    e[cnt].cap=cap;
    e[cnt].flow=flow;
    e[cnt].pay=pay;
    e[cnt].next=head[from];
    head[from]=cnt++;
}
bool SPFA(int s,int t) {
    queue<int>q;
    memset(vis,0,sizeof(vis));
    memset(pre,-1,sizeof(pre));
    memset(dis,0x3f,sizeof(dis));
    dis[s]=0;
    vis[s]=1;
    q.push(s);
    while(!q.empty()) {
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(int i=head[u]; ~i; i=e[i].next) {
            int v=e[i].to;
            if(e[i].cap>e[i].flow&&dis[v]>dis[u]+e[i].pay) {
                dis[v]=dis[u]+e[i].pay;
                pre[v]=i;
                if(vis[v])continue;
                vis[v]=1;
                q.push(v);
            }
        }
    }
    return pre[t]!=-1;
}
int MCMF(int s,int t) {
    int mincost=0;
    while(SPFA(s,t)) {
        int d=inf,v=t;
        while(v!=s) {
            int i=pre[v];
            d=min(d,e[i].cap-e[i].flow);//可增加的流量
            v=e[i^1].to;//换点
        }
        v=t;
        while(v!=s) {
            int i=pre[v];
            e[i].flow+=d;
            e[i^1].flow-=d;//实流
            v=e[i^1].to;
        }
        mincost+=dis[t]*d;
    }
    return mincost;
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >>n>>m;
    s=0,t=2*n+1,sx=t+1,
    memset(head,-1,sizeof(head));
    Add(s,sx,m,0,0),Add(sx,s,0,0,0);
    for(int i=1; i<=n; i++) {
        int v;
        cin >>v;
        Add(i+n,t,v,0,0),Add(t,i+n,0,0,0);
        Add(s,i,v,0,0),Add(i,s,0,0,0);
    }
    for(int i=1; i<=n; i++)
        for(int j=i+1; j<=n; j++) {
            int p;
            cin >>p;
            if(p==-1)continue;
            Add(i,j+n,inf,0,p),Add(j+n,i,0,0,-p);
        }
    for(int i=n+1;i<=2*n;i++)
        Add(sx,i,inf,0,0),Add(i,sx,0,0,0);
    cout <<MCMF(s,t);
    return 0;
}

LuoguP3980

题目大意:略

思路:如果按照常规思路,一个人为一点流量,必然建不出图,问题的本质在于不同日子可能使用同一个人,而代表同一人的流量是1,那么现在就要解决使得让一个人的流量能够覆盖所有能使用他的位置,那么对于每一个志愿者,如果他的工作时间为 s , t s,t s,t,费用为 c c c,就建一条 s − > t + 1 s->t+1 s>t+1的边,费用为c,容量无穷,代表这个人的流量流完了这个区域,但是仅有这个信息是不够的,还有每天的人数限制,在i到i+1建边,设流量容量为无穷大-人数限制,最后的最大流就一定是inf

代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5;
const int N=1212;
const int inf=0x3f3f3f3f;
int s,t,sx,cnt,head[N],n,m,dis[N],pre[N];
bool vis[N];
struct node {
    int next,to,cap,flow,pay;
} e[maxn<<1];
void Add(int from,int to,int cap,int flow,int pay) {
    e[cnt].to=to;
    e[cnt].cap=cap;
    e[cnt].flow=flow;
    e[cnt].pay=pay;
    e[cnt].next=head[from];
    head[from]=cnt++;
}
bool SPFA(int s,int t) {
    queue<int>q;
    memset(vis,0,sizeof(vis));
    memset(pre,-1,sizeof(pre));
    memset(dis,0x3f,sizeof(dis));
    dis[s]=0;
    vis[s]=1;
    q.push(s);
    while(!q.empty()) {
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(int i=head[u]; ~i; i=e[i].next) {
            int v=e[i].to;
            if(e[i].cap>e[i].flow&&dis[v]>dis[u]+e[i].pay) {
                dis[v]=dis[u]+e[i].pay;
                pre[v]=i;
                if(vis[v])continue;
                vis[v]=1;
                q.push(v);
            }
        }
    }
    return pre[t]!=-1;
}
int MCMF(int s,int t) {
    int mincost=0;
    while(SPFA(s,t)) {
        int d=inf,v=t;
        while(v!=s) {
            int i=pre[v];
            d=min(d,e[i].cap-e[i].flow);//可增加的流量
            v=e[i^1].to;//换点
        }
        v=t;
        while(v!=s) {
            int i=pre[v];
            e[i].flow+=d;
            e[i^1].flow-=d;//实流
            v=e[i^1].to;
        }
        mincost+=dis[t]*d;
    }
    return mincost;
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >>n>>m;
    memset(head,-1,sizeof(head));
    s=0,t=n+2;
    for(int i=1; i<=n; i++) {
        int c;
        cin >>c;
        Add(i,i+1,inf-c,0,0);
        Add(i+1,i,0,0,0);
    }
    Add(s,1,inf,0,0),Add(1,s,0,0,0);
    Add(n+1,t,inf,0,0),Add(t,n+1,0,0,0);
    for(int i=1; i<=m; i++) {
        int u,v,p;
        cin >>u>>v>>p;
        Add(u,v+1,inf,0,p);
        Add(v+1,u,0,0,-p);
    }
    cout <<MCMF(s,t);
    return 0;
}

总结

最小费用最大流属于网络流问题,建图仍然是主要的问题,如何将题目给定条件转换成图的边权和点权是最为关键的问题,之后就是套用模板的过程,根据题设条件用对应算法求解

举个例子,如果费用与流量平方成正比,容量 c a p cap cap为整数,每条边有一个费用系数 a a a,该边的流量为 x x x时边费用为 a x 2 ax^2 ax2,这时为了求出最小费用流,对于费用为 a x 2 ax^2 ax2的边,需要将这条边拆了,拆成 c c c条容量为1,费用为 a , 3 a , … ( 2 c − 1 ) a a,3a,\dots (2c-1)a a,3a,(2c1)a的边,流量为x时,走前x条费用最小的边

参考文献

  1. 算法竞赛入门经典-网络流之最小费用流
  2. 最小费用最大流问题与算法实现(Bellman-Ford、SPFA、Dijkstra)
  3. POJ2175-消圈算法
  4. P4553 80人环游世界 题解
  5. P3980 [NOI2008] 志愿者招募 题解
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个使用Matlab实现最小费用最大流算法的示例代码: ```matlab function [max_flow, min_cost] = min_cost_max_flow(capacity, cost, source, sink) % capacity: 流量限制矩阵,capacity(i,j)表示从节点i到节点j的流量上限 % cost: 每单位流量的花费矩阵,cost(i,j)表示从节点i到节点j的每单位流量花费 % source: 源点 % sink: 汇点 n = size(capacity, 1); % 计算残量矩阵 residual = capacity; % 初始化流量和费用 max_flow = 0; min_cost = 0; while true [augment, path, cost] = bellman_ford(residual, cost, source, sink); if augment == 0 break; % 不存在增广路,结束 end max_flow = max_flow + augment; min_cost = min_cost + augment * cost(path(end-1), path(end)); for i = 1:length(path)-1 residual(path(i), path(i+1)) = residual(path(i), path(i+1)) - augment; residual(path(i+1), path(i)) = residual(path(i+1), path(i)) + augment; end end end function [augment, path, cost] = bellman_ford(residual, cost, source, sink) n = size(residual, 1); % 初始化距离和前驱节点 dist = inf(1, n); prev = zeros(1, n); % 起点到自身的距离为0 dist(source) = 0; % 进行n-1轮松弛操作 for i = 1:n-1 for u = 1:n for v = 1:n if residual(u, v) > 0 && dist(u) + cost(u, v) < dist(v) dist(v) = dist(u) + cost(u, v); prev(v) = u; end end end end % 判断是否存在增广路 if dist(sink) == inf augment = 0; path = []; cost = 0; return; end % 寻找增广路 augment = inf; u = sink; path = u; while u ~= source v = prev(u); augment = min(augment, residual(v, u)); path = [v, path]; u = v; end % 计算增广路的花费 cost = 0; for i = 1:length(path)-1 cost = cost + cost(path(i), path(i+1)); end end ``` 该代码实现了基于Bellman-Ford算法最小费用最大流算法。在实际应用中,还需要考虑如何构造流量限制矩阵和每单位流量的花费矩阵,以及如何处理无解情况等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值