zoukankan      html  css  js  c++  java
  • 【学习笔记】网络流算法及其优化

    关于网络流

    网络最大流,简称网络流,是一种流量问题。

    用数学语言就是如下表示:

    给定原图 (G=(E,V)),要求出 (G'=(E',V')),满足:

    • (V'=V),即两图点集相同。
    • (forall ein E,exists e'in E' e_d=e'_d,e_s=e'_s,e_vge e'_v),即两图的结构相同,且 (G') 里的边权不大于 (G) 里的对应边的边权。
    • (exists s,tin V sumlimits_{ein E'\ e_t=s}e_v=0,sumlimits_{ein E'\e_s=t}e_v=0),即新图存在一个源点 (s),和一个汇点 (t),使得 (s) 入度为 (0)(t) 出度为 (0)
    • (forall vin V,v eq s,v eq t sumlimits_{ein E'\e_s=v}e_v=sumlimits_{ein E'\e_t=v}e_v),即对于所有非源点、汇点的点,带权入度与带权出度相等。

    求出 (max sumlimits_{ein E'\e_d=t}e_v)

    用通俗语言讲,就是一个水系统,在注满了水以后,从源点到汇点的最大流水量。

    如何求解网络流

    有两种主流方法:

    1. 增广路算法。指不断寻找能够增加流量的路径来求解。
    2. 预流推进算法。指通过模拟水流的推动过程来求解。

    一般来说,增广路算法有 EK,Dinic 和 ISAP。而预留推进算法有 HLPP 算法。

    在求解最短路时,通常采用 ISAP 或 HLPP 算法。但是 HLPP 算法常数较大,一般情况下都使用 ISAP 算法求解网络最大流。

    关于增广路算法

    关于增广路算法,有一下几个定义:

    反向边 (&) 残余网络

    定义 (e')(e) 的反向边,当且仅当 (e'_s=e_t)(e'_t=e_s)(e'_v)(e) 已经使用的流量。

    反向边的意义为执行贪心算法时,用于“反悔”。即就算选择了错误的增广路,也可以通过走反向边纠正。

    定义原图和原图每一条边的反向边的并集为原图的残余网络。

    增广路

    定义一条从源点到汇点的路径为最短路,当且仅当这条路径上每一条边的剩余流量不为零。

    增广路算法的基本逻辑

    即以下流程:

    1. 寻找一条或几条增广路,若找不到,退出算法;
    2. 将增广路上能增加的流量增加到整体流量上;
    3. 返回第一步。

    何为 ISAP 算法?

    ISAP (Improved Shortest Augment Path) 是 SAP 算法的优化。融合了 SAP 和 Dinic 的思想。是增广路算法中平均速度最快的算法。

    ISAP 在基本的增广路算法流程上,优化了寻找增广路的方法。

    基本操作如下:

    1. 进行一次从汇点到源点的反向宽搜,对原图分层。
    2. 从源点开始进行若干次深搜。深搜出来增广路,再将增广路上的节点层数 (+1)

    接下来我们想一想,为什么这样是正确的,以及为什么这么做效率高。

    注:下面的图片,若未注明,则编号最小的节点为源点,编号最大的节点为汇点。


    首先,你要找增广路。怎么去找呢?

    妈妈,我会 DFS!

    很好,于是你就去 DFS 找增广路了。

    顺带一提,这样的算法称作 Ford-Fulkson 算法

    直到有一天,你看到了这样一张图:

    G0aZbd.png

    不知道什么奇怪的原因,你搜到了 (1 ightarrow 2 ightarrow 3 ightarrow 4) 这条路径。

    接下来,你又搜了 (1 ightarrow 3 ightarrow 2 ightarrow 4)

    然后……(又经过了 (199998) 步后)……你终于搜完了。

    看起来没毛病……然而出题人看着很不爽。

    过了一天,出题人加强了数据。现在,这条边变得更粗♂了:

    G0dion.png

    真·硬核变粗

    结果,你的算法依旧出现了那个问题……出题人看你不爽,丢了一个 TLE 给你。

    Ford-Fulkson:卒。


    完了,这下该怎么办呢?

    妈妈,我会 BFS!

    女少口阿!利用 BFS 总是能够搜出最短路的 特性,我们就能够完美解决这个问题。

    这个算法叫做 Edmonds-Karp,或者是 SAP。

    就在你洋洋得意之时,出题人读透了你的心思,便给你丢了这么一张图:

    G001xK.png

    你的算法成功被卡到了 (mathcal{O}(n^2)),TLE。

    Edmonds-Karp:卒。


    此时,你的内心应该是崩溃的。

    啊啊啊,DFS 不行,BFS 不行,还能怎么办啊?

    难不成……要结合起来?

    Bingo!加一分。

    没错,我们确实要让 DFS 与 BFS 结合起来。那么,怎么结合呢?

    我们先来比较 DFS 与 BFS 的好坏:

    DFS BFS
    优点 占用空间少,速度快 可以搜出最短增广路
    缺点 可能因为增广路过长而导致重复 有时会被卡到全图遍历

    我们经过反复考量后,打算改造 DFS。

    为什么?因为 BFS 的结构导致一定会被卡到全图遍历。除非变成 A* 才能有优化。

    (额我是不是一不小心说漏了什么)

    那么,我们怎样才能让 DFS 跑的是最短增广路呢?我们考虑给图分层。

    每一次 BFS 都将这张图分层。接下来 DFS 只会在相邻两层之间跑来跑去。

    当两个节点之间没有剩余流量时,就视为不连通。

    这样,我们就能既有最短路,又能够避免全图遍历了。

    恭喜你,你已经想到了 Dinic 算法。Dinic 的核心就是上面的流程。

    当然,Dinic 还有很多细节上的优化,这个我们待会儿再讲。


    当然,某些毒瘤出题人看 Dinic 不爽,打算来卡一卡。

    这样的数据是有的,而且有的是。

    卡 Dinic 的核心就是让它不停地跑 BFS,导致整体复杂度趋近上限 (mathcal{O}(n^2m))

    所以,我们还需要一个究极算法,来拯救被毒瘤出题人蹂躏的 Dinic。

    就在这时,ISAP 横空出世,成功教那些出题人做人。

    ISAP 的核心优化就是将那个可恶的反复 BFS 干掉了,变成了只做一次 BFS 的事情。

    问题是,如何在只有一次 BFS 的情况下完成分层,并且一直维护这个分层呢?

    之前 Dinic 要一直跑 BFS 的原因是 DFS 无法维护。这个分层是连通与否的分层。

    ISAP 的分层则是在每一次找到一条增广路后,提高每一个点的层数。

    这样,就能依次走最短路,次短路,等等。就可以不用多次跑 BFS 了。

    出题人听到了这句话以后很开心,因为下面这张图你跑不过:

    G0dion.png

    ???为什么跑不过啊,因为你一次 DFS 只能找一条最短增广路。但是,最短增广路完全有可能不止一条。

    那么,我能不能在一次 DFS 中找到很多条最短增广路,以至于形成了一个增广路网呢?

    Bingo!再加一分。

    这样,你就成功解锁了基本版 ISAP。(当然,对于每一个增广路网上的节点,高度只需要增加 (1)

    问题是,出题人发现你的 ISAP 很弱,于是就来疯狂卡你。你不得不通过寻找优化来跟进速度。

    首先,如果你的 DFS 在某一个节点上没有跑满,也就是说还有节点可以往外灌,那么你这个节点还是有潜力的。也就是说你卷土重来时还是有机会的。

    那么,当你回来时,你是否还要访问之前那些边呢?实际上没有必要了,易证。

    那么,我们就可以记录一下当前这个节点遍历到哪一条出边,跑完后再重新开始。

    这个优化就成为 当前弧优化

    (Tip:这个优化也适用于 Dinic,有很大的速度提升。)

    同时,我们对于 ISAP 的高度很感兴趣。

    因为高度相邻时,两条边才能算作连通。那么如果高度出现了断层,那么不就可以洗洗睡了吗?

    所以,我们对于每一次 DFS 完毕后统计这个节点的高度增加后,会不会出现高度断层。出现了,就可以 over 了。

    这个优化就被成为 Gap 优化

    加上了这两个优化以后,我们的 ISAP 就所向披靡了。

    建议大家以后都用 ISAP 做网络最大流问题。常数小,基本不可能跑满,很少会卡,码量还很小。

    最后给大家放上模板题的代码:

    测试地址:(本题可以在洛谷上提交通过,没有吸氧)

    LG3376

    // @author 5ab
    
    /*
    变量解释:
    hd[],des[],val[],nxt[],edge_cnt:邻接表存图,不用解释,-1 代表末尾。
    occ[p]:代表边 p 有多少流量被使用。
    hei[i]:i 号点的高度。
    gap[i]:高度为 i 的点的数量。
    cur[i]:当前弧优化。
    flag:是否出现断层取反,即是否可以继续。
    */
    
    #include <queue>
    #include <cstdio>
    #include <cctype>
    #include <cstring>
    using namespace std;
    
    const int max_n = 10000, max_m = 100000, INF = 2147483647;
    
    int hd[max_n], des[max_m<<1], val[max_m<<1], occ[max_m<<1] = {}, nxt[max_m<<1], edge_cnt = 0;
    int hei[max_n], gap[max_n] = {}, cur[max_n];
    bool flag = true;
    
    queue<int> q;
    
    inline int my_min(int a, int b) { return (a < b)? a:b; }
    
    int aug(int s, int t, int lim) // 深搜找最短增广路
    {
    	if (s == t)
    		return lim;
    	if (!flag)
    		return 0;
    	
    	int tmp, flow = 0;
    	
    	for (int& p = cur[s]; p != -1; p = nxt[p])
    		if (hei[des[p]] == hei[s] - 1) // 高度差 1
    		{
    			tmp = aug(des[p], t, my_min(lim, val[p] - occ[p]));
    			flow += tmp, occ[p] += tmp, occ[p^1] -= tmp, lim -= tmp; // 正向边减权,反向边加权
    			
    			if (lim <= 0)
    				return flow;
    		}
    	
    	gap[hei[s]]--;
    	
    	if (!gap[hei[s]])
    		flag = false; // 检测断层
    	
    	hei[s]++, gap[hei[s]]++, cur[s] = hd[s];
    	
    	return flow;
    }
    
    inline int read()
    {
    	int ch = getchar(), n = 0, t = 1;
    	while (isspace(ch)) { ch = getchar(); }
    	if (ch == '-') { t = -1, ch = getchar(); }
    	while (isdigit(ch)) { n = n * 10 + ch - '0', ch = getchar(); }
    	return n * t;
    }
    
    void add_edge(int s, int t, int v)
    {
    	des[edge_cnt] = t, val[edge_cnt] = v;
    	nxt[edge_cnt] = hd[s], hd[s] = edge_cnt++;
    }
    
    int main()
    {
    	memset(hd, -1, sizeof(hd));
    	memset(hei, -1, sizeof(hei));
    	
    	int n = read(), m = read(), s = read() - 1, t = read() - 1, ta, tb, tc, ans = 0, eg;
    	
    	for (int i = 0; i < m; i++)
    	{
    		ta = read() - 1, tb = read() - 1, tc = read();
    		
    		add_edge(ta, tb, tc); // 正向边
    		add_edge(tb, ta, 0); // 反向边
    	}
    	
    	for (int i = 0; i < n; i++)
    		cur[i] = hd[i];
    	
    	hei[t] = 0, gap[0] = 1;
    	q.push(t);
    	
    	while (!q.empty()) // 宽搜分层
    	{
    		eg = q.front();
    		q.pop();
    		
    		for (int p = hd[eg]; p != -1; p = nxt[p])
    			if (hei[des[p]] == -1)
    			{
    				hei[des[p]] = hei[eg] + 1;
    				gap[hei[des[p]]]++;
    				
    				q.push(des[p]);
    			}
    	}
    	
    	while (flag)
    		ans += aug(s, t, INF); // 不停寻找最短增广路
    	
    	printf("%d
    ", ans);
    	
    	return 0;
    }
    

    后记

    如果你能够一字不落地看完整篇笔记,希望你也能感受到发现算法的乐趣。

    当然,如果你只是看完了代码,那么也恭喜你学会了一个新模板。

    如果你只是看了标题,那么谢谢你给我白嫖了阅读数

    经过了将近 (3) 个小时的时间,终于写完了这篇笔记。

    希望我的学习笔记能给你带来启发或是灵感。

  • 相关阅读:
    Oracle忘记用户名和密码以及管理员用户新增修改删除用户
    Oracle11.2安装和完全卸载及卸载后重装等问题
    软件测试之路2
    软件测试之路1
    Git入门笔记
    CentOS 6.5下二进制安装 MySQL 5.6
    十款压力测试工具
    tomcat 内存设置
    tomcat 安全
    tomcat 模式详解
  • 原文地址:https://www.cnblogs.com/5ab-juruo/p/note-network-flow.html
Copyright © 2011-2022 走看看