『正睿OI 2019SC Day5』
<更新提示>
<第一次更新>
<正文>
网络流
网络流的定义
一个流网络\(G=(V,E)\)为一张满足以下条件的有向图:
- 每一条边有一个非负容量,即对于任意\(E\)中的\((u,v)\) , 有\(c(u,v)\geq0\)。
- 如果\(G\)中存在边\((u,v)\) ,那么不存在\((v,u)\) 。我们将图中不存在的边的容量定为\(0\)。
- 图中含有两个特殊节点:源\(s\)与汇\(t\)。
一个流\(f\)是定义在节点二元组\((u\in V,v\in V)\)上的实数函数,满足以下两个个性质:
- 容量限制:对于任意\((u,v)\),满足\(0\leq f(u,v)\leq c(u,v)\)。
- 流量守恒:对于任何非源汇的中间节点\(u\),有\(\sum_{v\in V}f(v,u)=\sum_{v\in V}f(u,v)\)
一个流\(f\)的流量\(|f|\)定义为:\(|f|=\sum_{v\in V}f(s,v)-\sum_{v\in V}f(v,s)\)。
最大流问题
定义
由于图\(G\)中不存在反向边,所以在我们一般只关注流量定义式的前半部分,即:\(|f|=\sum_{v\in V}f(s,v)\)。
那么对于一个网络\(G\),我们称\(\max\{|f|\}=\max\{\sum_{v\in V}f(s,v)\}\)为这个网络的最大流。
预备知识
残量网络:对于网络\(G\),其残量网络\(G_f\)与\(G\)的差别在于每条边的边容量修改为\(G\)中边容量减去当前流的该边流量。具体来说,\(c_f(u,v)=c(u,v)-f(u,v)\)。
另外,残量网络中还包含原图中所有边的反向边,容量等同于正向边在\(f\)中当前流量,用于"反悔"时将流送回起点:\(c_f(v,u)=f(u,v)\)。
简单的理解,残量网络就是原网络流了一股流以后剩下的网络,容量也对应的相减。
而反向边的存在,就给了反悔旧流的机会,也就是说,一股新的流,可以沿反向边流过,代表的涵义就是让之前流这条边的流不再流这条边,而是流向另一个方向。这样我们也就能够理解为什么反向边的容量就是原来的流量了。
增广:设\(f\)为网络\(G\)上的一个流,\(f'\)为残量网络\(G_f\)上的一个流,那么定义增广后的网络为:
\]
引理\(1\):增广后网络的流量等于两个流量直接相加,即:\(|f↑ f'|=|f|+|f'|\)。
这个引理为我们之后的最大流算法铺垫了基础,涵义即为我们得知了残量网络上的一次增广操作可以直接由原流量和增广流量计算得到新的网络流量。
增广路:残量网络中从\(s\)到\(t\)的一条简单路径定义为一条增广路,增广路的流量\(c_f(p)\)定义为\(\min\{c_f(u,v)|(u,v)\in p\}\)。
结论\(1\) :增广后流量增加。令\(f_p\)为残量网络\(G_f\)上的一条增广路,则有:\(|f↑ f_p|=|f|+|f_p|>|f|\)。
有了结论\(1\),我们就可以尝试思考如何设计求解网络最大流的算法了。我们得知,只要找到一条增广路,就能增加原网络的流量,并且可以快速计算出新的流量。所以,最初的想法就是不断地在残量网络中找增广路,不断扩大流量。
同时,我们也得知:当残量网络不存在增广路时,原网络的流量即为最大流。
Ford-Fulkerson算法
由预备知识可知,我们有一种最简单的求网络最大流的方法,那就是不断寻找残量网络的增广路,并将增广路的流量累加到答案中。直到残量网络不存在增广路,我们就得到了网络最大流。
于是我们就得到了著名的\(Ford-Fulkerson\)算法,容易写出如下的代码:
function Ford-Fulkerson(G,s,t)
maxflow = 0
for each edge (u,v) belongs to G.E
(u,v).f = 0
while there exists a path p from s to t in the residue network Gf
cf(p) = min { cf(u,v) | (u,v) belongs to p }
maxflow = maxflow + cf(p)
for each edge (u,v) belongs to p
if (u,v) belongs to E
(u,v).f = (u,v).f + cf(p)
else (v,u).f = (v,u).f - cf(p)
return maxflow
而如何找增广路呢?最简单的方法就是\(dfs\),每一次的时间复杂度\(O(m)\)。于是\(Ford-Fulkerson\)算法的时间复杂度就是\(O(m|f_{max}|)\),\(f_{max}\)代表网络\(G\)的最大流。
Edmonds-Karp算法
直接找增广路太暴力了,于是我们想到要对\(FF\)算法进行一些优化。我们发现\(FF\)算法最大的瓶颈就是增广次数太多,那么是否存在一种增广方法,使得增广的次数得到限制呢?答案是肯定的。
我们改造\(FF\)算法,每一次寻找最短路径增广路,就能在不超过\(nm\)次增广后得到网络的最大流。
引理\(2\):按照最短路径增广路增广,每次使所有顶点\(v∈V−
\{s,t\}\)到\(s\)的最短距离\(d_v\)增大。
证明:
反证法,假设存在点\(v\in V-\{s,t\}\)使得\(d'_v<d_v\)。那么取\(v\)为第一个成立的节点,并且令\(u\)为\(v\)在最短路径上的前驱节点。由此我们可以得到:
\]
若边\((u,v)\in E\),则有\(d_v\leq d_u+1\leq d'_u+1=d'_v\),与假设矛盾。
若边\((u,v)\not\in E\),则边\((v,u)\)在增广路上,有\(d_v=d_u-1\leq d'_u-1=d'_v-2\),与假设矛盾。
结论\(2\):按照最短路径增广路增广,每条边最多作为瓶颈边\(\frac{n}{2}-1\)次。
证明:
如果\((u,v)\)是瓶颈边,则\((u,v)\)在\(s\)到\(t\)的增广路上,有\(d_v=d_u+1\)。而增广后,\((u,v)\)将会从残量网络中消失,若边\((u,v)\)重新出现,当且仅当\((v,u)\)在增广路上,而此时又有\(d'_v=d'_u+1\)。
由引理可知,\(d'_v>d_v\),故有\(d'_u\geq d_v+1=d_u+2\)。所以每次重新出现会使最短路的最短距离\(+2\),而最短距离最大为\(n-2\),所以每条边最多作瓶颈边\(\frac{n}{2}-1\)次。
由结论\(2\)我们就能得知,这样增广的总次数不会超过\(nm\)次,如果采用\(bfs\)实现找最短路径增广路,时间复杂度为\(O(nm^2)\),我们称这种最大流算法为\(Edmonds-Karp\)算法。
\(Code:\)
inline bool EdmondsKarp(void)
{
memset( vis , 0x00 , sizeof vis );
queue < int > q; q.push( s );
vis[s] = true , Min[s] = INF;
while ( !q.empty() )
{
int x = q.front(); q.pop();
for (int i=Head[x];i;i=e[i].next)
{
if ( !e[i].val ) continue;
int y = e[i].ver;
if ( vis[y] ) continue;
Min[y] = min( Min[x] , e[i].val );
pre[y] = i;
q.push( y ) , vis[y] = true;
if ( y == t ) return true;
}
}
return false;
}
inline void update(void)
{
int x = t;
while ( x != s )
{
int i = pre[x];
e[i].val -= Min[t];
e[i^1].val += Min[t];
x = e[i^1].ver;
}
maxflow += Min[t];
}
int main(void)
{
input();
while ( EdmondsKarp() ) update();
printf("%d\n",maxflow);
return 0;
}
dinic算法
我们不妨对\(EK\)算法的增广过程进行思考,发现\(EK\)算法的本质就是每次在残量网络上构建最短路树,然后找到一条增广路进行增广。其实,这当中不难发现\(EK\)算法还有优化的余地。
在残量网络构建的最短路树当中,很可能存在多条增广路,而\(EK\)算法却每次只增广一条,就重新构建最短路树了。那么,我们能否设计一个算法,在一次构建最短路树以后实现多路增广,同时处理掉所有的增广路呢?
可行网络:在残量网络上由最短路树构成的子网络我们称为可行网络。
阻塞流:在可行网络上无法在扩充的流称为阻塞流。阻塞流不必要是残量网络的最大流。
利用上述的思路,我们每次构建残量网络的可行网络(最短路树),并用\(dfs\)实现多路增广,直接增广掉可行网络的阻塞流,就能得到一个更高效的算法,我们称之为\(dinic\)算法。
\(Code:\)
inline bool Search(void)
{
memset( d , 0x00 , sizeof d );
memcpy( cur , Head , sizeof Head );
queue < int > q; q.push( s );
d[s] = 1;
while ( !q.empty() )
{
int x = q.front(); q.pop();
for (int i=Head[x];i;i=e[i].next)
{
int y = e[i].ver;
if ( e[i].val && !d[y] )
{
d[y] = d[x] + 1;
q.push( y );
if ( y == t ) return true;
}
}
}
return false;
}
inline int dinic(int x,int flow)
{
if ( !flow || x == t ) return flow; // 剪枝1
int residue = flow;
for (int i=cur[x];i;i=e[i].next)
{
int y = e[i].ver; cur[x] = i; // 剪枝2
if ( e[i].val && d[y] == d[x] + 1 )
{
int k = dinic( y , min( residue , e[i].val ) );
if ( !k ) d[y] = 0; // 剪枝3
e[i].val -= k , e[i^1].val += k;
residue -= k;
if ( !residue ) break; // 剪枝4
}
}
return flow - residue;
}
int main(void)
{
input();
while ( Search() )
maxflow += dinic( s , INF );
printf("%d\n",maxflow);
return 0;
}
可以发现,除了之前提到的算法流程外,我们还在\(dinic\)函数中加了若干剪枝,其中最重要的剪枝为:
\(1.\) 当前弧优化(剪枝\(2\)):不增广同一条边多次,每次记录增广到的最后一条边。
\(2.\) 无效点优化(剪枝\(3\)):对于一个流入流量却没有有效流出任何流量的点,我们不再重复访问。
可以证明,当我们在\(dinic\)算法中加入了如上\(4\)个剪枝后,\(dinic\)算法的时间复杂度为\(O(n^2m)\),实际运行速度则更快。
于是,我们就得到了实现网络最大流最简单而又高效的算法。
其他增广路算法
我们发现,之前我们提到的三种最大流算法都基于一个最基础的思想:寻找增广路。其实,寻找增广路的算法还有一种:\(ISAP\)算法。
\(ISAP\)算法是\(SAP\)算法的优化,和\(EK\)算法的思路基本相同,不过只需要进行一次\(bfs\)。虽然\(ISAP\)算法的时间复杂度理论上界同样是\(O(n^2m)\),但是通常来说会有更好的表现。
这里将不再详细介绍\(ISAP\)算法,具体可以参照这篇博客。
预流推进算法
我们之前一直都在围绕增广路算法进行讨论,其实,网络最大流问题还有另一种思路的算法,叫做预流推进算法。
预留推进算法的思想很简单,首先假设源点\(s\)有无限多的余流,然后不断将余流推送给相邻的节点,对于其他的点也是同理,直到不能推送了,\(t\)点的余流即为最大流。
预流推进算法中比较优秀的一种叫做\(HLPP\)算法,时间复杂度的理论上界为\(O(n^2\sqrt m)\),这里不再详解,可以参照这篇博客。
最小割问题
定义
割:一个对点集\(V\)的划分称为割,其中点集被划分为两部分\(S\),\(T\),源\(s\)在\(S\)中,汇\(t\)在\(T\)中。对于一个流\(f\)而言,割\((S,T)\)的流定义为:$$f(S,T)=\sum_{u\in S}\sum_{v\in T}f(u,v)-\sum_{u\in S}\sum_{v\in T}f(v,u)$$
割\((S,T)\)的容量定义为:\[c(S,T)=\sum_{u\in S}\sum_{v\in T}c(u,v)
\]
那么,对于一个网络\(G\),我们称\(\min\{c(S,T)\}\)为这个网络的最小割。
预备知识
引理\(3\):对于任意流\(f\),任意割的网络流量不变,即\(f(S,T)=|f|\)。
由流量守恒我们得知这条引理的正确性,而有了这一条引理,我们就可以进一步地推导流与割之间的联系,找到解决最小割问题的算法。
结论\(3\):任意流\(f\)的流量不超过任意割的容量,即\(|f|\leq c(S,T)\)
证明:
\]
根据结论\(3\),我们得知了割与流之间的整体关系。我们也发现,流的流量和割的容量可能存在重合点(两值相等),此时最小割等于最大流,那么我们能否证明更一般的结论呢?
最大流最小割定理
最大流最小割定理:对于一个网络\(G\),以下三个命题总是等价的:
- 流\(f\)是\(G\)的最大流
- 当前流\(f\)的残量网络\(G_f\)上不存在增广路
- 存在某个割使得\(|f|=c(S,T)\)成立,此时割\((S,T)\)即为网络\(G\)的最小割
证明:
\(Part1\ (1)⇒(2):\)
反证法,若当前流\(f\)的残量网络\(G_f\)上存在增广路,则由结论\(1\)可知经过这次增广流量\(|f|\)可以增大,与流\(f\)是\(G\)的最大流矛盾。
\(Part2\ (2)⇒(3):\)
构造点集\(S\)为源\(s\)在残量网络上能够到达的点的集合,\(T=V-S\),那么汇\(t\)在\(T\)中,进而\((S,T)\)时原网络\(G\)的一个割。
考虑割\((S,T)\)间的任意点对\((u,v)\),若\((u,v)\)在原网络中存在,则必然有\(f(u,v)=c(u,v)\),否则与残量网络\(G_f\)不存在增广路矛盾,若\((v,u)\)在原网络中存在,则必然有\(f(v,u)=0\),否则这股流无法回流(由上可知,只存在满流边)。
那么就有:
\]
那么由引理\(3\)可知:\(|f|=c(S,T)\),结论成立。
\(Part3\ (3)⇒(1):\)
由结论\(3\)可知:\(|f|\leq c(S,T)\),那么当等号成立时,所对应的流\(f\)必然是最大流。
于是,我们就得到了有关最小割问题最重要的定理,那么最小割问题就可以转换为最大流问题解决。
最大权闭合子图问题
最大权闭合子图:给出一张有向图,每个点都有一个点权。在有向图中选取一张权值最大的子图,使得每个节点的后继也都在子图中。
最大权闭合子图问题较难直接求解,但是我们可以将其转换为最小割问题,这是最小割问题最经典的一个运用。
我们建立超级源点\(s\),\(s\)连所有正权点,容量为点权,建立超级汇点\(t\),\(t\)连所有负权点,容量为点权的相反数。对于原图中的所有边\((u,v)\),连接\((u,v)\),容量为\(\infty\)。然后我们在图上求最小割,答案即为正权点权值和\(-\)最小割。
考虑最小割\((S,T)\),那么最大权闭合子图的节点即为\(S\)中的节点。首先,对于一条容量为正无穷的边\((u,v)\),它一定不会选在最小割中,这就保证了每个点和它的后继被分在同一个集合中。假设一开始所有正权点都在我们选中的最大权闭合子图中,那么:
- 对于一个正权点\(u\),如果割掉\((s,u)\),就代表不把这个点选入闭合子图,答案刚好减去\(val_u\)
- 对于一个负权点\(v\),如果割掉\((v,t)\),就代表把这个点选入闭合子图中,答案加上\(val_v\),刚好减去\(-val_v\)
至此,最大权闭合子图问题也得到了较好的解决。
\(Code:\) \((\)\(NOIP2009\)植物大战僵尸\()\)
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
const int N = 35 , INF = 0x7f7f7f7f;
struct edge { int ver,val,next; } e[2*N*N*N*N+6*N*N];
int n,m,S,T,s[N][N],c[N][N],indeg[N*N],flag[N*N];
int tot=1,maxflow,Head[N*N],d[N*N],cur[N*N],ans;
pair < int , int > a[N][N][N*N];
vector < int > Link[N*N];
inline int id(int p,int q) { return ( p - 1 ) * m + q; }
inline int read(void)
{
int x = 0 , w = 0; char ch = ' ';
while ( !isdigit(ch) ) w |= ch=='-' , ch = getchar();
while ( isdigit(ch) ) x = x*10 + ch-48 , ch = getchar();
return w ? -x : x;
}
inline void input(void)
{
n = read() , m = read();
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
{
s[i][j] = read() , c[i][j] = read();
for (int k=1;k<=c[i][j];k++)
{
a[i][j][k].fi = read() , a[i][j][k].se = read();
++ a[i][j][k].fi , ++ a[i][j][k].se;
Link[id(i,j)].push_back
( id(a[i][j][k].fi,a[i][j][k].se) );
indeg[ id(a[i][j][k].fi,a[i][j][k].se) ] ++;
}
if ( j < m )
Link[id(i,j+1)].push_back( id(i,j) ) , indeg[id(i,j)] ++;
}
}
inline void insert(int x,int y,int v)
{
e[++tot] = (edge){y,v,Head[x]} , Head[x] = tot;
e[++tot] = (edge){x,0,Head[y]} , Head[y] = tot;
}
inline void Topsort(void)
{
queue < int > q;
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
if ( !indeg[id(i,j)] ) q.push( id(i,j) );
while ( !q.empty() )
{
int x = q.front(); q.pop();
flag[x] = true;
for ( auto y : Link[x] )
if ( ! --indeg[y] ) q.push(y);
}
}
inline void build(void)
{
S = n*m+1 , T = n*m+2;
for (int i=1;i<=n;i++)
{
for (int j=1;j<=m;j++)
{
if ( !flag[id(i,j)] ) continue;
if ( s[i][j] > 0 ) ans += s[i][j];
for (int k=1;k<=c[i][j];k++)
if ( flag[id(a[i][j][k].fi,a[i][j][k].se)] )
insert( id(a[i][j][k].fi,a[i][j][k].se) , id(i,j) , INF );
if ( j < m && flag[id(i,j+1)] ) insert( id(i,j) , id(i,j+1) , INF );
if ( s[i][j] > 0 ) insert( S , id(i,j) , s[i][j] );
if ( s[i][j] < 0 ) insert( id(i,j) , T , -s[i][j] );
}
}
}
inline bool Search(void)
{
memset( d , 0 , sizeof d );
memcpy( cur , Head , sizeof Head );
queue < int > q;
q.push( S ) , d[S] = 1;
while ( !q.empty() )
{
int x = q.front(); q.pop();
for (int i=Head[x];i;i=e[i].next)
{
int y = e[i].ver;
if ( e[i].val && !d[y] )
{
d[y] = d[x] + 1;
q.push( y );
if ( y == T ) return true;
}
}
}
return false;
}
inline int dinic(int x,int flow)
{
if ( !flow || x == T ) return flow;
int residue = flow;
for (int i=cur[x];i;i=e[i].next)
{
int y = e[i].ver; cur[x] = i;
if ( e[i].val && d[y] == d[x] + 1 )
{
int k = dinic( y , min( residue , e[i].val ) );
if ( !k ) d[y] = 0;
e[i].val -= k , e[i^1].val += k;
residue -= k;
if ( !residue ) break;
}
}
return flow - residue;
}
int main(void)
{
input();
Topsort();
build();
while ( Search() )
maxflow += dinic( S , INF );
printf("%d\n",ans-maxflow);
return 0;
}
<后记>
『正睿OI 2019SC Day5』的更多相关文章
- 『正睿OI 2019SC Day8-Day17』
于是就迎来\(10\)天的自闭考试了,每天写点小总结吧. Day8 第一天就很自闭啊,考题分别是数学题+建模题+图论. 前两道题都没有什么算法,但是难度还是有的,于是就做不太出来,特别是第一题.第二题 ...
- 『正睿OI 2019SC Day7』
简单数论 质因子分解 素性测试 素性测试指的是对一个正整数是否为质数的判定,一般来说,素性测试有两种算法: \(1.\) 试除法,直接尝试枚举因子,时间复杂度\(O(\sqrt n)\). \(2.\ ...
- 『正睿OI 2019SC Day4』
总结 今天是一场欢乐的\(ACM\)比赛,于是我队得到了全场倒数的好排名. 好吧,其实还是怪自己不能怪队友啦.对于\(ACM\),可能最主要的还是经验不足,导致比赛的时候有点紧张.虽然队友为了磕一道题 ...
- 『正睿OI 2019SC Day1』
概率与期望 总结 老师上午几乎是在讲数学课,没有讲什么和\(OI\)有关的题目,所以我就做了一点笔记. 到了下午,老师讲完了有关知识点和经典模型,就开始讲例题了.前两道例题是以前就做过的,所以没有什么 ...
- 『正睿OI 2019SC Day6』
动态规划 \(dp\)早就已经是经常用到的算法了,于是老师上课主要都在讲题.今天讲的主要是三类\(dp\):树形\(dp\),计数\(dp\),\(dp\)套\(dp\).其中计数\(dp\)是我很不 ...
- 『正睿OI 2019SC Day3』
容斥原理 容斥原理指的是一种排重,补漏的计算思想,形式化的来说,我们有如下公式: \[\left | \bigcup_{i=1}^nS_i \right |=\sum_{i}|S_i|-\sum_{i ...
- 『正睿OI 2019SC Day2』
分治 普通分治 普通分治是指针对序列或平面问题的分治算法. 思想 普通分治的思想是指将一个序列问题或平面问题通过某种划分方式划分为若干个子问题,直到子问题规模足够小,可以直接回答,再通过合并得到原问题 ...
- 正睿OI DAY3 杂题选讲
正睿OI DAY3 杂题选讲 CodeChef MSTONES n个点,可以构造7条直线使得每个点都在直线上,找到一条直线使得上面的点最多 随机化算法,check到答案的概率为\(1/49\) \(n ...
- 正睿OI国庆DAY2:图论专题
正睿OI国庆DAY2:图论专题 dfs/例题 判断无向图之间是否存在至少三条点不相交的简单路径 一个想法是最大流(后来说可以做,但是是多项式时间做法 旁边GavinZheng神仙在谈最小生成树 陈主力 ...
随机推荐
- 人生物语——哲海拾贝
如今的这个社会,物欲横流.纸醉金迷.浮躁不安是这个时代的主旋律,在这样一个浮华年代的大染缸里,每个人内心都有那么一颗浮躁不安分的种子,或许它才开始发芽,或许它已经占据了你的心灵,人生当中追求 ...
- 红黑树实现(c/c++)
红黑树 简介 一直想写的一种数据结构,非常厉害的思想,插入,删除,查找,修改,都是\(log_2 n\)的时间复杂度. 比AVL更强大的是,插入删除综合效率比AVL要优秀一点. 性质 一颗红黑树是满足 ...
- JavaScript 作用域不完全指北
什么是作用域 对于几乎所有编程语言,最基本的功能之一就是能够存储变量的值,并且能在之后对这个值进行访问和修改.这样就会带来几个问题,这些变量存储在哪里?程序在需要的时候又是如何找到它们的?要解决这些问 ...
- MySQL数据物理备份之lvm快照
使用lvm快照实现物理备份 优点: 几乎是热备(创建快照前把表上锁,创建完后立即释放) 支持所有存储引擎 备份速度快 无需使用昂贵的商业软件(它是操作系统级别的) 缺点: 可能需要跨部门协调(使用操作 ...
- 初学者git的用法
初学者github的用法 1.在github上创建一个自己的工程 2.按着上面的要求执行你的命令行 3.将你的代码放到这个已经创建了.git的文件夹中,执行git add . 系统出现如下错误:war ...
- 1 NLP学习大纲
一.自然语言处理概述 1)自然语言处理:利用计算机为工具,对书面实行或者口头形式进行各种各样的处理和加工的技术,是研究人与人交际中以及人与计算机交际中的演员问题的一门学科,是人工智能的主要内容. 2) ...
- python脚本测试websocket接口协议
import websocket url = 'wss://host:port/rt/tr' #websocket连接地址 ws = websocket.create_connection(url) ...
- Checkout 显示 URL /../../.. 不存在
Checkout 显示 URL /../../.. 不存在 Checkout 显示 URL /../../.. 不存在 如果库的路径是 svn库的路径为:/usr/local/svn/test/ 启动 ...
- java spring框架的定时任务
由于测试的原因,最近有接触java spring @Scheduled的定时任务,当时还以为配置起来表达式和crontab是完全一样的,没想到还有些许不一样. 在spring中,一个cron表达式至 ...
- vue-cli3.0启动项目,在局域网内其他电脑通过自己ip访问
最近一直在使用vue-cli3.0做项目, package.json中配置后,自启动项目,也就没留意过小黑窗, "scripts": { "serve": &q ...