最大流算法之EK(最短路径增广算法)
这是网络流最基础的部分——求出源点到汇点的最大流(Max-Flow)。
最大流的算法有比较多,本次介绍的是其中复杂度较高,但是比较好写的EK算法。(不涉及分层,纯粹靠BFS找汇点及回溯找最小流量得到最终的答案)
EK算法,全名Edmonds-Karp算法(最短路径增广算法)。
首先简单介绍一下网络流的基本术语:
源点:起点。所有流量皆从此点流出。只出不进。
汇点:终点。所有流量最后汇集于此。只进不出。
流量上限:有向边(u,v)(及弧)允许通过的最大流量。
增广路:一条合法的从源点流向汇点的路径。
计算最大流就是不停寻找增广路找到最大值的过程。
合法的网络流具有以下性质:
1.f(i,j) <= c(i,j);//任意有向边的流量一定小于等于该边的流量限制
2.f(i,j) = -f(j,i);//从i流向j的流量与j流向i的流量互为相反数(反向边)
3.out(i) = in(i);//除源点、汇点外,任意一点i流入的流量与流出的相等(只是路过)
(截自洛谷)
EK算法思路:
1.通过BFS拓展合法节点(每个节点在本次BFS中仅遍历一次),找到汇点,并记录每个节点的前面节点(pre)(若找不到增广路,算法结束)
2.通过BFS的记录,从汇点回溯回源点,记录下每条弧流量的**最小值**minn, ans += minn(否则就会超出某条边的限制流量)
3.将所有经过的边的流量减去minn,反向边加上minn
4.重复上述步骤,直到找不到增广路,算法结束。
朴素版EK:
最为简单的写法,通过邻接矩阵存储。
优点:代码简单,一目了然。
缺点:轻易爆内存,(N^2)的空间太大,N >10000基本就废了(MLE)。
源代码:
#include <bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f
#define maxn 10005
int n, m, st, en, flow[maxn][maxn], pre[maxn];
int q[maxn], curr_pos, st_pos, end_pos;
bool wh[maxn];
int max_flow;
void Init()//初始化
{
int i, a, b, c;
scanf("%d%d%d%d", &n, &m, &st, &en);
for(i = 0; i != m; ++i)
{
scanf("%d%d%d", &a, &b, &c);
flow[a][b] += c;
}
return ;
}
bool Bfs(int st, int en)//广搜找源点
{
st_pos = -1, end_pos = 0;
memset(wh, 0, sizeof wh);
wh[st] = 1;
q[0] = st;
while(st_pos != end_pos)
{
curr_pos = q[++st_pos];
for(int i = 1; i != n+1; ++i)
{
if(!wh[i] && flow[curr_pos][i] > 0)
{
wh[i] = 1;
pre[i] = curr_pos;
if(i == en)
{
return true;
}
q[++end_pos] = i;
}
}
}
return false;
}
int EK(int start_pos, int end_pos)
{
int i, minn;
while(Bfs(start_pos, end_pos))//回溯
{
minn = INF;
for(i = end_pos; i != start_pos; i = pre[i])
{
minn = min(minn, flow[pre[i]][i]);
}
for(i = end_pos; i != start_pos; i = pre[i])
{
flow[pre[i]][i] -= minn;
flow[i][pre[i]] += minn;//反向弧加上该值(具体原因下文详解)
}
max_flow += minn;
}
return max_flow;
}
int main()
{
//freopen("test.in", "r", stdin);
//freopen("test.out", "w", stdout);
Init();
printf("%d", EK(st, en));
//fclose(stdin);
//fclose(stdout);
}
经过洛谷测评(P3376 【模板】网络最大流),50分,MLE五个点。
关于反向边加上最小流值的原因:
如果存在a->b有流,那么如果某一点c->b有流,则流量实际上可以再从b流到a,可以理解为把原本a->b的流量怼了回去。
BFS遍历得到的剩下部分的增广路的流量实际上是原本a->b的流,而原本a->b之后流向汇点的增广路的部分流量变为由c->b的流量。
增广路依旧合法。
这证明了邻接矩阵是不可行的,于是我们想到了麻烦一点但是省空间的存储方法——边表。至少从m*m优化到n*m,实则通常n*(m/10)便足够。
定义一个结构体
struct qi{
int st, en, num;//弧的始点,终点,权值
}flow[maxm];
相对应的广搜也要稍作调整,注意一下pre的存储方法:
pre[i]=k,i是点,k是弧的编号。
bool Bfs(int st, int en)
{
st_pos = -1, end_pos = 0;
memset(wh, 0, sizeof wh);
wh[st] = 1;
q[0] = st;
while(st_pos != end_pos)
{
curr_pos = q[++st_pos];
for(int i = 0; i < 2*m; ++i)
{
if(flow[i].st == curr_pos && flow[i].num > 0 && !wh[flow[i].en])
{
ne = flow[i].en;
wh[ne] = 1;
pre[ne] = i;
if(ne == en)
{
return true;
}
q[++end_pos] = flow[i].en;
}
}
}
return false;
}
所以Tracback的遍历循环也要改变。
for(i = end_pos; i != st; i = flow[pre[i]].st)//注意一下i如何取下一个的
{
minn = min(minn, flow[pre[i]].num);
}
事实证明:MLE的问题完美解决,测试结果:94ms , 18785kb,TLE三个点。
那么还需要优化一下时间。
注意BFS拓展时的循环:i:=0->2*m//m是弧的个数(因为有反向边,故是2*m)
由于题目范围是m <= 100000,每次最大循环达到2e5,太浪费了。
于是我们在边表再做一点手脚:开二维数组对每个始点所有的弧分别记录。
x[i][m]指以i为始点的第m条弧(再开一个num[i]记录每个始点弧的个数)。
那么代码就更加繁琐了。
首先是读入优化:
int read()
{
input = 0;
a = getchar();
while(a < '0' || a > '9')
a = getchar();
while(a >= '0' && a <= '9')
{
input = input*10+a-'0';
a = getchar();
}
return input;
}
然后是初始化:
void Init()
{
int i, a, b, c;
memset(num, -1, sizeof num);
n = read(), m = read(), st = read(), en = read();
for(i = 0; i != m; ++i)
{
flow[i].st = read();
flow[i].en = read();
flow[i].num = read();
re[flow[i].st][++num[flow[i].st]] = i;
re[flow[i].en][++num[flow[i].en]] = i+m;//初始化时将第i条弧的反向边编号为i+m
}
for(int i = m; i != 2*m; ++i)
{
flow[i].st = flow[i-m].en;
flow[i].en = flow[i-m].st;
flow[i].num = 0;
}
pre[st] = -1;
return ;
}
BFS部分:
bool Bfs(int st, int en)
{
int i, j;
st_pos = -1, end_pos = 0;
memset(wh, 0, sizeof wh);
wh[st] = 1;
q[0] = st;
while(st_pos != end_pos)
{
curr_pos = q[++st_pos];
for(i = 0; i < num[curr_pos]+1; ++i)
{
j = re[curr_pos][i];//当前可拓展节点的候选节点,即当前节点的第i条弧的终点
if(flow[j].st == curr_pos && flow[j].num > 0 && !wh[flow[j].en])
{
ne = flow[j].en;
wh[ne] = 1;
pre[ne] = j;
if(ne == en)
{
return true;
}
q[++end_pos] = flow[j].en;
}
}
}
return false;
}
Traceback只需改变反向边部分:
flow[pre[i]+m].num += minn;
主函数部分不需要调整。
最终测评结果:392ms , 56988kb,AC。
通过上述过程,我们得知网络流的存储结构应该为边表而非邻接矩阵。当然,使用vector来存储弧也是可以的,这样会更大程度上节省空间,但会对时间造成一定影响,根据实际情况自行取舍。
最后附上完整代码:
/*
Max_flow- EK
2017/04/07
While(BFS can find the end)
for i,i->j:=st -> end:
f[i][j] -= minn;f[j][i] += minn; ans += minn;//so we can delete at least one edge(the one which satisfies f[i][j] == minn)
*/
#include <bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f
#define maxm 200005
#define maxn 10005
struct qi{int st, en, num;}flow[maxm];
int n, m, st, en, pre[maxn], re[maxn][maxn/10], num[maxn];
int q[maxn], curr_pos, st_pos, end_pos, ne, max_flow;
bool wh[maxn];
int input;
char a;
int read()
{
input = 0;
a = getchar();
while(a < '0' || a > '9')
a = getchar();
while(a >= '0' && a <= '9')
{
input = input*10+a-'0';
a = getchar();
}
return input;
}
void Init()
{
int i, a, b, c;
memset(num, -1, sizeof num);
n = read(), m = read(), st = read(), en = read();
for(i = 0; i != m; ++i)
{
flow[i].st = read();
flow[i].en = read();
flow[i].num = read();
re[flow[i].st][++num[flow[i].st]] = i;
re[flow[i].en][++num[flow[i].en]] = i+m;
}
for(i = m; i != 2*m; ++i)
{
flow[i].st = flow[i-m].en;
flow[i].en = flow[i-m].st;
}
return ;
}
bool Bfs(int st, int en)
{
int i, j;
st_pos = -1, end_pos = 0;
memset(wh, 0, sizeof wh);
wh[st] = 1;
q[0] = st;
while(st_pos != end_pos)
{
curr_pos = q[++st_pos];
for(i = 0; i < num[curr_pos]+1; ++i)
{
j = re[curr_pos][i];
if(flow[j].st == curr_pos && flow[j].num > 0 && !wh[flow[j].en])
{
ne = flow[j].en;
wh[ne] = 1;
pre[ne] = j;
q[++end_pos] = flow[j].en;
if(ne == en)
return true;
}
}
}
return false;
}
int EK(int start_pos, int end_pos)
{
int i, minn;
while(Bfs(start_pos, end_pos))
{
minn = INF;
for(i = end_pos; i != st; i = flow[pre[i]].st)
{
minn = min(minn, flow[pre[i]].num);
}
for(i = end_pos; i != st; i = flow[pre[i]].st)
{
flow[pre[i]].num -= minn;
flow[pre[i]+m].num += minn;
}
max_flow += minn;
}
return max_flow;
}
int main()
{
//freopen("test.in", "r", stdin);
//freopen("test.out", "w", stdout);
Init();
printf("%d", EK(st, en));
//fclose(stdin);
//fclose(stdout);
}
自此,EK算法的讲解便结束了。
箜瑟_qi 2017.04.07 2:32
2017.04.21略加修改:
/*
Max_flow- EK
2017/04/07
*/
#include <bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f
#define maxm 200005
#define maxn 10005
struct Edge{
int st, en, num;
Edge(){}
Edge(int s, int e, int n):
st(s), en(e), num(n){}
}flow[maxm];
int n, m, st, en, pre[maxn], re[maxn][maxn/10], num[maxn];
int q[maxn], curr_pos, st_pos, end_pos, ne, max_flow;
bool wh[maxn];
int input;
char a;
static int read()
{
a = getchar();
input = 0;
while(a < '0' || a > '9') a = getchar();
while(a >= '0' && a <= '9')
{
input = input*10+a-'0';
a = getchar();
}
return input;
}
static void Init()
{
int i, a, b, c, cur;
memset(num, -1, sizeof num);
n = read(), m = read(), st = read(), en = read();
for(i = 0; i != m; ++i)
{
cur = 2*i;
a = read(), b = read(), c = read();
flow[cur] = Edge(a, b, c);
flow[cur^1] = Edge(b, a, 0);
re[flow[cur].st][++num[flow[cur].st]] = cur;
re[flow[cur].en][++num[flow[cur].en]] = cur^1;
}
return ;
}
static bool Bfs(int st, int en)
{
int i, j;
st_pos = -1, end_pos = 0;
memset(wh, 0, sizeof wh);
wh[st] = 1;
q[0] = st;
while(st_pos != end_pos)
{
curr_pos = q[++st_pos];
for(i = 0; i < num[curr_pos]+1; ++i)
{
j = re[curr_pos][i];
if(flow[j].st == curr_pos && flow[j].num > 0 && !wh[flow[j].en])
{
ne = flow[j].en;
wh[ne] = 1;
pre[ne] = j;
q[++end_pos] = flow[j].en;
if(ne == en)
return true;
}
}
}
return false;
}
static int EK(int start_pos, int end_pos)
{
int i, minn;
while(Bfs(start_pos, end_pos))
{
minn = INF;
for(i = end_pos; i != st; i = flow[pre[i]].st)
{
minn = min(minn, flow[pre[i]].num);
}
for(i = end_pos; i != st; i = flow[pre[i]].st)
{
flow[pre[i]].num -= minn;
flow[pre[i]^1].num += minn;
}
max_flow += minn;
}
return max_flow;
}
int main()
{
//freopen("test.in", "r", stdin);
//freopen("test.out", "w", stdout);
Init();
printf("%d", EK(st, en));
//fclose(stdin);
//fclose(stdout);
}
最大流算法之EK(最短路径增广算法)的更多相关文章
- 【BZOJ-3638&3272&3267&3502】k-Maximum Subsequence Sum 费用流构图 + 线段树手动增广
3638: Cf172 k-Maximum Subsequence Sum Time Limit: 50 Sec Memory Limit: 256 MBSubmit: 174 Solved: 9 ...
- 【Luogu】P3381最小费用最大流模板(SPFA找增广路)
题目链接 哈 学会最小费用最大流啦 思路是这样. 首先我们有一个贪心策略.如果我们每次找到单位费用和最短的一条增广路,那么显然我们可以把这条路添加到已有的流量里去——不管这条路的流量是多大,反正它能 ...
- python数据结构与算法——图的最短路径(Floyd-Warshall算法)
使用Floyd-Warshall算法 求图两点之间的最短路径 不允许有负权边,时间复杂度高,思路简单 # 城市地图(字典的字典) # 字典的第1个键为起点城市,第2个键为目标城市其键值为两个城市间的直 ...
- python数据结构与算法——图的最短路径(Dijkstra算法)
# Dijkstra算法——通过边实现松弛 # 指定一个点到其他各顶点的路径——单源最短路径 # 初始化图参数 G = {1:{1:0, 2:1, 3:12}, 2:{2:0, 3:9, 4:3}, ...
- python数据结构与算法——图的最短路径(Bellman-Ford算法)解决负权边
# Bellman-Ford核心算法 # 对于一个包含n个顶点,m条边的图, 计算源点到任意点的最短距离 # 循环n-1轮,每轮对m条边进行一次松弛操作 # 定理: # 在一个含有n个顶点的图中,任意 ...
- POJ 1273 - Drainage Ditches - [最大流模板题] - [EK算法模板][Dinic算法模板 - 邻接表型]
题目链接:http://poj.org/problem?id=1273 Time Limit: 1000MS Memory Limit: 10000K Description Every time i ...
- LOJ 2979 「THUSCH 2017」换桌——多路增广费用流
题目:https://loj.ac/problem/2979 原来的思路: 优化连边.一看就是同一个桌子相邻座位之间连边.相邻桌子对应座位之间连边. 每个座位向它所属的桌子连边.然后每个人建一个点,向 ...
- 数据增广imgaug库的使用
记录一下这两天用imgaug库做数据增广的代码,由于是算用算学的,所以只能把代码写出来,具体每种增广算法的原理和一些参数就不得而知了,不过我觉得也没必要把这么些个算法搜搞懂,毕竟重点是扩种数据.所以, ...
- 一般增广路方法求网络最大流(Ford-Fulkerson算法)
/* Time:2015-6-18 接触网络流好几天了 写的第一个模版————Ford-Fulkerson算法 作用:求解网络最大流 注意:源点是0 汇点是1 如果题目输入的是1到n 请预处理减1 * ...
随机推荐
- 【PHP实现】高效使用印象笔记之命令行快速保存
一.功能 脑袋中冒出一个想法时,命令行(Terminal)中输入一条命令快速保存到Evernote. 注:这里适用于保存简短的内容 不喜欢听絮叨的,直接文末找Github地址吧. 二.想法来源 一直使 ...
- 运行错误:应用程序无法启动因为并行配置不正确。the application has failed to start because its side-by-side configuration is incorrect 解决方法
问题描述: 当电脑同时安装VS2008和VS2008 SP1时,编译出来的Visual C++程序的manifest 文件会默认引用VS2008的MFC版本和CRT版本.如下: <depende ...
- iOS开发之数据存储之XML属性列表(plist)归档
1.概述 “归档”意思是持久化存储数据.plist文件是一种XML格式的文件,拓展名为plist.如果对象是NSString.NSDictionary.NSArray.NSData.NSNumber等 ...
- CSS.03 -- 浏览器行高、字体;盒子模型--边框、内边距、外边距
如果此时你也在自学中,请使用 FireWorks CS6 进行切图测距等,百度一下吧~ Fireworks的基本使用 新建文件 ctrl+n 打开文件 ctrl+o 调出和隐藏标尺 ctrl+r ...
- 关于微信小程序图片失真的解决方案
今天来说一说 关于微信小程序的图片失真问题的解决,微信小程序的image标签要设置其宽高,不然图片若宽高过大会撑开原始图片大小的区域:如下 但是宽高设置固定了会导致有些图片和规定显示图片大小的比例不一 ...
- 基于CDIF实现的——API在线自动化测试
传统的测试工具在测试一个API的时候,必须手动填写这个API所需要接收的所有信息,比如一个查询航班动态的API,他接收两个输入字段,一个叫flight, 一个叫date,那么测试这个API的用户,需要 ...
- 【2017-03-31】JS-DOM操作:操作属性、彩虹导航栏、定时器、操作内容、创建元素并添加、操作相关元素
一.操作属性 1.什么是属性: <div class="div" id="div1" style="" ></div> ...
- Java并发基础:进程和线程之由来
转载自:http://www.cnblogs.com/dolphin0520/p/3910667.html 在前面,已经介绍了Java的基础知识,现在我们来讨论一点稍微难一点的问题:Java并发编程. ...
- 文本挖掘预处理之向量化与Hash Trick
在文本挖掘的分词原理中,我们讲到了文本挖掘的预处理的关键一步:"分词",而在做了分词后,如果我们是做文本分类聚类,则后面关键的特征预处理步骤有向量化或向量化的特例Hash Tric ...
- Centos7部署Zabbix
转载于http://www.cnblogs.com/xqzt/p/5124894.html,更正了部分错误,并增加了个别问题处理办法. 一.Zabbix简介 zabbix是一个基于WEB界面的提供分布 ...