网络流学习笔记

一、基本概念

1.网络流问题

给定指定的一个有向图,其中有两个特殊的点源点 S 和汇点 T,每条边有指定的容量,求满足条件的从 S 到 T 的最大流。

通俗来说:源点可以看作自来水厂,汇点可以看作你家
然后自来水厂和你家之间修了很多水管,水管的最大通水量是不一样的(超了,水管会爆炸)
然后问水厂开闸送水,你家收到水的最大流量是多少
如果水厂停水了,那么你家的流量就是 0,这肯定不是最大流量
但是如果你办了一张水厂的VIP卡,水厂拼命的通水,但是你家的流量会达到一个值并且不会再变,这时就达到了最大流。

2.三个基本性质

对于任意时刻,设 f(u,v) 实际流量,c(u,v) 最大容量,则整个图 G 的六网络满足 3 个性质:

  1. 容量限制:对任意 u,v∈V,f(u,v) <= c(u,v)。
  2. 反对称性:对任意 u,v∈V,f(u,v) = -f(v,u)。从 u 到 v 的流量一定是从 v 到 u 的流量的相反值。(可以对照物理中的一维位移)
  3. 流守恒性:对于任意 u,若 u 不为 s 或者 t,一定有 ∑f(u,v) = 0,(u,v)∈E。即 u 到相邻节点的流量之和为 0,因为流入 u 的流量和 u 流出的流量相同,u 只是流量的“搬运工”。

3.容量网络 流量网络 残留网络

  • 网路:有源点和汇点的有向图,关于什么的网络就是指有向图中的边权代表的含义是什么
  • 容量网络:关于容量的网络,基本是不改变的(极少数问题需要变变动)
  • 流量网络:关于流量的网络,在求解问题的过程中,通常在不断的改变,但是总是满足上述三个性质,调整到最后就是最大流网络,同时也可以得到最大流值
  • 残留网络:残留网络 = 容量网络 - 流量网络(始终成立),但是残留网络可能大于容量网络

4.割 割集

  • 无向图的割集:C[A,B] 是指能将图 G 分为 A 和 B 两个点集的 A 和 B 之间的边的全集
  • 网络的割集:C[S,T] 是指能将网络 G 分为 S 和 T 两个点集(s 属于 S 且 t 属于 T)的从 s 到 t 的边的全集
  • 带权图的割:割集中边或者有向边的权值和

通俗的说:
割集就好比有人把水厂到你家的水管网络砍断了一些
然后,你家接收不到水厂的自来水了
这个人会关心割的大小,毕竟细水管好割一些,而最小割花费的力气最小。

二、最大流基本算法

模板题目:https://www.luogu.com.cn/problem/P3376

1.FF算法(Ford-Fullkerson)(增广路方法)

增广路方法是很多网络流的基础,一般都在残留网络中实现
其思路:每次找出一条从源点到汇点的能够增加流的路径,调整流值和残留网络,不断调整知道没有增广路为主。
FF算法的基础是增广路定理:网络达到最大流当且仅到残留网络中没有增广路
记最大流的流量为 F ,FF算法最多进行 F 次DFS,时间复杂度:O(F|E|),这是一个很松的上界,达到这种最坏复杂度的情况几乎不存在。

//用于表示边的结构体(终点,容量,反向边)
struct edge{
    int to,cap,rev;
};
vector<edge> G[maxn];
bool used[maxn];	//DFS中用到的访问标记
//向图中增加一条从 from 到 to 容量为 cap 的边
void add(int from,int to,int cap)
{
    G[from].push_back((edge){to,cap,G[to].size()});
    G[to].push_back((edge){from,0,G[from].size()-1});
}
//通过DFS寻找增广路
int dfs(int v,int t,int f)
{
    if(v==t)    return f;
    used[v] = true;
    for(int i=0;i<G[v].size();i++){
        edge &e = G[v][i];
        if(!used[e.to]&&e.cap>0){
            int d = dfs(e.to,t,min(f,e.cap));
            if(d>0){
                e.cap -= d;
                G[e.to][e.rev].cap += d;
                return d;
            }
        }
    }
    return 0;
}
//求解从 s 到 t 的最大流
int max_flow(int s,int t)
{
    int flow = 0;
    for( ; ; ){
        memset(used,0,sizeof(used));
        int f = dfs(s,t,INF);
        if(f==0)    return flow;
        flow += f;
    }
}
Dinic算法

上面介绍的FF算法是通过深度优先搜素寻找增广路,并沿着它增广。与之相对,Dinic算法总是寻找最短的增广路,并沿着它增广。因为最短增广路的长度在增广过程中始终不变短,所以无需每次都通过宽度预先搜索来寻找最短增广路。
我们先进行一次宽度优先搜索,然后考虑由近距离点指向远距离顶点的边所组成的分层图,在上面进行深度优先搜索寻找最短增广路。
如果在分层图上找不到新的增广路了,则说明最短增广路的长度确实变长了,也就是不存在最短增广路了,于是重新通过宽度优先搜索构建新的分层图。
时间复杂度:O(|E||V|2)
弧优化和多路增广之后的代码:

//用于表示边的结构体(终点,容量,反向边)
struct edge{
    int to,cap,rev;
};
vector<edge> G[maxn];
int level[maxn];    //顶点到源点的距离标号
int iter[maxn];		//当前弧,在其之前的边已经没用了
//向图中增加一条从 from 到 to 容量为 cap 的边
void add(int from,int to,int cap)
{
    G[from].push_back((edge){to,cap,G[to].size()});
    G[to].push_back((edge){from,0,G[from].size()-1});
}
//通过BFS计算从原点出发的距离标号
void bfs(int s)
{
    memset(level,-1,sizeof(level));
    queue<int> que;
    level[s] = 0;
    que.push(s);
    while(!que.empty()){
        int v = que.front();
        que.pop();
        for(int i=0;i<G[v].size();i++){
            edge &e = G[v][i];
            if(e.cap>0&&level[e.to]<0){
                level[e.to] = level[v] + 1;
                que.push(e.to);
            }
        }
    }
}
//通过DFS寻找增广路
int dfs(int v,int t,int f)
{
    if(v==t)    return f;
    for(int &i=iter[v];i<G[v].size();i++){
        edge &e = G[v][i];
        if(e.cap>0&&level[v]<level[e.to]){
            int d = dfs(e.to,t,min(f,e.cap));
            if(d>0){
                e.cap -= d;
                G[e.to][e.rev].cap += d;
                return d;
            }
        }
    }
    return 0;
}
//求解从 s 到 t 的最大流
int max_flow(int s,int t)
{
    int flow = 0;
    for( ; ; ){
        bfs(s);
        if(level[t]<0)  return flow;
        memset(iter,0,sizeof(iter));
        int f;
        while((f=dfs(s,t,INF))>0){
            flow += f;
        }
    }
}
最高标号预流推进(HLPP):

参考自:https://www.cnblogs.com/owenyu/p/6858123.html

例题:https://loj.ac/problem/127

预流推进是一种很直观的网络流算法。如果给到一个网络流让你手算,一般的想法是从源点开始流,遇到不够的就减掉,一直往前推到汇点。这就是预流推进算法的基本思想。

每个节点是一个储水池,最开始源点有无限多的水。用一个队列维护需要处理的点。最开始把源点加进去,对于每一个当前点,我们把将这个点水池中有的流量沿着边(水管)推到相邻的点,然后把相邻的点加入队列中。

算法思想如此,但其中有一个问题:这样做有可能出现两个点一个推过来一个推回去,结果就死循环了。这时候我们给每个点引入一个高度来解决这个问题。

源点的高度为n,汇点的高度为0,其他点初始高度为0,我们规定,水往下一层流,即我们只推level[x] = level[v] + 1的边(x,v)。

如果一个点还有水,但是却无法推出去,即周围的点都比他高,那么我们就抬高这个点,因为h值是连续的,所以每次出现这种情况我们就给它加一。如果这个点根本就流不出去,那么最后它会被抬高到n+1的高度,回流给源点。

时间复杂度:O(n2 m \sqrt{m} m )

struct edge{
    int to,cap,rev;
};
vector<edge> G[maxn];
int level[maxn],gap[maxm],ep[maxn];
bool inq[maxn];
int n,m,s,t;
struct cmp{
    bool operator()(int a,int b) const{
        return level[a] < level[b];
    }
};
priority_queue<int, vector<int>, cmp> q;
void add(int from, int to, int cap){
    G[from].push_back((edge){ to, cap, G[to].size() });
    G[to].push_back((edge){ from, 0, G[from].size() - 1 });
}
bool bfs(){
    queue<int> que;
    memset(level,inf,sizeof(level));
    level[t] = 0;
    que.push(t);
    while(!que.empty()){
        int v = que.front();
        que.pop();
        for(int i=0;i<G[v].size();i++){
            edge &e = G[v][i];
            if(G[e.to][e.rev].cap>0&&level[e.to]>level[v]+1){
                level[e.to] = level[v] + 1;
                que.push(e.to);
            }
        }
    }
    return level[s] != inf;
}
void push_flow(int v)
{
    for(int i=0;i<G[v].size();i++){
        edge &e = G[v][i];
        if(e.cap>0&&level[e.to]+1==level[v]){
            int f = min(ep[v],e.cap);
            e.cap -= f;G[e.to][e.rev].cap += f;
            ep[v] -= f;ep[e.to] += f;
            if(e.to!=s&&e.to!=t&&!inq[e.to]){
                q.push(e.to);
                inq[e.to] = true;
            }
            if(!ep[v])  break;
        }
    }
}
void reset(int v)
{
    level[v] = inf;
    for(int i=0;i<G[v].size();i++){
        edge &e = G[v][i];
        if(e.cap&&level[e.to]+1<level[v]){
            level[v] = level[e.to] + 1;
        }
    }
}
int hlpp(){
    if(!bfs())  return 0;
    level[s] = n;
    memset(gap,0,sizeof(gap));
    for(int i=1;i<=n;i++){
        if(level[i]<inf)   gap[level[i]]++;
    }
    for(int i=0;i<G[s].size();i++){
        edge &e = G[s][i];
        if(e.cap){
            int pas = e.cap;
            e.cap -= pas;G[e.to][e.rev].cap += pas;
            ep[s] -= pas;ep[e.to] += pas;
            if(e.to!=s&&e.to!=t&&!inq[e.to]){
                q.push(e.to);
                inq[e.to] = true;
            }
        }
    }
    while(!q.empty()){
        int pas = q.top();
        inq[pas] = false;
        q.pop();
        push_flow(pas);
        if(ep[pas]){
            gap[level[pas]]--;
            if(!gap[level[pas]]){
                for(int i=1;i<=n;i++){
                    if(i!=s&&i!=t&&level[i]>=level[pas]&&level[i]<n+1){
                        level[i] = n + 1;
                    }
                }
            }
            reset(pas);
            gap[level[pas]]++;
            q.push(pas);
            inq[pas] = true;
        }
    }
    return ep[t];
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逃夭丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值