图论---网络流
前言
传说中的省选知识点
鲁迅曾经说过:“网络流代码只有普及-难度,主要看建模”
暂时只会最大流、费用流
Update 20210228:更新了算法分析
正文
蒟蒻只会网络流,学习的下面两篇文章
理解大体思路粘一个板子就差不多了
感觉解释在板子代码中挺清晰了
板子&算法分析
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]假期的宿舍