网络流算法

本文深入探讨了最大流算法,特别是Dinic算法的实现及其优化策略,包括当前弧优化、炸点优化和分层优化。同时,文章介绍了最小割的概念,并将其与最大流联系起来。此外,还详细讲解了费用流的基础概念及最小费用最大流的实现,包括使用SPFA和Dijkstra算法进行路径查找的优化方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最大流(Dinic算法)

最大流:给定一张带权有向图,有源点和汇点。源点可以流出无穷多的水,汇点可以接受无穷多的水,但是每条边有最大可承载流量,要求输出汇点接受到的最大流量。

/*
Dinic算法,理论上界O(n^2*m) 
步骤:
1.在残量网络上bfs求出节点的层次,构造分层图
2.在分层图上dfs寻找增广路,回溯时更新流量 
优化:
1.当前弧优化:当dfs增广的时候,从有效的边开始增广,不再增广以前增广过的
2.炸点优化:dfs时当前点没有流量了,就不再使用了
3.分层优化:bfs时找到汇点就退出 
*/ 

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;

typedef long long ll; 
const int maxn = 1205;

struct edges{
	int to,next;
	ll val;
}edge[300005];

int head[maxn],cnt;
int dep[maxn];      //depth[i]表示第i个点的层次,即从源点到它的距离 
int cur[maxn];      //当前弧优化,每一个点当前增广到的边 
int s,e;
int tot;   //总共有多少点 

void add(int u,int v,ll val) {
	edge[cnt].to = v;
	edge[cnt].val = val;
	edge[cnt].next = head[u];
	head[u] = cnt++;
}
bool bfs() {    //bfs分层 
	queue<int> q;
	memset(dep,0,sizeof(dep));
	dep[s] = 1;
	q.push(s);
	while( !q.empty() ) {
		int x = q.front();
		q.pop();
		for (int i = head[x]; i != -1; i = edge[i].next) {
			int v = edge[i].to;
			if( edge[i].val > 0 && dep[v] == 0 ) { //若该残量不为0,且V[i]还未分配深度,则给其分配深度并放入队列
				dep[v] = dep[x] + 1;
				q.push(v); 
				if( v == e ) return true;    //分层优化,找到汇点就退出 
			}
		}
	} 
	if( dep[e] == 0 ) return false;  //当汇点的深度不存在时,说明不存在分层图,同时也说明不存在增广路
	return true;
}
ll dfs(int x,ll flow) {   //当前节点,当前状态这个点能流的最大流 
	if( x == e ) return flow;   //到终点直接返回 
	ll used = 0;    //记录当前点流出去的最大流量 
	for (int i = cur[x]; i != -1; i = edge[i].next) { //当前弧优化 
		cur[x] = i;    //x节点增广到第i条边了 
		if( dep[edge[i].to] == dep[x] + 1 && edge[i].val > 0 ) {
			ll d = dfs(edge[i].to,min(flow-used,edge[i].val));   //向下增广 
			edge[i].val -= d;
			edge[i^1].val += d;
			used += d;
			if( used == flow ) break;
		}
	}
	if( used == 0 ) dep[x] = -1;   //炸点优化,这个点已经没有流量了,说明没有用了,置为-1在循环中不会用到了 
	return used;   //否则说明没有增广路,返回0
}
ll Dinic() {
	ll ans = 0;
	while( bfs() ) {
		for (int i = 0; i <= tot; i++)   //每次分层后更新cur,当前增广的边肯定是第一条边,这里必须覆盖了全部的点 
			cur[i] = head[i];
		ans += dfs(s,1e18);
	}
	return ans;
}
int main()
{
	int n,m;
	cin >> n >> m >> s >> e;
	tot = n; 
	memset(head,-1,sizeof(head));
	for (int i = 1; i <= m; i++)
	{
		int x,y;
		ll v;
		cin >> x >> y >> v;
		add(x,y,v);
		add(y,x,0);
	}
	cout << Dinic() << '\n';
	return 0;
}

最小割

给定一个网络,若一个边集被删去后,起点无法抵达终点,则成该边集为这个网络的割。
最小权值的割称为最小割。
最小割=最大流

费用流

在最大流的基础上,给每条边权加上了单位流量的花费p,f的流量会对答案产生p*f的贡献。
最小费用最大流:在最大化流量的前提下最小化总费用。

/*
最小费用最大流
加上单位花费后,只要在EK算法的基础上加以改进即可。
将EK算法中bfs找一条从s到t的路径变成spfa找一条从s到t的路径单位花费和最小的路径即可。 
复杂度上界为O(V^2*E) 
*/ 

#include <iostream>
#include <queue>
#include <cstring> 
using namespace std;
typedef long long ll;
const ll INF = 1e18;
const int maxn = 5e3+5;

struct Edge{
	int to,next;
	ll val,cost;
}edge[100010];
int head[maxn],cnt = 0;

void add(int u,int v,ll val,ll cost)
{
	edge[cnt].to = v;
	edge[cnt].val = val;
	edge[cnt].cost = cost;
	edge[cnt].next = head[u];
	head[u] = cnt++;
}

int s,e,tot;
int exist[maxn],preid[maxn],tag[100010];
ll dist[maxn];
//exist[i]表示是否在队列中
//preid[i]表示i的前一个节点编号
//tag[i]表示preid到i的边的下标编号 
  
bool spfa()       //找一条从起点到终点单位花费和最少的路径 
{
	for (int i = 0; i <= tot; i++)   //一定要包括所有的点 
	{
		dist[i] = INF;
	}
	memset(preid,0,sizeof(preid));
	memset(tag,0,sizeof(tag));
	memset(exist,0,sizeof(exist));
	queue<int> q;
	q.push(s);         //把起点入队 
	exist[s] = 1; 
	dist[s] = 0;
	while( !q.empty() )
	{
		int x = q.front();
		exist[x] = 0;
		q.pop();
		for (int i = head[x]; i != -1; i = edge[i].next)   //遍历与x相邻的节点 
		{
			Edge t = edge[i];
			if( t.val && dist[t.to] > dist[x] + t.cost )   //如果这条边的权值大于0,表明可以走 
			{
				dist[t.to] = dist[x] + t.cost;
				preid[t.to] = x;    //preid置为x 
				tag[t.to] = i;      //tag置为i 
				if( !exist[t.to] )
				{
					q.push(t.to);
					exist[t.to] = 1;
				}
			}
		}
	}
	if( dist[e] == INF ) return false;   //找不到这条路径 
	return true; 
}

void mcmf(ll &flow,ll &cost)
{
	while( spfa() )
	{
		ll mi = 1e18;
		for (int t = e; t != s; t = preid[t])
		{
			mi = min(mi,edge[tag[t]].val);    //回溯找到最小流量 
		}
		for (int t = e; t != s; t = preid[t])
		{
			edge[tag[t]].val -= mi;         //正边减去最小流量 
			edge[tag[t]^1].val += mi;       //负边加上最小流量 
		}	
		flow += mi;    						//答案加上流量 
		cost += (ll)mi * dist[e];           //花费加上流量乘单位花费和 
	}
}

int main()
{
	int n,m;
	cin >> n >> m >> s >> e;
	tot = n;
	memset(head,-1,sizeof(head));
	for (int i = 1; i <= m; i++)
	{
		int x,y;
		ll v,w;
		cin >> x >> y >> v >> w;
		add(x,y,v,w);
		add(y,x,0,-w);
	}
	ll flow = 0,cost = 0;
	mcmf(flow,cost);
	cout << flow << ' ' << cost << '\n';
	return 0;
}

Dijkstra优化费用流
由于我们需要跑多次的spfa,相当不优秀,所以我们可以通过引入势函数去修正边权使得边权为负。
给每个点赋一个势函数h[i],如果我们保证h[u]-h[v]+w[u][v]>=0,那么我们在松弛时就把边变为这个东西,这样就都是正权边了。那么这个基于这个跑出来的单源最短路dis’[x],实际上的dis[x]=dis’[x]-f[s]+f[x],s为起点。
那么如何保证这个不等式呢,其实移项一下就变成为h[u]+w[u][v]>=h[v],这个就是典型的三角形不等式,所以h[i]的值就是为原图下的最短路径的值。

那么我们考虑利用以上的算法来优化费用流。
在费用流里面,初始的费用如果都是非负的,那么初始的势函数为0。如果图为有向无环图,则可以用dp来转移O(m)算出最短路,否则需要跑一遍spfa来确定初始的f[i]值。
确定了f的初值后,我们考虑每次跑一遍最短路之后,残量网络的变化。其实就是有反边的加入,所以需要修正这个势函数。
实际上对于h[i],新的势函数就是原来的加上这次跑的最短路。可以证明这样操作后不影响正边,同时反边的加入也能得到解决。
证明:
首先费用流跑一次后只会在最短路径上加入反边,那么对于最短路径上的任意两点u->v,均有dis’[u]+w’[u][v]=dis’[v]。即dis’[u]+h[u]-h[v]+w[u][v]=dis’[v]。移项后(dis’[u]+h[u])-(dis’[v]+h[v])+w[u][v]=0。而括号里的就是新的势函数,满足三角形不等式,所以这样更新后对原来的正边没有影响。等式两边同时取负号,(dis’[v]+h[v])-(dis’[u]+h[u])+w[v][u]=0,也能看出也满足反边的要求。

/*
最小费用最大流
dij优化费用流
利用势函数将负权变成正权,dij来替换spfa
复杂度(n*mlogm) 
*/ 

#include <iostream>
#include <queue>
#include <cstring> 
using namespace std;
typedef long long ll;
const ll INF = 1e18;
const int maxn = 5e3+5;

struct Edge{
	int to,next;
	ll val,cost;
}edge[100010];
int head[maxn],cnt = 0;

void add(int u,int v,ll val,ll cost)
{
	edge[cnt].to = v;
	edge[cnt].val = val;
	edge[cnt].cost = cost;
	edge[cnt].next = head[u];
	head[u] = cnt++;
}

struct node{      //dij需要堆优化里放的节点 
	int id;
	ll val;
	node(int a,ll b)
	{
		id = a;
		val = b;
	}
	bool operator<(const node&n)const
	{
		return val > n.val;
	}
};

int s,e,tot;
int preid[maxn],vis[maxn],tag[100010];
ll dist[maxn];
ll h[maxn];    //势函数 
//preid[i]表示i的前一个节点编号
//tag[i]表示preid到i的边的下标编号 
  
bool dij()       //找一条从起点到终点单位花费和最少的路径 
{
	for (int i = 0; i <= tot; i++)   //一定要包括所有的点 
		dist[i] = INF;
	memset(vis,0,sizeof(vis));
	memset(preid,0,sizeof(preid));
	memset(tag,0,sizeof(tag));
	priority_queue<node> q;
	q.push(node(s,0));         //把起点入队 
	dist[s] = 0;
	while( !q.empty() )
	{
		int x = q.top().id;
		q.pop();
		if( vis[x] ) continue;
		vis[x] = 1;
		for (int i = head[x]; i != -1; i = edge[i].next)   //遍历与x相邻的节点 
		{
			Edge t = edge[i];
			if( t.val && dist[t.to] > dist[x] + t.cost + h[x] - h[t.to] )   //如果这条边的权值大于0,表明可以走 
			{
				//注意一定要加势函数 
				dist[t.to] = dist[x] + t.cost + h[x] - h[t.to];
				preid[t.to] = x;    //preid置为x 
				tag[t.to] = i;      //tag置为i 
				q.push(node(t.to,dist[t.to]));
			}
		}
	}
	if( dist[e] == INF ) return false;   //找不到这条路径 
	return true; 
}

void mcmf(ll &flow,ll &cost)
{
	memset(h,0,sizeof(h));    //本题中费用均为正数,所以势函数初值为0,否则需要用dp或spfa求出初始的势函数 
	while( dij() )
	{
		ll mi = 1e18;
		for (int t = e; t != s; t = preid[t])
		{
			mi = min(mi,edge[tag[t]].val);    //回溯找到最小流量 
		}
		for (int t = e; t != s; t = preid[t])
		{
			edge[tag[t]].val -= mi;         //正边减去最小流量 
			edge[tag[t]^1].val += mi;       //负边加上最小流量 
		}	
		flow += mi;    						//答案加上流量 
		ll dis = dist[e] - h[s] + h[e];     //求出实际的最短路  
		cost += mi * dis;           //花费加上流量乘单位花费和 
		for (int i = 0; i <= tot; i++) h[i] += dist[i];    //更新势函数 
	}
}

int main()
{
	int n,m;
	cin >> n >> m >> s >> e;
	tot = n;
	memset(head,-1,sizeof(head)); 
	for (int i = 1; i <= m; i++)
	{
		int x,y;
		ll v,w;
		cin >> x >> y >> v >> w;
		add(x,y,v,w);
		add(y,x,0,-w);
	}
	ll flow = 0,cost = 0;
	mcmf(flow,cost);
	cout << flow << ' ' << cost << '\n';
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值