zoukankan      html  css  js  c++  java
  • 图论网络流

    前言

    传说中的省选知识点

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

    暂时只会最大流、费用流

    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]假期的宿舍

  • 相关阅读:
    JAVA中的for循环
    Android项目开发全程(三)-- 项目的前期搭建、网络请求封装是怎样实现的
    Android项目开发全程(二)--Afinal用法简单介绍
    Android项目开发全程(一)--创建工程
    Spring中的线程池和定时任务功能
    JVM内存分析
    并发基本概念介绍
    javax顶层接口分析
    HttpServlet源码分析
    SpringMVC创建HelloWorld程序
  • 原文地址:https://www.cnblogs.com/Silymtics/p/14412602.html
Copyright © 2011-2022 走看看