首页 > 技术文章 > 网络流$1$·简单的$EK$与$Dinic~of~Net-work ~ Flow$学习笔记

pks-t 2018-08-20 11:36 原文

\(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 ;
}

推荐阅读