图论---网络流

前言

传说中的省选知识点

鲁迅曾经说过:“网络流代码只有普及-难度,主要看建模”

暂时只会最大流、费用流

Update 20210228:更新了算法分析

正文

蒟蒻只会网络流,学习的下面两篇文章

网络流详解

OI-wiki 最大流

理解大体思路粘一个板子就差不多了

感觉解释在板子代码中挺清晰了

板子&算法分析

模板题

EK 算法

比较暴力的一种算法,还有一种更慢的FF算法

算法流程:
1、每次bfs增广找到一条还有残量的路径并记录经过的路径
2、根据可到达汇点的流量dfs回溯到源点,将所经过的边的正边减掉到达汇点的流量,反边加上,结束时统计答案
3、不断进行bfs \(\to\) dfs \(\to\) bfs \(\to\) dfs …… 直道bfs不能到达汇点时停止
注:建反边的目的是为“反悔”操作创造条件

时间复杂度:最劣 \(O(nm^2)\)

/*
Work by: Suzt_ilymics
Knowledge: 网络流 EK算法 
Time: O(nm^2)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#define LL long long
#define int long long
#define orz cout<<"lkp AK IOI!"<<endl

using namespace std;
const int MAXN = 1e5+5;
const int INF = 1 << 29;
const int mod = 1e9+7;

struct edge{
	int to, w, nxt;
}e[MAXN << 1];
int head[MAXN], num_edge = 1;//初始为1方便转化 

int n, m, s, t, maxflow = 0;
int pre[MAXN], incf[MAXN];//分别记录到过的边,和当前结点的流量 
bool vis[MAXN];//记录是否到达 

int read(){
	int s = 0, f = 0;
	char ch = getchar();
	while(!isdigit(ch))  f |= (ch == '-'), ch = getchar();
	while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
	return f ? -s : s;
}

void add_edge(int from, int to, int w){ e[++num_edge] = (edge){to, w, head[from]}, head[from] = num_edge; }

bool bfs(){
	memset(vis, false, sizeof vis);//初始化 
	queue<int> q;
	q.push(s);
	vis[s] = true, incf[s] = INF;//起点的流量是无限的 
	while(!q.empty()){
		int u = q.front(); q.pop();
		for(int i = head[u]; i; i = e[i].nxt){
			if(e[i].w){//如果还有流量 
				int v = e[i].to;
				if(vis[v]) continue;
				incf[v] = min(incf[u], e[i].w);//在u点的流量和能通过的流量中取最小值 
				pre[v] = i;//记录路径 
				q.push(v);//入队 
				vis[v] = true;//标记 
				if(v == t) return true;//碰到终点就退出 
			}
		}
	}
	return false;
}

void update(){
	int u = t;
	while(u != s){//不断回溯到起点 
		int i = pre[u];//找到经过的哪条边 
		e[i].w -= incf[t];//正边减掉用掉的流量 
		e[i ^ 1].w += incf[t];//反边加上用掉的流量,为“反悔 ”做准备 
		u = e[i ^ 1].to;//回溯到上一个点 
	}
	maxflow += incf[t];//加上新获得的流量 
}

signed main()
{
	n = read(), m = read(), s = read(), t = read();
	for(int i = 1, u, v, w; i <= m; ++i){
		u = read(), v = read(), w = read();
		add_edge(u, v, w), add_edge(v, u, 0);//正边流量为w,反边"可反悔"的流量为0 
	} 
	while(bfs()){//如果还能到达汇点 就继续 
		update();//更新答案 
	}
	printf("%lld", maxflow);
	return 0;
}

Dinic弧优化

Dinic 对算法的优化在于多路增广,弧优化是减少枚举边的次数

算法流程
1、主要思想是分层,利用bfs进行分层,记录一下该点所处的层数dep(我习惯用dis)
2、进行dfs多路增广,增广过程中要保证每次一定会转移到下一层
3、遇到终点进行回溯,回溯时顺便更新剩余流量
4、交替进行bfs和dfs,直到bfs不能到达汇点

时间复杂度:最劣 \(O(n^2m)\)
估计没有卡弧优化的了吧

/*
Work by: Suzt_ilymics
Knowledge: dinic弧优化
Time: O(n^2 m)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#define LL long long
#define int long long
#define orz cout<<"lkp AK IOI!"<<endl

using namespace std;
const int MAXN = 1e5+5;
const int INF = 1 << 30;
const int mod = 1e9+7;

struct edge{
	int to, w, nxt;
}e[MAXN << 1];
int head[MAXN], num_edge = 1;

int n, m, s, t;
int cur[MAXN], dis[MAXN];//cur是弧优化, dis表示分的层数(即分在了第几层 

int read(){
	int s = 0, f = 0;
	char ch = getchar();
	while(!isdigit(ch))  f |= (ch == '-'), ch = getchar();
	while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
	return f ? -s : s;
}

void add_edge(int from, int to, int w){ e[++num_edge] = (edge){to, w, head[from]}, head[from] = num_edge; }

bool bfs(){
	memset(dis, -1, sizeof dis);
	queue<int> q;
	q.push(s);
	dis[s] = 0, cur[s] = head[s];
	while(!q.empty()){
		int u = q.front(); q.pop();
		for(int i = head[u]; i; i = e[i].nxt){
			int v = e[i].to;
			if(dis[v] == -1 && e[i].w){
				q.push(v);
				cur[v] = head[v]; 
				dis[v] = dis[u] + 1;
				if(v == t) return true;
			}
		}
	}
	return false;
}

int dfs(int u, int limit){//多路增广 
	if(u == t) return limit;
	int flow = 0;
	for(int i = cur[u]; i && flow < limit; i = e[i].nxt){//第二个条件是 流量用要够用 
		cur[u] = i;//当前弧优化,保证每条边只增广一次
		int v = e[i].to;
		if(e[i].w && dis[v] == dis[u] + 1){//如果还有残量并且v点在下一层中 
			int f = dfs(v, min(e[i].w, limit - flow));//向下传递的流量是边限制的流量和剩余的流量去较小值 
			if(!f) dis[v] = -1;//如果没有流量了,标记为-1,表示不能经过了 
			e[i].w -= f;//正边减流量 
			e[i ^ 1].w += f;//反边加流量 
			flow += f;//汇点流量加上获得的流量 
		}
	}
	return flow;
}

int dinic(){
	int maxflow = 0, flow = 0;
	while(bfs()){
		while(flow = dfs(s, INF)){
			maxflow += flow;
		}
	}
	return maxflow;
}

signed main()
{
	n = read(), m = read(), s = read(), t = read();
	for(int i = 1, u, v, w; i <= m; ++i){
		u = read(), v = read(), w = read();
		add_edge(u, v, w), add_edge(v, u, 0);
	}
	printf("%lld", dinic());
	return 0;
}

基于Dinic的最小费用最大流

如果您平常在最大流中用 \(dis\) 分层的话,费用流接受起来一点也不难
因为相较于一般的最大流,费用流加上了花费的限制
想到SPFA是不断更新最短路的,考虑用它去替换最大流的bfs,

每次跑SPFA时,如果源点到某个结点最短距离可以更新并就将其加入队列中
(和SPFA跑最短路几乎相同,唯一不同的是更新的前提是还有流量)

同时,在dfs增广求到汇点的流量时
要注意只能在最短路上增广,
例:设一条边 \((u, v)\),这条路的花费是 \(c\)每单位流量,那么进行增广的充要条件是 \(dis_v == dis_u + c\)

统计费用:
dfs到汇点回溯过程中,统计经过当前边的花费
设当前边的花费是 \(c\) 每单位流量,通过当前边到达汇点的流量为 \(flow\),那么当前边对最大流最小花费的贡献显然是 \(c \times flow\)

注:费用流同样可以套上弧优化哦

/*
Work by: Suzt_ilymics
Knowledge: 最小费用最大流 (基于Dinic的实现 
Time: O(??)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<deque>
#define LL long long
#define orz cout<<"lkp AK IOI!"<<endl

using namespace std;
const int MAXN = 1e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;

struct edge{
	int to, w, cost, nxt;
}e[MAXN << 1];
int head[MAXN], num_edge = 1;

int n, m, s, t, res;
int dis[MAXN], cur[MAXN];
bool vis[MAXN];

int read(){
	int s = 0, f = 0;
	char ch = getchar();
	while(!isdigit(ch))  f |= (ch == '-'), ch = getchar();
	while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
	return f ? -s : s;
}

void add_edge(int from, int to, int w, int cost){ e[++num_edge] = (edge){to, w, cost, head[from]}, head[from] = num_edge; }

bool spfa(){
	bool flag = false;
	memset(dis, 0x3f, sizeof dis);
	memcpy(cur, head, sizeof head);//拷贝head数组到cur数组里 
	deque<int> q;//slf优化spfa 
	q.push_back(s);
	dis[s] = 0, vis[s] = true;
	while(!q.empty()){
		int u = q.front(); q.pop_front();
		vis[u] = false;
		for(int i = head[u]; i; i = e[i].nxt){
			int v = e[i].to;
			if(e[i].w && dis[v] > dis[u] + e[i].cost){//加了一个必须有剩余流量的条件 (其他都是正常SPFA最短路 
				dis[v] = dis[u] + e[i].cost;
				if(!vis[v]) {
					vis[v] = true;
					if(!q.empty() && dis[v] < dis[q.front()]) q.push_front(v);
					else q.push_back(v);
				}
				if(v == t) flag = true;
			}
		}
	}
	return flag;
}

int dfs(int u, int limit){
	if(u == t) return limit;//遇到汇点就返回 
	int flow = 0;
	vis[u] = true;
	for(int i = cur[u]; i && flow < limit; i = e[i].nxt){//指针:可以使cur[u]随i的变化而变化(为啥用指针会慢 
		cur[u] = i; 
		int v = e[i].to;
		if(!vis[v] && e[i].w && dis[v] == dis[u] + e[i].cost){//一个点只能访问一次,并且这条边还有流量,并且这个点在最短路上 
			int f = dfs(v, min(e[i].w, limit - flow));//继续dfs 
			if(!f) dis[v] = INF;//如果没有流量,说明这条路已经行不通了 
			res += f * e[i].cost;//统计答案 
			e[i].w -= f;//更新残量 
			e[i ^ 1].w += f;
			flow += f;
//			if(flow == limit) break;
		}
	}
	vis[u] = false;
	return flow;
}

int dinic(){
	int maxflow = 0, flow = 0;
	while(spfa()){
		while(flow = dfs(s, INF)){
			maxflow += flow;
		}
	}
	return maxflow;
}

int main()
{
	n = read(), m = read(), s = read(), t = read();
	for(int i = 1, u, v, w, cost; i <= m; ++i){
		u = read(), v = read(), w = read(), cost = read();
		add_edge(u, v, w, cost), add_edge(v, u, 0, -cost);
	}
	int ans = dinic();
	printf("%d %d\n", ans, res);
	return 0;
}

网络流24题(6/24)

飞行员配对方案问题

Description:P2756 飞行员配对方案问题
Solution:建两个超级源点,输出方案的时候看看哪条反向边有流量即可
Code

试题库问题

Description:P2763 试题库问题
Solution:交换起点跑两次最大流,如果都是满流说明没问题
Code

分配问题

Description:P4014 分配问题
Solution:先构建一个二分图模型,把人放一边,工作放一边,然后连出超级源点和超级汇点来。求最小效益,跑一个最小费用最大流即可;求最大效益,权值取反再跑一次
Code

骑士共存问题

Description:P3355 骑士共存问题
Solution:对棋盘进行黑白染色,发现每个m能攻击到的格子的颜色与自己的不同,于是被分成了一个二分图,跑二分图最大匹配即可。源点向黑点连边,白点向汇点连边。每个m向攻击到的格子连边。然后跑出来的最大流就是最大点覆盖集(或者说最小割?),答案就是 所有能放的格子-最大流
Code

负载平衡问题

Description:P4016 负载平衡问题
Solution:和HAOI2008糖果传递一模一样,可以直接用贪心过掉。
考虑一下网络流做法:最后的每个人的糖果数是总糖果数的平均值,我们建一个源点连向所有比平均值大的点,所有比平均值小的点连向汇点,每个点都可以想左右两个点连边,然后跑最大流即可(我也没跑过所以只有贪心代码)
Code

其他例题

普通板子类:
P2740 [USACO4.2]草地排水Drainage Ditches
P2936 [USACO09JAN]Total Flow S
P1343 地震逃生
二分图类:
P3386 【模板】二分图最大匹配
P2055 [ZJOI2009]假期的宿舍

posted @ 2021-02-18 17:09  Suzt_ilymtics  阅读(127)  评论(2编辑  收藏  举报