网络流$1$·简单的$EK$与$Dinic~of~Net-work ~ Flow$学习笔记
\(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$学习笔记的更多相关文章
- C#可扩展编程之MEF学习笔记(一):MEF简介及简单的Demo
在文章开始之前,首先简单介绍一下什么是MEF,MEF,全称Managed Extensibility Framework(托管可扩展框架).单从名字我们不难发现:MEF是专门致力于解决扩展性问题的框架 ...
- maven权威指南学习笔记(三)——一个简单的maven项目
目标: 对构建生命周期 (build lifecycle),Maven仓库 (repositories),依赖管理 (dependency management)和项目对象模型 (Project O ...
- Log4j简单学习笔记
log4j结构图: 结构图展现出了log4j的主结构.logger:表示记录器,即数据来源:appender:输出源,即输出方式(如:控制台.文件...)layout:输出布局 Logger机滤器:常 ...
- 学习笔记:利用GDI+生成简单的验证码图片
学习笔记:利用GDI+生成简单的验证码图片 /// <summary> /// 单击图片时切换图片 /// </summary> /// <param name=&quo ...
- Dynamic CRM 2013学习笔记(四十六)简单审批流的实现
前面介绍过自定义审批流: Dynamic CRM 2013学习笔记(十九)自定义审批流1 - 效果演示 Dynamic CRM 2013学习笔记(二十一)自定义审批流2 - 配置按钮 Dynamic ...
- [转载]SharePoint 2013搜索学习笔记之搜索构架简单概述
Sharepoint搜索引擎主要由6种组件构成,他们分别是爬网组件,内容处理组件,分析处理组件,索引组件,查询处理组件,搜索管理组件.可以将这6种组件分别部署到Sharepoint场内的多个服务器上, ...
- 简单的玩玩etimer <contiki学习笔记之九 补充>
这幅图片是对前面 <<contiki学习笔记之九>> 的一个补充说明. 简单的玩玩etimer <contiki学习笔记之九> 或许,自己正在掀开contiki ...
- 简单的玩玩etimer <contiki学习笔记之九>
好吧,我承认etimer有点小复杂,主要是它似乎和contiki的process搅在一起,到处都在call_process.那就先搜搜contiki下的etimer的example看看,然后再试着写一 ...
- Ext.Net学习笔记19:Ext.Net FormPanel 简单用法
Ext.Net学习笔记19:Ext.Net FormPanel 简单用法 FormPanel是一个常用的控件,Ext.Net中的FormPanel控件同样具有非常丰富的功能,在接下来的笔记中我们将一起 ...
- JSP学习笔记(三):简单的Tomcat Web服务器
注意:每次对Tomcat配置文件进行修改后,必须重启Tomcat 在E盘的DATA文件夹中创建TomcatDemo文件夹,并将Tomcat安装路径下的webapps/ROOT中的WEB-INF文件夹复 ...
随机推荐
- cf1000F. One Occurrence(线段树 set)
题意 题目链接 Sol (真后悔没打这场EDU qwq) 首先把询问离线,预处理每个数的\(pre, nxt\),同时线段树维护\(pre\)(下标是\(pre\),值是\(i\)),同时维护一下最大 ...
- js-权威指南学习笔记3
第四章 表达式和运算符 1.最简单的表达式是原始表达式,是表达式的最小单位——它们不再包含其他表达式.JS中原始表达式包含常量或直接量.关键字和变量. 2.一个对象的属性名不是固定值时,必须使用方括号 ...
- scss-函数
在scss中除了可以定义变量,具有@extend和@mixins等特性之外,还自备了一系列的函数功能. scss本身带有大量的内置函数,具体可以参阅官网函数模块. 一.字符串函数 unquote($s ...
- windows下生成上传git时需要用的SSH密钥
参考:Windows上传代码到Github 打开“Git Bash” 输入 ssh-keygen -C "your email" -t rsa 出现如下结果: 成功后,信息里会显示 ...
- DOM增删操作(创建删除表格)
<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title>& ...
- 在weblogic下部署找不到授权文件的解决方法
很多用户在weblogic上部署的时候,会遇到类似的报错信息,提示授权找不到,解决这个问题的思路如下: 第一步确定授权的没有过期, 客户如果修改了系统时间,会对授权生效产生影响,在进行操作前先将 ...
- Flex Box 简单弹性布局
弹性盒子模型有两种规范:早起的display:box 和后期的display:flex.它可以轻易的实现均分.浮动.居中等灵活布局,在移动端只考虑webkit内核时很实用. 一.display:box ...
- Sink Prosessor - Flume的可靠性保证:故障转移、负载均衡
Flume的一些组件(如Spooling Directory Source.File Channel)能够保证agent挂掉后不丢失数据. 1.负载均衡 1)Load balancing Sink P ...
- Python Socket实现简单web服务器
#!/usr/bin/python env # coding:utf-8 import socket ip_port = ('127.0.0.1', 80) back_log = 10 buffer_ ...
- Tomcat – Java.Lang.OutOfMemoryError: PermGen Space
很多时候,在开发阶段Tomcat重复的重启过程中会遇到java.lang.OutOfMemoryError : PermGen space 错误. 1 2 3 4 java.lang.OutOfMem ...