\(2333\)这是好久之前学的了,不过一直在咕咕咕咕。

一般来讲,正常的网络流笔记一开始都是要给网络流图下定义的。那么我们不妨也来先进行一波这种操作。

那么网络流图,类似于有向图,边上带权,但是这个权值变成了“容量”。那么,我们定义容量为\(c(u,v) \in E ? c(u,v) : 0\)。在整张图中有一个源点和一个汇点,且对于每个点来说有$$\sum F_{in} = \sum F_{out}$$并且我们人为的将\(S\)的\(F_{in}\)设置为\(0\),\(F_{out}\)设置为\(+\infty\)。\(T\)正好相反。当然,如果非说不合适的话,可以将源点和汇点看做同一个点233.

最短路最多从2增到n这显然是一个线性规划的标准方程,那么通过线性规划我们可以证明的是最大流等价于最小割

博主现在对线性规划还只是一知半解,等什么时候“贯通了”再整理博客吧233

好的,窝觉得定义什么的可以不说了,我们直接上\(EK\)

一、不知道可以用来干啥的\(EK\)

其实,\(EK\)身为大家眼中的\(basis\)算法,他居然是比\(Dinic\)晚发表的……\(233\)

全程是\(Edmond-Karp\) ,由两位科学家一起发表的,复杂度上界大约在\(\Theta(nm^2)\)左右,是个比较没用的算法

他的原理就是,我们通过两个杀器来实现最大流:

\(Killer1:\)增广路

这个东西就是我们不断寻找从源点到汇点的可行路径,不断流直到不能流为止,也没有什么技巧可言,毕竟网络流是线性规划而不是动态规划,图集与解是单射的逻辑关系而不是一对多的非映射关系。

\(Killer2:\) 反向边

虽然图集与解是单射的逻辑关系,即虽然对于同一张图\(G(U, V)\)无论怎么走,最优解(最大流)总是一个定值,但是我们在执行算法的时候可能会因为选择了错误的增广路经而导致算法的错误。所以此时我们考虑建立反向边。其实这就是一个小小的反悔操作。这个正确性在于我们建立了反向边,对于执行反悔操作并没有什么问题,对于执行正常的增广操作也不会影响什么结果,因为毕竟是反向边——是从\(T\)连向\(S\)的,等同于原来没反向边时的情况。

嗯,那么我们程序实现的时候,大概就是这样

bool BFS(){
queue<int> q ;
fill(_F, _F + N + 1, Inf) ;
fill(dis, dis + N + 1, 0) ;
q.push(S), dis[S] = 1, pre[T] = -1;
while (!q.empty()){
now = q.front() ; q.pop() ;
for (int k = head[now]; k != -1 ; k = e[k].next){
if(e[k].v && !dis[e[k].to]){
dis[e[k].to] = dis[now] + 1 ;
_F[e[k].to] = min(e[k].v, _F[now]) ;
pre[e[k].to] = now, Last[e[k].to] = k ;
q.push(e[k].to) ;
}
}
}
return dis[T] != 0 ;
}
void _EK(){
while(BFS()){
now = T, MAX_F += _F[T] ;
while(now != S)
e[Last[now]].v -= _F[T], e[Last[now] ^ 1].v += _F[T], now = pre[now] ;
}
}

其中\(Last\)记录前驱,\(dis\)就是个\(mark\),\(\_F\)数组记录增广路上最大的流量 。

那我们接下来分析复杂度。值得注意的是,\(EK\)由于采用\(BFS\),所以每次找的一定是最短路。而在最短路不变的一段时间内一条边和它的反向边不可能都被增广(如果增广反向边的话,\(dis_{min}++\)),所以在每条边都作为残量最小值增广一次之后(至多\(m\)次)最短路就会增加。而最短路最多从\(2\)增到\(n\),所以最多增广\(n \times m\)次。而每次\(bfs\)至多是\(\Theta(m)\)的,所以总复杂度上界是\(\Theta(nm^2)\)

但事实上,随机的数据大多数情况下是要远远小于这个复杂度上界的,所以\(EK\)可以解决朴素的最大流问题。

全部的代码存档:

#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#define MAX 100010
#define Inf 1926081700 using namespace std ;
int N, M, S, T ;
struct edges{
int to, next, v ;
}e[MAX << 1] ;
int MAX_F, i ;
int head[MAX], cnt = -1, pre[MAX], now ;
int A, B, C, Last[MAX], _F[MAX], dis[MAX] ; inline int qr(){
int k = 0 ; char c = getchar() ;
while (c < '0' || c > '9') c = getchar() ;
while (c <= '9' && c >= '0') k = (k << 1) + (k << 3) + c - 48, c = getchar() ;
return k ;
}
inline void add(int u, int v, int w){
e[++ cnt].to = v, e[cnt].v = w ;
e[cnt].next = head[u], head[u] = cnt ;
e[++ cnt].to = u, e[cnt].v = 0 ;
e[cnt].next = head[v], head[v] = cnt ;
}
bool BFS(){
queue<int> q ;
fill(_F, _F + N + 1, Inf) ;
fill(dis, dis + N + 1, 0) ;
q.push(S), dis[S] = 1, pre[T] = -1;
while (!q.empty()){
now = q.front() ; q.pop() ;
for (int k = head[now]; k != -1 ; k = e[k].next){
if(e[k].v && !dis[e[k].to]){
dis[e[k].to] = dis[now] + 1 ;
_F[e[k].to] = min(e[k].v, _F[now]) ;
pre[e[k].to] = now, Last[e[k].to] = k ;
q.push(e[k].to) ;
}
}
}
return dis[T] != 0 ;
}
void _EK(){
while(BFS()){
now = T, MAX_F += _F[T] ;
while(now != S)
e[Last[now]].v -= _F[T], e[Last[now] ^ 1].v += _F[T], now = pre[now] ;
}
}
int main(){
cin >> N >> M >> S >> T ;
fill (head + 1, head + N + 1, -1) ;
for (i = 1; i <= M; ++ i)
A = qr(), B = qr(), C = qr(), add(A, B, C) ;
_EK() ;
cout << MAX_F << endl ;
return 0 ;
}

二、据说可以拯救世界的\(Dinic\)

那么接下来我们说\(Dinic\),这个算法是由\(Dinic\)教授创造的\(qwq\)

然后\(Dinic\)在\(EK\)的基础上,采用了两个新的优化方案:

\(Case1:\)分层图

每次我们选择用\(bfs + dfs\)去增广一张“增广网”,大体上就是我们记录深度(或者说是离源点的最小距离),然后我们用\(dfs\)遍历这张增广网。

\(Case2:\)当前弧

我们依仗的是这一段(句)代码:

for(int &i=cur[now];i!=-1;i=line[i].nxt)

其中比较重要的是引用符号,此处引用的目的是不断更新\(cur\),达到不重复枚举的目的。

那么整体代码就是:

#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#define MAX 100010 using namespace std ;
int N, M, S, T ;
struct edges{
int to, next, v ;
}e[MAX << 1] ; int A, B, C, i ;
int head[MAX], cnt = -1, now, cur[MAX], dis[MAX] ; inline int qr(){
int k = 0 ; char c = getchar() ;
while (c < '0' || c > '9') c = getchar() ;
while (c <= '9' && c >= '0') k = (k << 1) + (k << 3) + c - 48, c = getchar() ;
return k ;
}
inline void add(int u, int v, int w){
e[++ cnt].to = v, e[cnt].v = w ;
e[cnt].next = head[u], head[u] = cnt ;
e[++ cnt].to = u, e[cnt].v = 0 ;
e[cnt].next = head[v], head[v] = cnt ;
}
bool bfs(){
queue<int> q ;
fill(dis, dis + N + 23, 0) ;
q.push(S), dis[S] = 1 ;
while (!q.empty()){
now = q.front() ; q.pop() ;
for (int k = head[now]; k != -1 ; k = e[k].next){
if (!dis[e[k].to] && e[k].v)
dis[e[k].to] = dis[now] + 1, q.push(e[k].to) ;
}
}
return dis[T] ? 1 : 0 ;
}
int dfs(int St, int Aim, int Flow){
if (St == Aim || !Flow) return Flow ; int Fl, res = 0 ;
for (int &k = cur[St] ; k != -1; k = e[k].next)
if (dis[e[k].to] == dis[St] + 1 && (Fl = dfs(e[k].to, Aim, min(Flow, e[k].v)))){
res += Fl, e[k].v -= Fl, e[k ^ 1].v+= Fl ;
Flow -= Fl ; if (!Flow) break ;
}
return res ;
}
int Dinic(){
int res = 0 ;
while(bfs()){
for(i = 1; i <= N; ++ i) cur[i] = head[i] ;
res += dfs(S, T, 0x7fffffff) ;
}
return res ;
} int main(){
cin >> N >> M >> S >> T ;
fill (head + 1, head + N + 1, -1) ;
for (i = 1; i <= M; ++ i)
A = qr(), B = qr(), C = qr(), add(A, B, C) ;
cout << Dinic() ;
return 0 ;
}

嗯,那么我们不难看出\(cur\)其实就是为了防止我们不断重复枚举边。因为对于一次\(dfs\),在同一张分好层次的图上执行,不会出现重复用一条边的情况——我们认为每条边已经流满。那么当前弧可以保证不会重复走。而复杂度没有变,但是确实会更快。

那么接下来证明一下\(Dinic\)的时间复杂度。

根据分层图而言,\(t\)的层次是单调增长的——因为每次增广完毕之后对于每条可行的增广路,都总会有至少一条边容量为零,所以最多会有\(n\)次重新分层。而对于每次在增广网上的操作,至多有\(m\)条增广路(每条边至多有一次机会置零),每条增广路要回溯+搜索总共\(O(2n)\)的操作。那么渐进意义上复杂度就是\(\Theta(n^2m)\)的。

很显然,这在随机数据的情况下也是跑不满的。而加了当前弧优化,复杂度理论上还是不变的,或者说,在跑满的情况下,复杂度更接近上限复杂度\(\Theta(n^2m)\) 。

据说随机图上跑个\(1 \cdot 1e4\)~\(5 \cdot 1e4\)是没什么问题的。

最后我们来说一下费用流。

三、费用流(最小费用最大流)

其实费用流……常见的,就是在最大流的前提下费用最小。那么我们直接把\(EK\)的\(bfs\)换成\(SPFA\)就行了233

至于为什么不能\(dinic\),很显然是因为没法分层啊……\(hhh\)

// luogu-judger-enable-o2
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#define MAX 100010
#define Inf 1926081700 using namespace std ;
int N, M, S, T ;
struct edges{
int to, next, v, c ;
}e[MAX << 1] ;
bool mark[MAX] ; int MAX_F, MAX_C, i ;
int head[MAX], cnt = -1, pre[MAX], now ;
int A, B, C, D, Last[MAX], _F[MAX], dis[MAX] ; inline int qr(){
int k = 0 ; char c = getchar() ;
while (c < '0' || c > '9') c = getchar() ;
while (c <= '9' && c >= '0') k = (k << 1) + (k << 3) + c - 48, c = getchar() ;
return k ;
}
inline void add(int u, int v, int w, int c){
e[++ cnt].to = v, e[cnt].v = w ;
e[cnt].next = head[u], e[cnt].c = c, head[u] = cnt ;
e[++ cnt].to = u, e[cnt].v = 0 ;
e[cnt].next = head[v], e[cnt].c = -1 * c, head[v] = cnt ;
}
bool SPFA(){
queue<int> q ;
fill(_F, _F + N + 1, Inf) ;
fill(dis, dis + N + 1, Inf) ;
fill(mark, mark + N + 1, 0) ;
q.push(S), dis[S] = 0, mark[S] = 1, pre[T] = -1;
while (!q.empty()){
now = q.front() ; q.pop() ; mark[now] = 0 ;
for (int k = head[now]; k != -1 ; k = e[k].next)
if (dis[e[k].to] > dis[now] + e[k].c && e[k].v){
dis[e[k].to] = dis[now] + e[k].c ;
_F[e[k].to] = min(e[k].v, _F[now]) ;
pre[e[k].to] = now, Last[e[k].to] = k ;
if(!mark[e[k].to]){
q.push(e[k].to) ;
mark[e[k].to] = 1 ;
}
}
}
return dis[T] != Inf;
}
void _EK(){
while(SPFA()){
now = T, MAX_F += _F[T], MAX_C += dis[T] * _F[T] ;
while(now != S)
e[Last[now]].v -= _F[T], e[Last[now] ^ 1].v += _F[T], now = pre[now] ;
}
}
int main(){
cin >> N >> M >> S >> T ;
fill (head + 1, head + N + 1, -1) ;
for (i = 1; i <= M; ++ i)
A = qr(), B = qr(), C = qr(), D = qr(), add(A, B, C, D) ;
_EK() ;
cout << MAX_F <<" "<< MAX_C << endl ;
return 0 ;
}

但是\(SPFA\)他,他他他他他已经死在了\(NOI2018\)……

那么我们考虑是否能用\(dijkstra\)来做。那我们要考虑的就是负权边,因为我们建的反向边是要把代价也跑回去的啊,所以我们致力于解决负权边问题。\(rqy\)当时是这么给我们讲的:

考虑给每个点加一个“势”\(h\) 。一条\(u\) → \(v\) 的费用为 \(c\) 的边变成一条\(u\)→\(v\)费用是\(c−h_v+h_u\) 的边。

那么我们从点\(S\)到点\(B\)点的距离便从\(dis_B\)变成了\(dis_B + h_s- h_B\),我们最后只需要把原来的势函数减去即可。

下面我们思考到底要选取什么作为势函数呢?

我们考虑将上次求出的最短路作为势函数,为什么呢?\(rqy\)是这么说的:

这为什么是对的呢?

考虑一条边 \(u→v​\) ,费用为 \(c​\) 。

如果它上一次增广时残量不为 \(0\) ,那么根据最短路的性质有\(dis_u + c ≥ dis_v\) (不然的话说明最短路求错了)。 如果它上次增广时残量为 \(0\) 而现在不为 \(0\) ,那说明它的反向边被增广了。而增广的路径是最短路径,反向边是 \(v → u\),费用 \(−c\) 。所以\(dis_v\) =\(dis_u −c\) ,也就是说 \(-c+dis_u −dis_v = 0\) 也是非负的,那么\(w+h_u −h_v\)就是非负的。

于是我们现在可以用 \(Dijkstra\) 增广,很快而且更难卡(

至于代码,大概长这样:

#include <queue>
#include <cstdio>
#include <iostream>
#include <algorithm>
#define MAX 100010
#define Inf 192608170 using namespace std ;
struct edge{
int to, next, c, f ;
}e[MAX << 1] ; int H[MAX], S ;
int dist[MAX], _F[MAX], Pre[MAX], i, k ;
int N, M, A, B, C, D, cnt = -1, x1, x2, head[MAX] ;
struct node{
int dist, num ;
bool operator <(const node & now) const{return dist > now.dist ; }
}; priority_queue<node> q ; bool vis[MAX] ; int Last[MAX], MAX_F, MAX_C, t, ww ; inline int qr(){
int k = 0 ; char c = getchar() ;
while (c < '0' || c > '9') c = getchar() ;
while (c <= '9' && c >= '0') k = (k << 1) + (k << 3) + c - 48, c = getchar() ;
return k ;
}
inline void Add(int u, int v, int f, int c){
e[++ cnt].to = v, e[cnt].f = f ;
e[cnt].next = head[u], e[cnt].c = c, head[u] = cnt ;
e[++ cnt].to = u, e[cnt].f = 0 ;
e[cnt].next = head[v], e[cnt].c = -1 * c, head[v] = cnt ;
}
bool dijkstra(){
for (i = 1 ; i <= N; ++ i) dist[i] = _F[i] = Inf, vis[i] = 0 ;
q.push((node){0, S}) ; dist[S] = 0 ;
while(!q.empty()){
node now = q.top() ; q.pop() ;
while(vis[now.num]&&!q.empty()) now = q.top(), q.pop();
x1 = now.num, x2 = now.dist ; if(vis[x1]) continue ;
vis[x1] = 1 ;
for(k = head[x1] ; k != -1 ; k = e[k].next)
if (e[k].f > 0 && !vis[e[k].to] && dist[e[k].to] > x2 + e[k].c + H[x1] - H[e[k].to]){
int T = e[k].to ; dist[T] = x2 + e[k].c + H[x1] - H[T] ;
_F[T] = min(_F[x1], e[k].f), Pre[T] = x1, Last[T] = k, q.push((node){dist[T], T}) ;
}
}
return dist[t] < Inf ;
}
inline void _EK(){
while(dijkstra()){
ww = t, MAX_F += _F[t], MAX_C += (dist[t] - H[S] + H[t]) * _F[t] ;
while(ww != S)
e[Last[ww]].f -= _F[t], e[Last[ww] ^ 1].f += _F[t], ww = Pre[ww] ;
for (i = 1 ; i <= N ; ++ i) H[i] += dist[i] ;
}
}
int main(){
cin >> N >> M >> S >> t ;
for (i = 0 ; i <= N ; ++ i) head[i] = -1 ;
for (i = 1 ; i <= M ; ++ i)
A = qr(), B = qr(), C = qr(), D = qr(), Add(A, B, C, D) ;
_EK() ;
cout << MAX_F << " " << MAX_C << endl ; return 0 ;
}

网络流$1$·简单的$EK$与$Dinic~of~Net-work ~ Flow$学习笔记的更多相关文章

  1. C#可扩展编程之MEF学习笔记(一):MEF简介及简单的Demo

    在文章开始之前,首先简单介绍一下什么是MEF,MEF,全称Managed Extensibility Framework(托管可扩展框架).单从名字我们不难发现:MEF是专门致力于解决扩展性问题的框架 ...

  2. maven权威指南学习笔记(三)——一个简单的maven项目

    目标: 对构建生命周期 (build  lifecycle),Maven仓库 (repositories),依赖管理 (dependency management)和项目对象模型 (Project O ...

  3. Log4j简单学习笔记

    log4j结构图: 结构图展现出了log4j的主结构.logger:表示记录器,即数据来源:appender:输出源,即输出方式(如:控制台.文件...)layout:输出布局 Logger机滤器:常 ...

  4. 学习笔记:利用GDI+生成简单的验证码图片

    学习笔记:利用GDI+生成简单的验证码图片 /// <summary> /// 单击图片时切换图片 /// </summary> /// <param name=&quo ...

  5. Dynamic CRM 2013学习笔记(四十六)简单审批流的实现

    前面介绍过自定义审批流: Dynamic CRM 2013学习笔记(十九)自定义审批流1 - 效果演示 Dynamic CRM 2013学习笔记(二十一)自定义审批流2 - 配置按钮 Dynamic ...

  6. [转载]SharePoint 2013搜索学习笔记之搜索构架简单概述

    Sharepoint搜索引擎主要由6种组件构成,他们分别是爬网组件,内容处理组件,分析处理组件,索引组件,查询处理组件,搜索管理组件.可以将这6种组件分别部署到Sharepoint场内的多个服务器上, ...

  7. 简单的玩玩etimer <contiki学习笔记之九 补充>

    这幅图片是对前面  <<contiki学习笔记之九>>  的一个补充说明. 简单的玩玩etimer <contiki学习笔记之九> 或许,自己正在掀开contiki ...

  8. 简单的玩玩etimer <contiki学习笔记之九>

    好吧,我承认etimer有点小复杂,主要是它似乎和contiki的process搅在一起,到处都在call_process.那就先搜搜contiki下的etimer的example看看,然后再试着写一 ...

  9. Ext.Net学习笔记19:Ext.Net FormPanel 简单用法

    Ext.Net学习笔记19:Ext.Net FormPanel 简单用法 FormPanel是一个常用的控件,Ext.Net中的FormPanel控件同样具有非常丰富的功能,在接下来的笔记中我们将一起 ...

  10. JSP学习笔记(三):简单的Tomcat Web服务器

    注意:每次对Tomcat配置文件进行修改后,必须重启Tomcat 在E盘的DATA文件夹中创建TomcatDemo文件夹,并将Tomcat安装路径下的webapps/ROOT中的WEB-INF文件夹复 ...

随机推荐

  1. YII 用gii生成modules模块下的mvc

    1.生成model ModelPath设置为: application.modules.[moduleName].models 2.生成CURD ModelClass设置为: application. ...

  2. BZOJ3451:Tyvj1953 Normal

    根据期望的线性性,答案就是 \(\sum\) 每个连通块出现次数的期望 而每个连通块次数的期望就是 \(\sum\) 连通块的根与每个点连通次数的期望 也就是对于一条路径 \((i,j)\),设 \( ...

  3. mysql_real_escape_string与mysqli_real_escape_string

    参考 mysql_real_escape_string  mysqli_real_escape_string mysql_real_escape_string是用来转义字符的,主要是转义POST或GE ...

  4. CSS 属性-webkit-tap-highlight-color的理解

    1.-webkit-tap-highlight-color 这个属性只用于iOS (iPhone和iPad).当你点击一个链接或者通过Javascript定义的可点击元素的时候,它就会出现一个半透明的 ...

  5. 关于j使用ava匿名类的好处总结

    匿名类,除了只能使用一次,其实还有其他用处,比如你想使用一个类的protected方法时,但是又和这个类不在同一个包下,这个时候匿名类就派上用场了,你可以定义一个匿名类继承这个类,在这个匿名类中定义一 ...

  6. 报表导出excel方式介绍

     报表导出excel提供了四种方式,在单元格属性"其他/导出excel方式"可以选择,如下图 一是导出缺省值:报表中的单元格包含两个值,一个真实值一个显示值,但是在excel中 ...

  7. settimeout、setinterval区别和相互模拟

    前几天翻书,看到“避免双重求值”一节时有提到settimeout().setinterval() 建议传入函数而不是字符串以作为第一个参数,所以这里总结一下settimeout()和setinterv ...

  8. Typescript 接口(interface)

    概述 typescript 的接口只会关注值的外形,实际就是类型(条件)的检查,只要满足就是被允许的. 接口描述了类的公共部分. 接口 interface Person { firstName: st ...

  9. git学习——简介、使用(一)

    本文是作者参考其他教程学习git的记录,原文:http://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c01 ...

  10. Oracle EBS 查询客户报错 查询已超出 200 行。可能存在更多的行,请限制查询。