最小费用流

最小费用流

  • 所谓费用流就是,在普通的网络流当中,每一条边加上一个费用,求出一组解,使得在流量满足一定的情况下,费用的花费情况。

既然要求费用最小的情况,很容易想到要使用最短路径,把每一条边的费用当成距离,这样得到的路径费用总和是最小的,就可以用最短路径算法求从 s s s t t t的最短路径,再结合之前的沿着这条路径增广即可。不过在求最短距离的时候,要加一个条件,就是保证这条边有流量,否则就没有意义了

例如求最小费用最大流

题目描述

如题,给出一个网络图,以及其源点和汇点,每条边已知其最大流量和单位流量费用,求出其网络最大流和在最大流情况下的最小费用。

输入

第一行包含四个正整数 N , M , S , T , N,M,S,T, NMST分别表示点的个数、有向边的个数、源点序号、汇点序号。

其中 N ⩽ 5000 , M ⩽ 50000 N \leqslant 5000,M \leqslant 50000 N5000M50000

接下来 M M M行每行包含四个正整数 u i 、 v i 、 w i 、 f i u_i、v_i、w_i、f_i uiviwifi,表示第i条有向边从 u i u_i ui出发,到达 v i v_i vi,边权为 w i w_i wi(即该边最大流量为 w i w_i wi),单位流量的费用为 f i f_i 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

源点是 4 4 4,汇点是 3 3 3

思路就是用bellman-ford找到一条从 s s s t t t的最短路径,同时记录下路径,因为后面要修改流量,同时由于是单位费用,找到这条路所带来的流量然后乘以到达 t t t的最短距离就是这条路的费用,不断找最短路径,直到不能增广为止

代码

#include <iostream>
#include <cstring>
#include <cstdio>
#include <cctype>
#define min(a,b) a>b?b:a
#define M 50005
#define N 5005
#define INF 0x3f3f3f3f
#define in(a) a = read()

inline int read () {
	int x = 0, f = 1; char c = getchar();
	while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
	while(isdigit(c)) {x = (x << 3) + (x << 1) + c - 48; c = getchar();}
	return f*x;
}

using namespace std;

int n, m, s, t, cnt;
int maxflow; //最大流
int dis[N]; //dis[i]表示从s到达i点的最短路径
int preV[N], preE[M]; //preV[i]表示顶点i的前置节点,preE[i]表示顶点i的前一条边

struct Edge {
	int from, to, cap, cost;
}a[M * 2];

int bellman_ford () {
	int res;
	memset(dis, INF, sizeof(dis));
	dis[s] = 0;
	bool update = true;
	while (update) {
		update = false;
		for(int i = 0; i < cnt; i++) {
			int u = a[i].from, v = a[i].to;
			if(dis[u] != INF && a[i].cap > 0 && dis[v] > dis[u] + a[i].cost) {
				dis[v] = dis[u] + a[i].cost;
				preE[v] = i;
				preV[v] = u;
				update = true;
			}
		}
	}
	if(dis[t] == INF || dis[t] < 0) return -1;
	int flow = INF;
	for(int v = t; v != s; v = preV[v]) 
		flow = min(flow, a[preE[v]].cap);
	res = flow * dis[t];
	maxflow += flow;
    //增广
	for(int v = t; v != s; v = preV[v]) {
		a[preE[v]].cap -= flow; //正边减去流量
		a[preE[v]^1].cap += flow; //反边加上流量
	}
	return res;
}

int main () {
	int u, v, w, f;
	in(n), in(m), in(s), in(t);
	for(int i = 1; i <= m; i++) { //所有的正边都是偶数,反边都是奇数
		in(u), in(v), in(w), in(f);
		a[cnt].from = u, a[cnt].to = v, a[cnt].cap = w, a[cnt].cost = f;
		cnt++;
		a[cnt].from = v, a[cnt].to = u, a[cnt].cap = 0, a[cnt].cost = -f;
		cnt++;
	}
	int ans = 0, t;
	while(1) {
		t = bellman_ford();
		if(t == -1) break;
		ans += t;
	}	
	printf("%d %d", maxflow, ans);
	return 0;
}

为了让算法效率更快,可以使用 D i j k s t r a Dijkstra Dijkstra算法求解,众所周知, D i j k s t r a Dijkstra Dijkstra算法无法判断负环。为了消除这一影响,需要人为的给每一个节点加上一个 h h h,使得原来的一条边 e ( u , v ) e(u,v) e(u,v)变成 e ′ ( u , v ) = e ( u , v ) + h ( u ) − h ( v ) e'(u,v) = e(u,v) + h(u)-h(v) e(u,v)=e(u,v)+h(u)h(v),只要保证 e ′ ( u , v ) ⩾ 0 e'(u,v) \geqslant 0 e(u,v)0就可以使用 D i j k s t r a Dijkstra Dijkstra算法了

现在对于原来的每一条边 e ( u , v ) e(u,v) e(u,v),边的权值变成了 w ′ ( u , v ) = w ( u , v ) + h [ u ] − h [ v ] w'(u,v) = w(u,v) + h[u] - h[v] w(u,v)=w(u,v)+h[u]h[v]

假设有一条源点 s s s t t t的路径, p 1 , p 2 , p 3 . . . p n p_1,p_2,p_3...p_n p1,p2,p3...pn,这条路径的边权之和为

( w 1 + w 2 + . . . + w n − 1 ) + ( h 1 − h 2 + h 2 − h 3 + h 3 − h 4 + . . . h n − 1 − h n ) (w_1+w_2+...+w_{n-1})+(h_1-h_2+h_2-h_3+h_3-h_4+...h_{n-1}-h_n) (w1+w2+...+wn1)+(h1h2+h2h3+h3h4+...hn1hn)

相当于 ∑ i = 1 n − 1 w i + h 1 − h n \sum^{n-1}_{i=1}w_i + h_1-h_n i=1n1wi+h1hn,只要算出这个路线的最短路之后,减去 h s − h t h_s - h_t hsht就可以得到原来的最短路了

那么现在就要考虑怎么求这个势 h h h

假设 d i s [ i ] dis[i] dis[i]表示从起点到达 i i i的最短路径,则一定有

  • d i s [ v ] ⩽ d i s [ u ] + e ( u , v )          − >         d i s [ u ] + e ( u , v ) − d i s [ v ] ⩾ 0 dis[v] \leqslant dis[u] + e(u,v) \,\,\,\,\,\,\,\, -> \,\,\,\,\,\,\,dis[u] + e(u,v) - dis[v] \geqslant 0 dis[v]dis[u]+e(u,v)>dis[u]+e(u,v)dis[v]0

因为 d i s [ u ] dis[u] dis[u]表示到 u u u点的最短距离, d i s [ v ] dis[v] dis[v]表示到 v v v的最短距离,而到 v v v的最短距离不一定是从 ( u , v ) (u,v) (u,v)这条边转移过来的。

显然,可以把到达这个点的最短距离当做这个点的势,这样图中就不会有负权边了,就可以跑 D i j k s t r a Dijkstra Dijkstra算法了

题目链接

#include <cstdio>
#include <queue>
#include <bits/stdc++.h>
#include <iostream>
#include <cstring>
#define N 5005
#define M 50005
#define INF 0x7fffffff
#define P pair<int, int >
using namespace std;
inline int min (int x, int y) {
	return x > y ? y : x;
}
int n, m, s, t, cnt = -1, maxflow, mincost;
int head[N], dis[N], h[N], preE[N], preV[N];
struct Edge {
	int next, to, cap, cost;
}edge[M * 2];

inline void Ad (int u, int v, int c, int w) {
	edge[++cnt].next = head[u], head[u] = cnt;
	edge[cnt].to = v, edge[cnt].cap = c, edge[cnt].cost = w;
}
inline void add (int u, int v, int c, int w) {
	Ad(u, v, c, w), Ad(v, u, 0, -w);
}
priority_queue <P, vector<P >, greater <P > > q;

void dijkstra () {
	while (!q.empty ()) q.pop();
	memset (dis, -1, sizeof (dis));
	dis[s] = 0, q.push (P (0, s));
	while (!q.empty ()) {
		P cur = q.top (); q.pop ();
		int u = cur.second;
		if (dis[u] < cur.first) continue; //点可能被更新多次
		for (int i = head[u]; i != -1; i = edge[i].next) {
			if (!edge[i].cap) continue; //保证要有流量
			int v = edge[i].to; //这条边指向的顶点
			if (dis[v] < 0 || dis[v] > dis[u] + edge[i].cost + h[u] - h[v]) {
				dis[v] = dis[u] + edge[i].cost + h[u] - h[v];
				preV[v] = u, preE[v] = i;
				q.push (P (dis[v], v));
			}
		}		
	}
}
void solve () {
	memset (h, 0, sizeof (h));
	for (int f = INF; f > 0;) {
		dijkstra ();
		if (dis[t] < 0) break;
		for (int i = 1; i <= n; i++) //更新势(很重要)
			h[i] += (dis[i] == -1 ? 0 : dis[i]);
		int flow = f;
		for (int i = t; i != s; i = preV[i])  //找到增广路带来的流量
			flow = min (flow, edge[preE[i]].cap);
		maxflow += flow, f -= flow;
		mincost += flow * h[t];
		for (int i = t; i != s; i = preV[i]) {
			edge[preE[i]].cap -= flow;
			edge[preE[i] ^ 1].cap += flow;
		}
	}
}
int main () {
	int u, v, ca, co;
	scanf ("%d %d", &n, &m);
	s = 1, t = n;
	memset (head, -1, sizeof (head));
	for (int i = 1; i <= m; i++) {
		scanf ("%d %d %d %d", &u, &v, &ca, &co);
		add(u, v, ca, co);	
	}
	solve ();
	cout << maxflow << " " << mincost;
	return 0;
}

未完待续。。。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值