网络流

前言

当听到网络 流量之后 感觉是在充话费

网络流(network-flows)是一种类比水流的解决问题方法,与线性规划密切相关。

它模拟了水流从起点经过复杂的网络流向终点的过程

就像自来水厂的水 经过无数根水管子 流到了家里

最大流就是最多有多少水流到了家里


算法流程

EK算法

首先,引入增广路的概念。

一条从s到t的路径,水流流过这条路,使得当前可以到达t的流量可以增加。

知道了增广路的概念,就可以很显然的想出一种贪心做法,不断通过bfs寻找增广路并处理和累加答案,直到找不到增广路,答案就是最大流。

但是它是错的

如图

image

如果我们找到了s-1-2-3-t 这条路径 流量为1

那么图会变成这样

image

再找不到了增广路了 但是显而易见原图的最大流是2

所以 我们引入了后悔药反向边

在流量减去最小值的时候,将反向边加上最小值

就像这样

image

于是多了一条增广路 s-3-2-1-t

综上所述 我们得到了一种求最大流的方法

总结一下就是

通过没流满的边或新建的反向边找增广路增广

在找不到增广路时算法结束

其实这个思想就是EK算法的思想

代码大概是这样的

#define maxn 250
#define INF 0x3f3f3f3f

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 EK {
  int n, m;             // n:点数,m:边数
  vector<Edge> edges;   // edges:所有边的集合
  vector<int> G[maxn];  // G:点 x -> x 的所有边在 edges 中的下标
  int a[maxn], p[maxn];  // a:点 x -> BFS 过程中最近接近点 x 的边给它的最大流
                         // p:点 x -> BFS 过程中最近接近点 x 的边

  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++) {  // 遍历以 x 作为起点的边
          Edge& e = edges[G[x][i]];
          if (!a[e.to] && e.cap > e.flow) {
            p[e.to] = G[x][i];  // G[x][i] 是最近接近点 e.to 的边
            a[e.to] =
                min(a[x], e.cap - e.flow);  // 最近接近点 e.to 的边赋给它的流
            Q.push(e.to);
          }
        }
        if (a[t]) break;  // 如果汇点接受到了流,就退出 BFS
      }
      if (!a[t])
        break;  // 如果汇点没有接受到流,说明源点和汇点不在同一个连通分量上
      for (int u = t; u != s;
           u = edges[p[u]].from) {  // 通过 u 追寻 BFS 过程中 s -> t 的路径
        edges[p[u]].flow += a[t];      // 增加路径上边的 flow 值
        edges[p[u] ^ 1].flow -= a[t];  // 减小反向路径的 flow 值
      }
      flow += a[t];
    }
    return flow;
  }
};

概念&&性质

我们需要补充几个概念

不学好像也行?

  1. 在网络流中,有一个源点s,还有一个汇点t,边的权值称为容量,且边为有向边
  2. 边的当前的流量应该小于等于容量(不然水管不就爆了)
  3. 边的当前的流量等于反向边的流量的相反数
  4. 每个中间点流入的等于流出的
  5. 整个图的流量为从源点出发的流量之和(流入汇点的之和)
  6. 残量网络:没流满的边和反向边所形成的图(找增广路其实就是在它上面找的)

所以EK算法就是在残量网络上求增广路的算法

还有对它正确性的证明 利用了最大流等于最小割


Dinic算法

Dinic算法是EK算法的优化

它有三处优化

  1. 分层图优化:在增广之前跑BFS建出分层的残量网络,可以同时多路增广
  2. 当前弧优化:将链式前向星的第一条边传址调用 直接修改它的值
  3. -1优化:在同一次DFS中 一个点无法引出任意增广路 直接删掉

好像多路增广不算优化 OI Wiki上这么说的

可能是由于大量网络资料的错误表述引发以讹传讹的情形,相当数量的选手喜欢将当前弧优化和多路增广并列称为 Dinic 算法的两种优化。实际上,当前弧优化是用于保证 Dinic 时间复杂度正确性的一部分,而多路增广只是一个不影响复杂度的常数优化。

利用这三个优化 EK算法就成了Dinic算法

struct MF
{
    struct edge
    {
        int v, nxt, cap, flow;
    } e[N];

    int head[N], cnt = 0;

    int n, S, T;
    ll maxflow = 0;
    int dep[N], cur[N];

    void init()
    {
        memset(head, -1, sizeof head);
        cnt = 0;
    }

    void addedge(int u, int v, int w)
    {
        e[cnt] = {v, head[u], w, 0};
        head[u] = cnt++;
        e[cnt] = {u, head[v], 0, 0};
        head[v] = cnt++;
    }

    bool bfs()//建分层图
    {
        queue<int> q;
        memset(dep, 0, sizeof(int) * (n + 1));

        dep[S] = 1;
        q.push(S);
        while (q.size())
        {
            int u = q.front();
            q.pop();
            for (int i = head[u]; ~i; i = e[i].nxt)
            {
                int v = e[i].v;
                if ((!dep[v]) && (e[i].cap > e[i].flow))
                {
                    dep[v] = dep[u] + 1;
                    q.push(v);
                }
            }
        }
        return dep[T];
    }

    int dfs(int u, int flow)
    {
        if ((u == T) || (!flow)) return flow;

        int ret = 0;
        for (int& i = cur[u]; ~i; i = e[i].nxt)//当前弧优化
        {
            int v = e[i].v, d;
            if ((dep[v] == dep[u] + 1) &&(d = dfs(v, min(flow - ret, e[i].cap - e[i].flow))))
            {
                ret += d;
                e[i].flow += d;
                e[i ^ 1].flow -= d;
                if (ret == flow) return ret;
            }
        }
        if(ret!=flow) dep[u]=-1;//-1优化
        return ret;
    }

    void dinic()
    {
        while (bfs())
        {
            memcpy(cur, head, sizeof(int) * (n + 1));
            maxflow += dfs(S, INF);
        }
    }
} mf;

例题

养猪

尼克在一家养猪场工作,这家养猪场共有M间锁起来的猪舍,由于猪舍的钥匙都给了客户,所以尼克没有办法打开这些猪舍,客户们从早上开始一个接一个来购买生猪,他们到达后首先用手中的钥匙打开他所能打开的全部猪舍,然后从中选取他要买的生猪,尼克可以在此期间将打开的猪舍中的猪调整到其它开着的猪舍中,每个猪舍能存放的猪的数量是没有任何限制的。买完猪后客户会将他打开的猪舍关上。

好在尼克事先知道每位客户手中有哪些钥匙,要买多少猪,以及客户到来的先后次序。请你写一个程序,帮助尼克求出最多能卖出多少头生猪。


这题的主要问题是每次被打开的房间内的猪可以相互移动。为了解决这个问题,不妨换个角度来思考:实际上如果某客户取的猪是以前从其他的房间过来的,那么是不是可以就让这个客户到它原先所在的房间去取呢?即猪本身并不移动,而是钥匙重新分配,即客户拥有之前来的客户的某些钥匙。

例如客户1有3和4的钥匙,客户2有4和5的钥匙,那么客户2就等价拥有3、4和5这3把钥匙。又如果客户3有5和6的钥匙,他又等价拥有3、4、5和6的钥匙。

也就是说:只要某客户拥有的钥匙和之前的某些客户(他们的钥匙也有可能是别人给的)有交集,那么那些客户就相当于把他们的钥匙都给了这个客户。

这样按照顺序预处理后,客户前后到来的有序限制和猪可以移动的不利因素就不存在了,构图后求最大流即可。构图方式是:图中的点有客户和房间,增加源点和汇点,源点到房间连边,容量为房内猪的数量;客户到汇点连边,容量为客户的需求;房间到客户,只要客户有该房间的钥匙就连边,容量为无穷大。

P3163 [CQOI2014] 危桥


网络流模型

当然 很多问题都能用网络流解决

关键在于如何建图

通常通过最小割转化为最大流

二选一模型

建模方式:

源点向每个物品连边,边权为收益,割掉该边表示不选该物品;

每个物品向汇点连边,边权为代价,割掉该边表示选该物品;

物品之间连边,边权和意义视约束而定。

最终答案=总收益-最小割。

例如P1646 [国家集训队] happiness

最大权闭合子图模型

给定一张有向图,求一个权值和最大的点集S,使得S中任意点可达的点都在S内。

建模方式:

源点向点权为正的点连边,边权为点权,割掉该边表示不选该点;

点权为负的点向汇点连边,边权为点权的绝对值,割掉该边表示选该点;

将原图中的边照搬到新图上,边权为inf。

最终答案=正点权和-最小割。


费用流

在最大流中 我们贪心地在残量网络上寻找增广路增广

现在多了费用的情况下

我们应该在残量网络上沿着费用最小的路径增广

此时残量网络上反向边的费用应该是原边费用的相反数

也就是说会出现负边权

所以我们应该使用SPFA寻找最短路

所以算法的核心是最短路与最大流操作交替进行

在求最短路的时候顺便把图分层 可以进行多路增广

同样的 可以用当前弧优化-1优化

#include <bits/stdc++.h>

using namespace std;
const int N = 5e3 + 5, M = 1e5 + 5;
const int INF = 0x3f3f3f3f;
int n, m, tot = 1, head[N], cur[N], ter[M], nxt[M], cap[M], cost[M], dis[N], ret;
bool vis[N];

void add(int u, int v, int w, int c)
{
    ter[++tot] = v;
    nxt[tot] = head[u];
    head[u] = tot;
    cap[tot] = w;
    cost[tot] = c;
}

void addedge(int u, int v, int w, int c)
{
    add(u, v, w, c);
    add(v, u, 0, -c);
}

bool spfa(int s, int t)
{
    memset(dis, 0x3f, sizeof(dis));
    memcpy(cur, head, sizeof(head));
    std::queue<int> q;
    q.push(s), dis[s] = 0, vis[s] = 1;
    while (!q.empty())
    {
        int u = q.front();
        q.pop(), vis[u] = 0;
        for (int i = head[u]; i; i = nxt[i])
        {
            int v = ter[i];
            if (cap[i] && dis[v] > dis[u] + cost[i])
            {
                dis[v] = dis[u] + cost[i];
                if (!vis[v]) q.push(v), vis[v] = 1;
            }
        }
    }
    return dis[t] != INF;
}

int dfs(int u, int t, int flow)
{
    if (u == t) return flow;
    vis[u] = 1;
    int ans = 0;
    for (int &i = cur[u]; i && ans < flow; i = nxt[i])
    {
        int v = ter[i];
        if (!vis[v] && cap[i] && dis[v] == dis[u] + cost[i])
        {
            int x = dfs(v, t, std::min(cap[i], flow - ans));
            if (x) ret += x * cost[i], cap[i] -= x, cap[i ^ 1] += x, ans += x;
        }
    }
    vis[u] = 0;
    return ans;
}

int mcmf(int s, int t)
{
    int ans = 0;
    while (spfa(s, t))
    {
        int x;
        while ((x = dfs(s, t, INF))) ans += x;
    }
    return ans;
}

int main()
{
    int s, t;
    scanf("%d%d%d%d", &n, &m, &s, &t);
    while (m--)
    {
        int u, v, w, c;
        scanf("%d%d%d%d", &u, &v, &w, &c);
        addedge(u, v, w, c);
    }
    int ans = mcmf(s, t);
    printf("%d %d\n", ans, ret);
    return 0;
}

例题

P2223 [HNOI2001] 软件开发

芯片难题 Chips Challenge

P4068 [SDOI2016] 数字配对

这回真写完了 撒花撒花


引用来源

网络流 - 知乎 (zhihu.com)

最大流 - OI Wiki (oi-wiki.org)

【知识点】网络流常见模型

费用流 - OI Wiki (oi-wiki.org)

信息学中“序”的浅析 - 桃子在路上 - 博客园 (cnblogs.com)