网络流的基础内容就不详细发了,网上到处都是,可自学。 总版点这里

ps:以下有些链接是hihocoder的题目(题面有详细讲解),请确保先登录hihocoder,再点击进入相应题目网页。

最大流

基础知识

EK算法

Dinic算法(接弧优化)

EK与Dinic算法的对比讲解

如果实在看不懂,这里简短地跟你说明白(本人已经好长一段时间没钻网络流了,这段是靠记忆手打的,如有误可以指正):

增广路径:从起点到终点的一条路径,该路径上所有边都没有满流。当残余网络中再没有增广路径时,也就是说原网络已经无法再往任何一条从起点到终点的路径中加流了,此时增广过的所有路径的流的总和就是最大流

普通dfs:dfs找到一条增广路径,增广这条路径后回溯,找新的增广路。(这个算法效率低是因为增广一条路径后,回溯时要从这条路径上的某个地方跳出该路径,改走残余网络中其它子路径到终点,从而形成新的增广路。但这条增广路前面一段已经被增广过,也就是说边的剩余容量普遍较低,从而导致回溯出新的增广路能新加的流较少,增广一次只能加少量流,因此所需的增广次数较多,增广效率低)

Ford-Fulkerson:这个方法跟普通dfs没多少区别,区别就是该算法增广一条路径后从起点重新找增广路。(但实际上它下一次找的增广路正常情况下还是按照上一次增广的路径走,只是找到第一条满流的边后改道,这跟普通dfs回溯到的位置是一模一样的……所以说这个方法效率也不高,也没什么人用)

EK:从起点开始bfs找增广路,当某条路径延伸到终点时,增广这条路径并从起点重新开始bfs找增广路。(bfs的优势就在于从每个点向所有边扩张,并更新从起点到每个点当前在一条路径中能通过的最大流量,这样从起点延伸到终点的流量是该残余网络中能通过流很大的增广路(目前还未想通是不是最大),这样增广一次就能加很多流,因此所需的增广次数较少,增广效率高。这就是为什么EK(bfs)面对10000个点的网络跑得飞快而dfs直接卡爆)

Dinic:用bfs给残余网络分层(建议从终点开始分层,原因在下文有补充),这里假设大家按照我的建议从终点开始分层,那么分层就可以理解为算出每个点到终点的最短距离。每次从起点开始,只能走邻层的点,这样可以确保增广路是残余网络的最短路径。

   Q:为什么要确保增广路是最短路径? A:上面我发的讲Dinic算法的那篇文章里有一个栏目叫“朴素算法的低效之处”,分层图就是解决那种例子的——优先让入终点的边满流(很显然从起点到终点的流必须得经过入终点的边),这样就不用担心像那个坑爹的例子一样,被多余的小流边把增广路的流卡得特别小,然后所需的增光次数就很多,拉低增广效率。

   按照走邻层的规则,用dfs找到一条增广路后,更新这条路径上的流,然后重新做残余网络的分层图,并回到起点重新用dfs找增广路。

   Q:那这里又为什么要用dfs找增广路? A:由于增广路上的流会被更新,因此至少会有一条边满流,不能出现在分层图中,因此每次增广一条路后要重新做分层图。如果只找一条不要求流量的增广路的话(要求邻层即可),用dfs一般情况下能直接搜出这条路径,效率较高。bfs的队列扩展就显得多余了。

模板题:luogu3376

2018.3.21补充:第一份30分普通dfs代码有问题(没有回溯到满流边之前,有可能把满流边再加流至溢出),但不知道为什么能过数据啊,难道是数据太水了?

 #include<bits/stdc++.h>
using namespace std;
inline int read(){
int x=,f=; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=-;
for(;isdigit(c);c=getchar()) x=(x<<)+(x<<)+c-'';
return x*f;
}
int n,m,s,t,fa[],faedge[],ans;
bool vis[];
struct edge{
int v,w,next;
}e[];
int head[],cnt;
inline void add(int w,int v,int u){
e[++cnt]=(edge){v,w,head[u]};
head[u]=cnt;
}
void dfs(int cur){
int i;
if(cur==t){
i=t;
int flow=;
while(i!=s) flow=min(flow,e[faedge[i]].w), i=fa[i];
i=t;
while(i!=s) e[faedge[i]].w-=flow, i=fa[i];
ans+=flow;
return;
}
vis[cur]=;
for(i=head[cur];i;i=e[i].next){
if(vis[e[i].v]) continue;
fa[e[i].v]=cur;
faedge[e[i].v]=i;
dfs(e[i].v);
}
vis[cur]=;
}
int main(){
n=read(),m=read(),s=read(),t=read();
for(int i=;i<=m;i++){
add(read(),read(),read());
}
dfs(s);
printf("%d\n",ans);
return ;
}

30分普通dfs(有问题)

30分普通dfs(已改正)
 #include<bits/stdc++.h>
#define maxn 10001
#define maxm 100001
#define inf 0x7f7f7f7f
using namespace std;
inline int read(){
int x=,f=; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=-;
for(;isdigit(c);c=getchar()) x=(x<<)+(x<<)+c-'';
return x*f;
}
int n,m;
struct edge{
int to,cap,flow,next;
}e[maxm<<];
int head[maxn],cnt,rhead[maxn];
int fa[maxn],faedge[maxn],flow[maxn];
struct EdmondsKarp{
inline void init(){
memset(head,-,sizeof head);
memset(rhead,-,sizeof rhead);
}
inline void addedge(int from,int to,int cap){
e[cnt]=(edge){to,cap,,head[from]};
head[from]=cnt++;
e[cnt]=(edge){from,,,rhead[from]};
rhead[from]=cnt++;
}
int maxflow(int s,int t){
int ans=,u,v,i;
while(){
memset(flow,,sizeof flow);
queue<int>q;
q.push(s);
flow[s]=inf;
while(!q.empty()){
u=q.front(); q.pop();
for(i=head[u];i!=-;i=e[i].next){
v=e[i].to;
if(!flow[v] && e[i].cap>e[i].flow){
fa[v]=u;
faedge[v]=i;
flow[v]=min(flow[u],e[i].cap-e[i].flow);
q.push(v);
}
}
if(flow[t]) break;
}
if(!flow[t]) break; i=t;
while(i!=s){
e[faedge[i]].flow+=flow[t];
e[faedge[i]^].flow-=flow[t];
i=fa[i];
}
ans+=flow[t];
}
return ans;
}
}aug; int main(){
int s,t,u,v,w;
n=read(),m=read(),s=read(),t=read();
aug.init();
for(int i=;i<m;i++){
u=read(),v=read(),w=read();
aug.addedge(u,v,w);
}
printf("%d\n",aug.maxflow(s,t));
return ;
}

100分EK(最慢点128ms)

 #include<bits/stdc++.h>
#define inf 0x7f7f7f7f
#define maxn 10001
#define maxm 100001
using namespace std;
inline int read(){
int x=,f=; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=-;
for(;isdigit(c);c=getchar()) x=(x<<)+(x<<)+c-'';
return x*f;
}
class Dinic_Enhancer
{
private:
struct edge{
int v,w,next;
}e[maxm<<];
int cnt,head[maxn];
int depth[maxn],cur[maxn];//cur就是记录当前点u循环到了哪一条边(弧优化)
public:
int n,m,s,t;
void init()
{
cnt=;
memset(head,-,sizeof(head)); n=read(),m=read(),s=read(),t=read();
int u,v,w;
for(int i=;i<m;i++){
u=read(),v=read(),w=read();
add_edge(u,v,w);
}
}
void add(int u,int v,int w)
{
e[cnt].v=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt++;
}
void add_edge(int u,int v,int w)
{
add(u,v,w);
add(v,u,);
}
int dfs(int u,int flow)
{
if(u==t) return flow;
for(int& i=cur[u];i!=-;i=e[i].next){ //注意这里的&符号,这样i增加的同时也能改变cur[u]的值,达到记录当前弧的目的
if(depth[e[i].v]==depth[u]+ && e[i].w)
{
int di=dfs(e[i].v,min(flow,e[i].w));
if(di>)
{
e[i].w-=di;
e[i^].w+=di;
return di;
}
}
}
return ;
}
int bfs()
{
queue<int> Q;
memset(depth,,sizeof(depth));
depth[s]=;
Q.push(s);
int i,u;
while(!Q.empty())
{
u=Q.front(); Q.pop();
for(i=head[u];i!=-;i=e[i].next)
if(depth[e[i].v]== && e[i].w>)
{
depth[e[i].v]=depth[u]+;
if(e[i].v==t) return ;
Q.push(e[i].v);
}
}
return ;
}
int dinic()
{
int ans=,i,flow;
while(bfs())
{
for(i=;i<=n;i++) cur[i]=head[i];//每一次建立完分层图后都要把cur置为每一个点的第一条边 感谢@青衫白叙指出这里之前的一个疏漏
while(flow = dfs(s,inf)) ans+=flow;
}
return ans;
}
}de; int main(){
de.init();
printf("%d\n",de.dinic());
return ;
}

100分Dinic(最慢点44ms)

考虑到ISAP算法网上没几个说得清楚的,我在这里单独说一下吧。

  ISAP的算法究竟在哪里做了优化呢?其实Dinic算法的弧优化给了我们很好的启示。既然能从上一次走过的边开始走,那我们就可以考虑一下是否可以每次dfs出一条增广路径后只更改单个点的层数(深度),而不用每次都bfs更新分层图。Dinic算法中,当我们沿着残量网络找到一条增广路增广后,残量网络肯定会变化(至少有一条边的流量满了,因此这条边相当于不在图中了),因此决定允许弧的d数组要进行相应的更新(Dinic 的做法就是每次增广后 都重新计算整个残量网络的d数组)。而ISAP改进的地方之一就是,其实没有必要马上更新d数组。这是因为,去掉一条边只可能令最短路径变得更长,而如果增广之前的残量网络存在另一条最短路,并且在增广后的残量网络中仍存在,那么这条路径毫无疑问是最短的。

  当残量网络中的一个点 与分层图中的所有邻层点连的边都满流 时,这时说明这个点到终点的最短路径都增广过了,要增广更长的路径了。因此要把它的弧指向剩下的未满流的边。但指向哪条呢?剩下的未满流的边指向的点都与这个点不是邻层的,由于当前走的一定是上一次残量网络的最短路径,因此 沿着这些不是邻层的点到终点的距离 一定是更长的,且连向这些不是邻层的点的边一定都未满流(可自行思考)。为了确保下一次仍然走的是残量网络中的最短路径,只要把弧指向 剩下的未满流的边指向的点 中 距离终点最近的一个,这样就走的是残量网络中的最短路径了。(具体证明可以自己接着意会一下)

  这样ISAP算法就只需要在最开始bfs一次,也就是给图分一次层就可以了。实际写代码的时候可以从终点开始分层(即终点深度为0),这样一个点的深度就是它到终点的最短距离了。当这个点所在的最短路都被增广过,也就是它 与分层图中的所有邻层点连的边都满流 时,把它的深度改为 所有与它相连的非邻层的点 中深度最低(即到终点距离最近)的加1 就可以了。其实从终点分层还有一个好处,见下面附录。

gap优化:如果你听说过ISAP算法也就应该听过gap优化。gap优化是个什么东西?可以这么说:

  gap优化可以提前结束程序,很多时候提速非常明显(高达 100 倍以上)。通过上面描述的ISAP算法,我们知道了可以通过不断更改满流边所连点的层数 来寻找新的增广路径。gap优化就是说,更改一个点u的层数时(设它的弧原来指向t,它原来的层数depth为d[u],点t原来的层数depth为d[t]),u,t之间的连通性会消失,但如果在这之前u是残量网络最后一个和t距离d[u](更新前)的点,那么此时源点和汇点也不连通了。从分层图的角度来说,两点之前连通时是邻层的,即d[u]-1=d[t],而如果残量网络中连接d[u]和d[t]两层的边只有这一条,那么当这条边的流量流完时(即需要更改点u的层数),整个网络的分层图就没有连接d[u]和d[t]两层的边了,而我们又知道只能走邻层点,且源点层数最大、汇点层数最小,因此此时源点和汇点就不连通了。gap优化的实现非常简单,用一个数组记录并在适当的时候判断要更改层数的点是否满足gap优化,若满足就结束程序即可。

 //ISAP
#include<bits/stdc++.h>
#define inf 0x7f7f7f7f
#define maxn 10001
#define maxm 100001
using namespace std;
inline int read(){
int x=,f=; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=-;
for(;isdigit(c);c=getchar()) x=(x<<)+(x<<)+c-'';
return x*f;
}
class ISAP
{
private:
struct edge{
int v,w,next;
}e[maxm<<];
int cnt,head[maxn];
int fa[maxn],faedge[maxn],num[maxn];
int depth[maxn],cur[maxn];//cur就是记录当前点u循环到了哪一条边(弧优化)
public:
int n,m,s,t;
void init()
{
cnt=;
memset(head,-,sizeof(head)); n=read(),m=read(),s=read(),t=read();
int u,v,w;
for(int i=;i<m;i++){
u=read(),v=read(),w=read();
add_edge(u,v,w);
}
}
void add(int u,int v,int w)
{
e[cnt].v=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt++;
}
void add_edge(int u,int v,int w)
{
add(u,v,w);
add(v,u,);
}
int bfs()
{
int i,u; queue<int> Q;
for(i=;i<=n;i++) depth[i]=n;
depth[t]=;
Q.push(t);
while(!Q.empty())
{
u=Q.front(); Q.pop();
for(i=head[u];i!=-;i=e[i].next){
if(depth[e[i].v]>depth[u]+ && e[i^].w) //注意ISAP中只做一次bfs,而Dinic要做若干次分层图。ISAP做bfs的目的不是做分层图,而是为了在主程序中从每个点到终点的最短距离开始计算路径,求的是每个点到终点的最短距离,因此可以不断更新;而Dinic的分层图是为了找到从起点到终点的最短路径,使在dfs过程中按照这条最短路径走,而不拐到最短路径外的点,所以分层图中每个点的层必须是第一次广搜遍历到的,不能做更改。这就是为啥这里判的是depth[e[i].v]>depth[u]+1
{
depth[e[i].v]=depth[u]+;
Q.push(e[i].v);
}
}
}
if(depth[s]>) return ;
return ;
}
int augment(){
int u=t,flow=inf;
while(u!=s) u=fa[u], flow=min(flow,e[cur[u]].w);
u=t;
while(u!=s) u=fa[u], e[cur[u]].w-=flow, e[cur[u]^].w+=flow;
return flow;
}
int isap()
{
int ans=,flow,u=s,i;
bfs();
for(i=;i<=n;i++) num[depth[i]]++, cur[i]=head[i];//每一次建立完分层图后都要把cur置为每一个点的第一条边 感谢@青衫白叙指出这里之前的一个疏漏
while(depth[s]<=n){
if(u==t){
ans+=augment();
u=s;
}
bool done=;
for(i=cur[u];i!=-;i=e[i].next){
if(depth[u]==depth[e[i].v]+ && e[i].w){
fa[e[i].v]=u;
cur[u]=i; //记录当前弧!在回溯边时也可以用!
done=;
u=e[i].v;
break;
}
}
if(!done){
int mn=n;
for(i=head[u];i!=-;i=e[i].next)
if(e[i].w) mn=min(mn,depth[e[i].v]);
if(--num[depth[u]]==) return ans;
num[depth[u]=mn+]++;
cur[u]=head[u];
if(u!=s) u=fa[u]; //一定要返回!因为这个点之前和前一个点在一条增广路上,两点深度相邻,而修改了这个点的深度后这个点和前一个点的深度就不相邻了(前一个点到起点的路径深度依然连续相邻),为了维护路径深度的连续性,要回到前一个点去找其他路径(第一次交这个代码没写这行,改完这个点的深度就接着从这条边往下走了,白搜了无数次)
}
}
return ans;
}
}ISAP; int main(){
ISAP.init();
printf("%d\n",ISAP.isap());
return ;
}

100分ISAP(最慢点20ms)

补充:Dinic和ISAP算法中 从终点开始分层的好处(有人可能不知道,不知道的话一定要看看)

作为一个资深OIer的你应该知道为什么做分层图时,要从终点开始分层。如果不知道,你画个图yy一下就明白了。

从起点开始分层标号后的分层图如上。那么你的程序从起点开始搜索增广路时,就可能先搜索长的那条路,即1-2-3-4-3。此时得走到4-3边时,程序才发现这条路径上有两个相邻点不是邻层(这里的邻层:后一个点的深度是前一个点的+1。不能逆过来),从而返回。但是这条更长的子路都走完了才发现它不是最短(即路径的最后两个点不满足深度递增1),假如有一组黑数据,把上面这张图中的长路径改得更长(比如经过十万八万个点的),那把这条路再走一遍,增广过程就退化成dfs寻找从起点到终点了,效率还不低死?有没有什么办法能让它在进入路径的地方就判断这条子路是不是最短(即是不是当前图的增广路的一部分)呢?

当然有了。从终点开始分层标号就好了。

此时,从起点出发的话,上面那条更长路径刚一进去就发现两个点不邻层了(这里的邻层:后一个点的深度是前一个点的-1。不能逆过来),从而立刻判断这条路不是最短,也就不是增广路中的一部分,从而退回去寻找其他路径。这样整个图每次成功前进时(即不退回)都走的是最短路,那么就能一次沿最短路走到终点,这条最短路也就是增广路了。

相信大家都已经理解了吧!

最小割最大流

讲解

【2018.2.8-】网络流学习笔记(含ISAP!)的更多相关文章

  1. 2018/2/14 设计模式学习笔记(一) 自己实现ArrayList,LinkedList和Iterator,以及在此过程中对于面向对象,面向接口,还有抽象类的一些思考感悟

    因为本人目前为止学习编程不过七个月,所以后面的感悟对于一些大神来说可能嗤之以鼻,但对于一些刚刚入门的萌新来说在理解面向对象的思想上,以及抽象类和接口应该怎么设计等方面应该还是会有所帮助的 首先我们定义 ...

  2. 2018/2/13 ElasticSearch学习笔记三 自动映射以及创建自动映射模版,ElasticSearch聚合查询

    终于把这些命令全敲了一遍,话说ELK技术栈L和K我今天花了一下午全部搞定,学完后还都是花式玩那种...E却学了四天(当然主要是因为之前上班一直没时间学,还有安装服务时出现的各种error真是让我扎心了 ...

  3. 2018/1/19 Netty学习笔记(一)

    这段时间学了好多好多东西,不过更多是细节和思想上的,比如分布式事物,二次提交,改善代码质量,还有一些看了一些源码什么的; 记录一下真正的技术学习,关于Netty的学习过程; 首先说Netty之前先说一 ...

  4. 2018/1/28 RocketMq学习笔记

    RocketMq是支持Topic模式的MQ中间件,它的传输格式为topic(主题,一个product对应一个主题,),Tag(标签,其实就是副标题,是为了更好的支持集群模式而出现的,这样客户端可以指定 ...

  5. 2018/1/9 redis学习笔记(一)

    本文不涉及redis基本命令以及javaapi的解释操作; 首先介绍下redis,一个nosql非关系型数据库,运行在缓存中,特点就是可存储的数据结构类型很多,做为KEY-VALUE数据库,它的键只能 ...

  6. $2018/8/19 = Day5$学习笔记 + 杂题整理

    \(\mathcal{Morning}\) \(Task \ \ 1\) 容斥原理 大概这玩意儿就是来用交集大小求并集大小或者用并集大小求交集大小的\(2333\)? 那窝萌思考已知\(A_1,A_2 ...

  7. 2018/3/14 Hadoop学习笔记(一)

    首先,什么是Hadoop?为什么它是现在大数据处理最热门的框架呢?(正确来说,现在Hadoop是一个生态圈) Hadoop是apache下一套开源的服务框架,它主要的作用就是利用服务器集群,来对海量数 ...

  8. RxJava2.0学习笔记2 2018年7月3日 周二

    摘记: 1.map -- 转换  有些服务端的接口设计,会在返回的数据外层包裹一些额外信息,这些信息对于调试很有用,但本地显示是用不到的.使用 map() 可以把外层的格式剥掉,只留下本地会用到的核心 ...

  9. 深度学习中的序列模型演变及学习笔记(含RNN/LSTM/GRU/Seq2Seq/Attention机制)

    [说在前面]本人博客新手一枚,象牙塔的老白,职业场的小白.以下内容仅为个人见解,欢迎批评指正,不喜勿喷![认真看图][认真看图] [补充说明]深度学习中的序列模型已经广泛应用于自然语言处理(例如机器翻 ...

随机推荐

  1. Windwos下安装和使用MongoDB

    1)下载安装包下载路径:https://www.mongodb.com/download-center#community包名称:mongodb-win32-x86_64-3.4.10-signed. ...

  2. selenium-Python之鼠标事件

    通过click()来模拟鼠标的单击操作,鼠标还具有鼠标右击,双击,悬停甚至鼠标拖动等功能.在webdriver中,将这些鼠标操作方法封装在ActionChains类提供. ActionChains类提 ...

  3. python 相关编码[转]

    python 相关编码[转]   三篇文章,导航:(一)(二)(三) (一) 怎么避免UnicodeEncodeError: ‘ascii’ codec can’t…类似的错误? 1.首先在py文件头 ...

  4. ARC和MRC混合模式下的编译问题

    在一个支持ARC (Automatic Reference Counting)的项目中,有时候需要禁止其中几个文件使用ARC模式编译(比如你用了第三方不支持ARC的类库).这时就要点击工程文件,在ta ...

  5. IOS7.1 企业应用 证书无效 已解决

    http://www.cocoachina.com/bbs/read.php?tid=194213&keyword=7.1 关于IOS7.1企业版发布后,用户通过SARAFI浏览器安装无效的解 ...

  6. Scrapy-架构

    Scrapy架构(各组件的功能)及Scrapy引擎控制数据流的过程 1. Scrapy架构图(绿线是数据流向): □ Scrapy引擎(Engine):引擎负责控制数据流在系统的所有组件中流动,并在相 ...

  7. Android之父Andy Rubin:被乔布斯羡慕嫉妒的天才

    今年中国掀起一股“苹果热”,智能手机iPhone.平板电脑iPad遭疯抢,一度卖断货.然而,令许多人意想不到的是,在“苹果”的老家——美国市场,智能手机中卖得最火的并不是iPhone,而是Androi ...

  8. Sql Server 自动备份

    1)启动代理服务 服务->Sql server 代理启动 2)设置维护计划 维护计划->设置维护计划向导->修改名称及说明 3)更改计划 4)选择维护任务 5)顺序调整不做解释 6) ...

  9. MySQL数据库安全配置

    文章来源:http://www.xfocus.net MySQL数据库安全配置 1.前言 MySQL 是完全网络化的跨平台关系型数据库系统,同时是具有客户机/服务器体系结构的分布式数据库管理系统.它具 ...

  10. POI把html写入word doc文件

    直接把Html文本写入到Word文件 获取查看页面的body内容和引用的css文件路径传入到后台. 把对应css文件的内容读取出来. 利用body内容和css文件的内容组成一个标准格式的Html文本. ...