最小费用最大流算法总结
考前水图论
我们已经学过了求最大流的算法,现在我们想象假如我们有一个流量网络,现在每个边除了流量,现在还有一个单位费用,这条边的费用相当于它的单位费用乘上它的流量,我们要保持最大流的同时,还要保持边权最小,这就是最小费用最大流问题。
我们该如何求最小费用最大流呢?
其实是可以用最短路更新的。
我们每次要更新的时候就可以在费用网络上跑最短路。因为有负权的问题,我们可以先用spfa跑。
核心代码如下
while(!que.empty()) { int now=que.front();que.pop();vis[now]=0; for (int i=st[now];~i;i=e[i].next) if (e[i].flow>0 && dis[e[i].to]>dis[now]+e[i].cost) { dis[e[i].to]=dis[now]+e[i].cost; pe[e[i].to]=i,pv[e[i].to]=now; if (!vis[e[i].to]) vis[e[i].to]=1,que.push(e[i].to); } }
这时spfa更新的条件就是:还有流量可以流且现在所连到的点的dis(费用)比现在自己的dis加上当前边的费用要大。(相当于spfa更新边权)同时还要更新一下能增广的最大流量。
最后的时候我们需要更新一下
核心代码:
while(spfa(S,T)) { flow=INF; for (int i=T;i!=S;i=pv[i]) flow=min(flow,e[pe[i]].flow); COST+=flow*dis[T]; FLOW+=flow; for (int i=T;i!=S;i=pv[i]) e[pe[i]].flow-=flow,e[pe[i]^1].flow+=flow; }
因为我们求出了最短路,实际上就是求出了这条增广路上的单位权值的和。那么总增广的费用就是最短路*总流量
在上一段代码中我们记录了一个p的数组,这样的话就可以通过汇点反着找到这条最短路,接着增广流量。
以为这种方法在稠密图较慢,但是它可以处理负权,而且可以方便的改成最大费用最大流。
代码
#include <cstdio> #include <algorithm> #include <queue> #include <cstring> #define M 2001000 #define N 1001000 #define INF 0x33333333 #define min(x,y) ((x<y)?(x):(y)) using namespace std; typedef pair<int,int> Pair; struct node{int from,to,next,flow,cost;}e[M]; int tot=-1,st[M]; int n,m,x,y,z; int pe[N],pv[N],dis[N],vis[N]; void add(int x,int y,int z,int zz){ e[++tot].to=y; e[tot].from=x; e[tot].flow=z; e[tot].cost=zz; e[tot].next=st[x]; st[x]=tot; } void add_edge(int x,int y,int z,int zz){add(x,y,z,zz),add(y,x,0,-zz);} queue<int>que; bool spfa(int S,int T) { memset(dis,0x33,sizeof dis); memset(vis,0,sizeof vis); que.push(S),vis[S]=1,dis[S]=0; while(!que.empty()) { int now=que.front();que.pop();vis[now]=0; for (int i=st[now];~i;i=e[i].next) if (e[i].flow>0 && dis[e[i].to]>dis[now]+e[i].cost) { dis[e[i].to]=dis[now]+e[i].cost; pe[e[i].to]=i,pv[e[i].to]=now; if (!vis[e[i].to]) vis[e[i].to]=1,que.push(e[i].to); } } return dis[T]<INF; } Pair mfmc(int S,int T) { int COST=0,FLOW=0,flow; while(spfa(S,T)) { flow=INF; for (int i=T;i!=S;i=pv[i]) flow=min(flow,e[pe[i]].flow); COST+=flow*dis[T]; FLOW+=flow; for (int i=T;i!=S;i=pv[i]) e[pe[i]].flow-=flow,e[pe[i]^1].flow+=flow; } return make_pair(FLOW,COST); } main() { int m,from,to; scanf("%d%d%d%d",&n,&m,&from,&to); memset(e,-1,sizeof e); memset(st,-1,sizeof st); int S=from,T=to; for(int i=0;i<m;i++) { int u,v,flow,cost; scanf("%d%d%d%d",&u,&v,&flow,&cost); add_edge(u,v,flow,cost); } Pair ans=mfmc(S,T); printf("%d %d\n",ans.first,ans.second); }
接下来就是重点,用Dijkstra求解费用流
有人可能会说,Dijkstra只能在正权图上求最短路呀,而费用流的网络构图中\(w[x][y] = -w[y][x]\),势必会产生负权边,怎么能用Dijkstra呢?(下文用\(w[i][j]\)表示费用,\(p[i]\)表示点,方便讲解)
我们求解费用流一般用spfa或zkw流,这两种方法在稀疏图上运行效率还不错,但是在稠密图上的表现却无法令人满意,而且其运行效率与图的形态关系太大,无法令人安心。
反观Dijkstra,它在稠密图\(O(n^2)\)和稀疏图\(O(mlogm)\)上的表现都不错,而且其运行时间与图的形态关系不大,较为稳定,唯一的缺点是不能运行在负权图上。
事实上在对网络进行一些处理后,Dijkstra是可以用来求解费用流的。
我们对网络G中的每个点i设置一个势函数\(h[i]\),在算法运行的每一时刻,对于每条残余网络中的边\((x, y)\),都要满足\(h[x] + w[x][y] – h[y] >= 0\)(三角不等式)。维护h[i]的方法下文再讨论。
构建新网络\(G’,V’ = V,w'[x][y] = h[x] + w[x][y] – h[y]\),这样在新网络中所有边的权值均非负,可以使用Dijkstra求解最短路。
\(G’\)中的一条路径 \(p[1], p[2],\cdots, p[m]\) 的权值为
\begin{align*}
w'[path]&=w'[p[1]][p[2]] + w'[p[2]][p[3]] + \cdots + w'[p[m-1]][p[m]]\\
&=h[p[1]]-h[p[2]] + w[p[1]][p[2]]\\
&+h[p[2]]-h[p[3]] + w[p[2]][p[3]]\\
&+\cdots\\
&+h[p[m-1]]-h[p[m]] + w[p[m-1]][p[m]]\\
&=w[p[1]][p[2]] + w[p[2]][p[3]] + \cdots + w[p[m-1]][p[m]] + h[p[1]]-h[p[m]] \\
&=w[path] + h[p[1]]-h[p[m]]
\end{align*}
而当p[1]和p[m]确定时,最优化\(w[path]\)与最优化\(w'[path]\)是等价的,所以我们可以在G’中求解S到T的最短路\(w'[path]\),G中S到T的最短路\(w[path] = w'[path] – h[S] + h[T]\)。
下面我们来讨论如何维护势函数\(h[]\)。
初始情况下,如果所有的权值都为正,那么可以简单地将所有\(h[i]\)设置为0;如果有负权值,那么我们做一遍spfa,让h[]等于距离函数。如果初始网络为DAG,也可以采用递推\(h[i] = min(h[j] + w[j][i], g[j][i] = true)\)。
让\(d'[i]\)表示G’中S到点i的距离,当某次增广结束后,G’中会新加入某些边\((j, i)\),而这些(j, i)必定满足\(d'[i] + w'[i][j] = d'[j]\)(否则(i, j)就不会在增广路中)。对上式进行一些代数变换:
\begin{align*}
d'[i] + w'[i][j] = d'[j]
\\d'[i] + w[i][j] + h[i]-h[j] = d'[j]
\\(d'[j] + h[j])-(d'[i] + h[i])-w[i][j] = 0
\\(d'[j] + h[j])-(d'[i] + h[i]) + w[j][i] = 0
\end{align*}
(因为是费用流,所以有w[i][j] = -w[j][i])
因此让所有\(h[i] += d'[i]\)后,新加入的边(j, i)也会满足势函数的性质。
同时我们有:
\begin{align*}
d'[i] + w'[i][j] – d'[j] >= 0\\
d'[i] + h[i] – h[j] + w[i][j] – d'[j] >= 0\\
(d'[i] + h[i]) – (d'[j] + h[j]) + w[i][j] >= 0\\
\end{align*}
因此修改\(h[i]\)后,\((i, j)\)依然会满足势函数的性质。
算法过程如下:
>S1 初始化h[]
>S2 在残留网络中做Dijkstra
>S3 若S到T有可行路径,则修改增广路上的边的容量并所有h[i] += d'[i],转S2,否则退出
算法时间复杂度为:\(O(spfa() + K * Dijkstra())\)或\(O(K * Dijkstra())\),这取决于初始化h[]时是否调用spfa,K表示增广次数。
代码如下
#include <cstdio> #include <algorithm> #include <queue> #include <cstring> #define M 2001000 #define N 1001000 #define INF 0x33333333 #define min(x,y) ((x<y)?(x):(y)) using namespace std; typedef pair<int,int> Pair; struct node { int from,to,next,flow,cost; }e[M]; int tot=-1,st[M]; int n,m,x,y,z; void add(int x,int y,int z,int zz) { e[++tot].to=y; e[tot].from=x; e[tot].flow=z; e[tot].cost=zz; e[tot].next=st[x]; st[x]=tot; } Pair main_pro(int s,int t) { static int h[N]; int flow=0,cost=0; while(1) { static int dis[N],pv[N],pe[N]; memset(dis,0x33,sizeof dis); //距离初始化为inf dis[s]=0; priority_queue<Pair,vector<Pair>,greater<Pair> >que; que.push(Pair(0,s)); while(!que.empty()) { Pair now=que.top(); que.pop(); if (now.first!=dis[now.second]) continue; //dis[now.second]是目前的值,now.first是入堆时的值,如果目前的优,直接continue if (now.second==t) break; for (int i=st[now.second];~i;i=e[i].next) { int nowcost=e[i].cost+h[now.second]-h[e[i].to]; //现在的费用为这条路径的费用+起始点的势函数-结尾点的势函数 if (e[i].flow>0 && dis[e[i].to]>dis[now.second]+nowcost) //松弛 { dis[e[i].to]=dis[now.second]+nowcost; que.push(Pair(dis[e[i].to],e[i].to)); pv[e[i].to]=now.second; //更新节点所对应的路径 pe[e[i].to]=i; } } } if (dis[t]==INF) //没有增广到T break; for (int i=0;i<n;i++) //更新势函数 h[i]=min(h[i]+dis[i],INF); int newflow=INF; for (int x=t;x!=s;x=pv[x]) //找到最小流量 newflow=min(newflow,e[pe[x]].flow); flow+=newflow; cost+=newflow*h[t]; for (int x=t;x!=s;x=pv[x]) //更新流量 e[pe[x]].flow-=newflow,e[pe[x]^1].flow+=newflow; } return make_pair(flow,cost); } int main() { int m,from,to; scanf("%d%d%d%d",&n,&m,&from,&to); memset(e,-1,sizeof e); memset(st,-1,sizeof st); for(int i=0;i<m;i++) { int u,v,flow,cost; scanf("%d%d%d%d",&u,&v,&flow,&cost); add(u,v,flow,cost); add(v,u,0,-cost); } Pair ans=main_pro(from,to); printf("%d %d\n",ans.first,ans.second); }