网络流学习记录

序言

话痨出现!!
XC兴起安排了一天讲课,其中还有网络流,趁这个时间把网络流好好总结一下。
想让我在Blog里讲网络流原理?讲原理是不可能讲原理的,这辈子都不可能讲原理的。
网络流的算法多如浮云,就像麦趣鸡盒。(我才没有打广告)
这里写图片描述
听完课之后高兴的去研究国家历史。
推荐dalao的Blog。


前置技能

最大流问题的基本模型

P3376 【模板】网络最大流
通俗地讲,题面大概是这样子的:
现在给出一张有向图:
这里写图片描述
现在有若干个点的有向图,每个点的容量是无限的,其中点 S S 有无限量的水,每条边有一个权值w,代表这条边的一次性流量为 w w 个单位,询问最多可以有多少水流到点T
显然,上图的最大流为 20 20

费用流问题的基本模型

P3381 【模板】最小费用最大流
通俗地讲,这个问题为最大流问题的加强版:每条边新增一个边权 c c ,代表这条边的单位流量单价为c,在保证流量最大的同时使得费用最小/最大。

增广路

对于一条路径 (S,T) ( S , T ) ,若其中所有边的残余流量都不为 0 0 ,那么我们称其为一条增广路。
前置技能Get!!

最大流基本算法

基本操作(Ford-Fulkerson方法)

  1. 找到一条增广路
  2. 将增广路流完,同时添加反向弧

反向弧

我们可以看到,在基本操作中,有一个添加反向弧的操作。
对于一条搜索到的增广路中的每一条边(u,v,w),在流完 w w ′ 的流量后,添加一条 (v,u,w) ( v , u , w ′ ) 的反向边。
为什么要有添加反向弧这一个操作?
现在给出一个明显的例子:
这里写图片描述
显然,这张图的最大流是 2 2
但是,当经过这样一条增广路:
这里写图片描述
这里写图片描述
整张网络的流量就变为1了,显然,这是不正确的。
错误的原因就是我们没有给程序一个改正的机会。
那我们应该怎么做?回溯搜索显然是不行的。
既然如此,我们引入反向弧。
为什么添加反向弧就可以保证正确性?
再添加反向边后,我们就可以将之前流的流量给退回去,从而保证了其正确性。
这就是精华部分,利用反向边,使程序有了一个后悔和改正的机会。

Ford-Fulkerson

Ford-Fulkerson算法利用DFS实现搜索增广路,在搜寻到增广路后,更新网络。(其实就是暴力方法)
时间复杂度: O(E|f|) O ( E | f ∗ | ) f f ∗ 为最大流)
Ford-Fulkerson算法的缺点非常明显,因为其时间随着最大流增大而增大。若是在下面的这张图,Ford-Fulkerson算法可能会出现搜索时间过大的情况。
这里写图片描述
假如你运气不好,你可能会在中间的那条边一直跑。

Edmonds-Karp

Edmonds-Karp算法Ford-Fulkerson算法最大的差异在于Edmonds-Karp算法利用BFS实现搜寻增广路,解决了由于最大流过大而引起的时间问题。
时间复杂度: O(V2E) O ( V 2 E )

Code

struct Edge
{
    int from, to , cap , flow;
    Edge(int u, int v, int c, int f) : from(u), to(v), cap(c), flow(f) {}
};

struct Edmonds_Karp
{
    int n, m;
    vector < Edge > edges;
    vector < int > G[MAXN];
    int a[MAXN];
    int p[MAXN];

    void init(int n)
    {
        for (int i = 0; i < n; ++i)
            G[i].clear();

        edges.clear();
    }

    void AddEdge(int from, int to, int cap)
    {
        edges.push_back(Edge(from, to, cap, 0));
        edges.push_back(Edge(to, from, 0, 0));
        m = edges.size();
        G[from].push_back(m - 2);
        G[to].push_back(m - 1);
    }

    int Maxflow(int s, int t)
    {
        int flow = 0;

        for (;;)
        {
            memset(a, 0, sizeof a);
            queue < int > Q;
            Q.push(s);
            a[s] = INF;

            while (!Q.empty())
            {
                int x = Q.front();
                Q.pop();

                for (int i = 0; i < G[x].size(); ++i)
                {
                    Edge &e = edges[G[x][i]];

                    if (!a[e.to] && e.cap > e.flow)
                    {
                        p[e.to] = G[x][i];
                        a[e.to] = min(a[x], e.cap - e.flow);
                        Q.push(e.to);
                    }
                }

                if (a[t])
                    break;
            }

            if (!a[t])
                break;

            for (int u = t; u != s; u = edges[p[u]].from)
            {
                edges[p[u]].flow += a[t];
                edges[p[u] ^ 1].flow -= a[t];
            }

            flow += a[t];
        }

        return flow;
    }
};

Dinic

Wiki:
Dinic算法Edmonds–Karp算法的不同之处在于它每轮算法都选择最短的可行路径进行增广。Dinic算法中采用高度标号(level graph)以及阻塞流(blocking flow)实现其性能。

Kdmonds-Karp算法,每进行一次增广,都要做一遍BFS,十分浪费。能否少做几次BFS? 这就是Dinic算法要解决的问题。
简单来讲,Dinic算法通过BFS在参与网络上处理出距离标号,然后严格按照顺序进行增广。
每条在增广路中的边 (u,v) ( u , v ) 必须严格满足 dep(u)=dep(v)+1 d e p ( u ) = d e p ( v ) + 1
这里写图片描述
意思就是说,下面这条增广路是不合法的。
这里写图片描述
同时,在一次标号后,Dinic算法可以进行多路增广。(详见代码)
时间复杂度: O(V2E) O ( V 2 E ) (二分图: O(VE) O ( V E )

Code

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <queue>
using namespace std;

struct node
{
    int v, w, next;
    node() {}
    node(int _v, int _w, int _next) {v = _v, w = _w, next = _next;}
};

const int MaxN = 10000;
const int MaxM = 100000;

int n, m, s, t;
int ans;
node d[MaxM * 2 + 1];
int final[MaxN + 1], cnt = 1;//"cnt = 1"方便找到反向边
int cur[MaxN + 1]/*当前弧优化*/, dep[MaxN + 1]/*距离标号*/;

void Insert(int u, int v, int w)
{
    d[++cnt] = node(v, w, final[u]), final[u] = cnt;
}

bool Bfs()//处理距离标号
{
    memset(dep, 0, sizeof(dep));

    for (int i = 1; i <= n; i++)
        cur[i] = final[i];

    queue <int> q;
    q.push(s);

    for (; !q.empty(); q.pop())
    {
        int u = q.front(), v;
        dep[u]++;

        for (int i = final[u]; i; i = d[i].next)
            if (d[i].w && i % 2 == 0)
            {
                v = d[i].v;

                if (!dep[v])
                {
                    dep[v] = dep[u];
                    q.push(v);
                }
            }
    }

    return dep[t];
}

int Dfs(int u, int w)
{
    if (u == t)
        return w;

    int _w = 0;

    for (int i = cur[u]; i; i = d[i].next)
    {
        int v = d[i].v;
        cur[u] = i;//当前弧优化

        if (dep[u] + 1 == dep[v])//按照标号增广
        {
            int tmp = Dfs(v, min(w - _w, d[i].w));
            _w += tmp;
            d[i].w -= tmp;
            d[i ^ 1].w += tmp;
            //更新反向弧

            if (w == _w)
                return _w;
        }
    }

    return _w;
}

void Dinic(int u)
{
    for (; Bfs();)
        ans += Dfs(s, 1 << 30);
}

int main(int argc, char const *argv[])
{
    //freopen("init.in", "r", stdin);
    scanf("%d%d%d%d", &n, &m, &s, &t);

    for (int i = 1; i <= m; i++)
    {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        Insert(u, v, w);
        Insert(v, u, 0);
        //添加正反向边
    }

    Dinic(s);
    printf("%d\n", ans);
    return 0;
}

ISAP

ISAP(Improved Shortest Augmenting Path)算法是另外一种基于分层思想的网络流算法,所不同的是,其节省了Dinic算法在每次增广后需要在残余网络上重新标号的问题,而是在每次增广完成后自动更新点的标号。(详见代码)
更重要的是,其引入了GAP优化,在下文会给予讲述。
时间复杂度:理论上为 O(V2E) O ( V 2 E ) ,但在非二分图的情况下,由于GAP优化的引入,时间玄学。

Code

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;

struct node
{
    int v, w, next;
    node() {}
    node(int _v, int _w, int _next) {v = _v, w = _w, next = _next;}
};

const int MaxN = 10000;
const int MaxM = 100000;

int n, m, s, t;
int ans;
node d[MaxM * 2 + 1];
int final[MaxN + 1], cnt = 1;
int cur[MaxN + 1];
int dep[MaxN + 1], st[MaxN + 1];

void Insert(int u, int v, int w)
{
    d[++cnt] = node(v, w, final[u]), final[u] = cnt;
}

int SAP(int u, int w)
{
    if (u == t)//流到汇点,退出
        return w;

    int _w = 0;

    for (int i = cur[u]; i; i = d[i].next)
    {
        int v = d[i].v;
        cur[u] = i;//当前弧优化

        if (d[i].w && dep[v] + 1 == dep[u])
        {
            int tmp = SAP(v, min(d[i].w, w - _w));
            _w += tmp, d[i].w -= tmp, d[i ^ 1].w += tmp;//更新反向弧

            if (w == _w)//满流退出
                return w;
        }
    }

    cur[u] = final[u];

    if (!(--st[dep[u]]))//重标号,GAP优化
        dep[s] = n;

    ++st[++dep[u]];//GAP优化
    return _w;
}

int main(int argc, char const *argv[])
{
    //freopen("init.in", "r", stdin);
    scanf("%d%d%d%d", &n, &m, &s, &t);

    for (int i = 1; i <= m; i++)
    {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        Insert(u, v, w);
        Insert(v, u, 0);//添加反向弧
    }

    st[0] = n;

    for (; dep[s] < n;)//进行增广直至出现断层
        ans += SAP(s, 1 << 30);

    printf("%d\n", ans);
    return 0;
}

当前弧优化

在上文叙述的Dinic算法以及ISAP算法中都出现了数组 cur[] c u r [ ] ——当前弧优化数组。
这是什么?为什么要这么做?
现在给出一个简单的问题:有一样事情在你的计划里,你已经完成的非常出色,现在叫你再去完成一遍,你愿意吗?
显然!!!没有人愿意。
同理,当前弧优化记录了当前走到的弧,避免了再次递归到时的重新枚举。

GAP(断层)优化

ISAP算法中引入了GAP优化,即代码中的数组 st[] s t [ ]
st[dep] s t [ d e p ] 表示距离标号为 dep d e p 的节点有 st[dep] s t [ d e p ] 个。
有个十分显然的性质就是当其中的一个 st[]=0 s t [ ] = 0 时,网络出现断层,进而无法继续增广,算法结束。(详见ISAP算法代码)

费用流基本算法

对于费用流问题,我们依旧采用Ford-Fulkerson方法,采用Dinic算法,只不过将BFS标号改为SPFA或Dijkstar算最短路,每次沿着最短路增广。(但貌似多路增广会出锅,求dalao解释)

Code

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <queue>
using namespace std;

struct node
{
    int v, w, dis, next;
    node() {}
    node(int _v, int _w, int _dis, int _next) {v = _v, w = _w, dis = _dis, next = _next;}
};

const int MaxN = 5000;
const int MaxM = 50000;

int n, m, s, t;
int ans, cost;
node d[MaxM * 2 + 10];
int final[MaxN + 1], cnt = 1;
int cur[MaxN + 1], dis[MaxN + 1];
int _w[MaxN + 1], prev[MaxN + 1];
bool bj[MaxN + 1];
queue <int> q;

void Insert(int u, int v, int w, int dis)
{
    d[++cnt] = node(v, w, dis, final[u]), final[u] = cnt;
}

bool SPFA(int s, int t)
{
    memset(dis, 0x7f, sizeof(dis));
    memset(_w, 0x7f, sizeof(_w));
    memset(bj, false, sizeof(bj));
    bj[s] = true;
    dis[s] = 0;
    prev[t] = 0;
    q.push(s);

    for (; !q.empty(); q.pop())
    {
        int u = q.front(), v;

        for (int i = final[u]; i; i = d[i].next)
        {
            v = d[i].v;

            if (d[i].w && dis[v] > dis[u] + d[i].dis)
            {
                dis[v] = dis[u] + d[i].dis;
                prev[v] = u;
                cur[v] = i;
                _w[v] = min(_w[u], d[i].w);

                if (!bj[v])
                {
                    q.push(v);
                    bj[v] = true;
                }
            }
        }

        bj[u] = false;
    }

    return prev[t];
}

void Mininum_Cost_Maxinum_Flow()
{
    for (; SPFA(s, t);)
    {
        int u = t;
        ans += _w[t];
        cost += _w[t] * dis[t];

        while (u != s)
        {
            d[cur[u]].w -= _w[t];
            d[cur[u] ^ 1].w += _w[t];
            u = prev[u];
        }
    }
}

int main(int argc, char const *argv[])
{
    freopen("init.in", "r", stdin);
    scanf("%d%d%d%d", &n, &m, &s, &t);

    for (int i = 1; i <= m; i++)
    {
        int u, v, w, dis;
        scanf("%d%d%d%d", &u, &v, &w, &dis);
        Insert(u, v, w, dis);
        Insert(v, u, 0, -dis);
    }

    Mininum_Cost_Maxinum_Flow();
    printf("%d %d\n", ans, cost);
    return 0;
}

总结

话痨出现!!
对的没有错,我又来说垃圾话了。
网络流这种1956年的巨佬提出来的东西,现在变成了OI毒瘤……
怎么讲,网络流这个东西如果裸的出现,那么它一般是预流推进了,你可以准备上暴力。
但如果它出现在题里,要你建模的话,恭喜你,你是不可能建出模型的,你可以准备上暴力。(除非你是dalao,蒟蒻在此膜拜)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值