网络流

网络流

最大流

基本概念

  1. 流网络
    不考虑反向边

  2. 可行流
    不考虑反向边
    (1)两个条件:容量限制、流量守恒
    (2)可行流的流量指从源点流出的流量 - 流入源点的流量
    (3)最大流是指最大可行流

  3. 残留网络
    考虑反向边,残留网络的可行流f' + 原图的可行流f = 原题的另一个可行流
    (1) \(|f' + f| = |f'| + |f|\)
    (2) \(|f'|\) 可能是负数

  4. 增广路径


  5. (1)割的定义
    (2)割的容量,不考虑反向边,“最小割”是指容量最小的割。
    (3)割的流量,考虑反向边,\(f(S, T) <= c(S, T)\)
    (4)对于任意可行流\(f\),任意割\([S, T],|f| = f(S, T)\)
    (5)对于任意可行流\(f\),任意割\([S, T],|f| <= c(S, T)\)

  6. 最大流最小割定理
    (1)可行流f是最大流
    (2)可行流f的残留网络中不存在增广路

  7. 二分图的最大独立集
    二分图的最大独立集 \(=\) 点数 \(-\) 匹配数

算法

1、Edmonds-Karp 动能算法 \(O(VE^2)\)

每次对一张网络流图,每次在残量网络中找到一条流量不为 0 的路径,对其进行增广。
由于其效率过于低下,在 OI 中几乎用不到。

P3376 【模板】网络最大流

EK算法
const int N = 1010, M = 20010, INF = 1e8;

int n, m, S, T;
int h[N], e[M], f[M], ne[M], idx;
int d[N], q[N], pre[M];
bool st[N];

void add(int a, int b, int c)
{
    e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
}

bool bfs()
{
    int hh = 0, tt = 0;
    memset(st, 0 ,sizeof st);
    q[0] = S, st[S] = 1, d[S] = INF;
    while (hh <= tt)
    {
        int t = q[hh ++ ];
        for (int i = h[t]; ~i; i = ne[i])
        {
            int ver = e[i];
            if (!st[ver] && f[i])
            {
                st[ver] = 1;
                d[ver] = min(d[t], f[i]);
                pre[ver] = i;
                if (ver == T) return true;
                q[ ++ tt] = ver;
            }
        }
    }
    return false;
}

int EK()
{
    int r = 0;
    while (bfs())
    {
        r += d[T];
        for (int i = T; i != S; i  = e[pre[i] ^ 1])
            f[pre[i]] -= d[T], f[pre[i] ^ 1] += d[T];
    }
    return r;
}

·
2、Dinic \(O(V^2E)\)

在 EK 算法基础上进行了改进,优化了寻找路径的方式,同时每次对多条路径增广。
具体来说:Dinic 算法每次增广前会先跑一遍 bfs,对残量网络按到 S 距离分层,只
寻找满足 \(d_v = d_u + 1\) 的路径。
同时我们每次不再只增广一条路,而是对所有满足条件的路径同时增广。
当前弧优化:在每次增广时对每个点维护一个指针,它指向最靠前的满足增广条件
的边 \((d_v = d_u + 1\)\(f_{u,v} > 0)\)
由于 Dinic 好写,并且速度较快,于是它是 OI 中最常使用的最大流算法。

P3376 【模板】网络最大流

Dinic
const int N = 10010, M = 2e5 + 10, INF = 1e8;

int n, m, S, T;
int h[N], e[M], f[M], ne[M], idx;
int q[N], d[N], cur[N];

void add(int a, int b, int c)
{
    e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
}

bool bfs()
{
    int hh = 0, tt = 0;
    memset(d, -1, sizeof d);
    q[0] = S, d[S] = 0, cur[S] = h[S];
    while (hh <= tt)
    {
        int t = q[hh ++ ];
        for (int i = h[t]; ~i; i = ne[i])
        {
            int ver = e[i];
            if (d[ver] == -1 && f[i])
            {
                d[ver] = d[t] + 1;
                cur[ver] = h[ver];
                if (ver == T) return true;
                q[ ++ tt] = ver;
            }
        }
    }
    return false;
}

int find(int u, int limit)
{
    if (u == T) return limit;
    int flow = 0;
    for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    {
        cur[u] = i;
        int ver = e[i];
        if (d[ver] == d[u] + 1 && f[i])
        {
            int t = find(ver, min(f[i], limit - flow));
            if (!t) d[ver] = -1;
            f[i] -= t, f[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

int dinic()
{
    int r = 0, flow;
    while (bfs())
        while (flow = find(S, INF)) r += flow;
    return r;
}

·

3、*最高标号预流推进算法 HLPP \(O(n^2\sqrt{m})\)

P4722 【模板】最大流 加强版 / 预流推进

Highest Label Preflow Push
const int N = 1300, M = 241000, INF = 0x3f3f3f3f;

int n, m, S, T;
int h[N], e[M], ne[M], f[M], idx;
int ht[N], ex[N], gap[N], q[N];
stack<int> B[N];
int level;

void add(int a, int b, int c)
{
    e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
}

bool bfs()
{
    memset(ht, 0x3f, sizeof ht);
    int hh = 0, tt = 0;
    q[0] = T, ht[T] = 0;
    while (hh <= tt)
    {
        int t = q[hh ++ ];
        for (int i = h[t]; ~i; i = ne[i])
        {
            int ver = e[i];
            if (f[i ^ 1] && ht[ver] > ht[t] + 1) ht[ver] = ht[t] + 1, q[ ++ tt] = ver;
        }
    }
    return ht[S] != INF;
}

int push(int u)
{
    bool init = u == S;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int ver = e[i], w = f[i];
        if (!w || !init && ht[u] != ht[ver] + 1) continue;
        int k = init ? w : min(w, ex[u]);
        if (ver != S && ver != T && !ex[ver])B[ht[ver]].push(ver), level = max(level, ht[ver]);
        ex[u] -= k, ex[ver] += k, f[i] -= k, f[i ^ 1] += k;
        if (!ex[u]) return 0;
    }
    return 1;
}

void relabel(int u)
{
    ht[u] = INF;
    for (int i = h[u]; ~i; i = ne[i])
        if (f[i]) ht[u] = min(ht[u], ht[e[i]]);
    if ( ++ ht[u] < n);
        B[ht[u]].push(u);
        level = max(level, ht[u]);
        gap[ht[u]] ++ ;
}

int select()
{
    while (B[level].size() == 0 && level > -1) level -- ;
    return level == -1 ? 0 : B[level]. top();
}

int HLPP()
{
    if (!bfs()) return 0;
    memset(gap, 0, sizeof gap);
    for (int i = 1; i <= n; i ++ )
        if (ht[i] != INF) gap[ht[i]] ++ ;
    ht[S] = n;
    push(S);
    int u;
    while (u = select())
    {
        B[level].pop();
        if (push(u))
        {
            if (!--gap[ht[u]])
                for (int i = 1; i <= n; i ++ )
                if (i != S && i != T && ht[i] > ht[u] && ht[i] < n + 1)
                ht[i] = n + 1;
            relabel(u);
        }
    }
    return ex[T];
}
·

应用

1、二分图

(1)二分图匹配
飞行员配对方案问题
[TJOI2011] 卡片

(2)二分图多重匹配
圆桌问题
试题库问题

2、上下界网络流

(1)无源汇上下界可行流(上下界循环流)

上下界循环流:在网络流基础上,每条边多给一个下界,表示至少流这么多。
先强制每条边流下界这么多。建立超级源汇点 \(S, t\),令 \(d_u\)\(u\) 的流出量-流入量,
如果 \(d_u < 0\),那么连 \((S, u, d_u)\),否则 \(d_u < 0\)\((u, T, −d_u)\)
最后跑一边最大流,检查一下是否满流。
正确性:这种做法保证了流量平衡,并且如果有解一定可以按如上方式构造。

无源汇上下界可行流

code
const int N = 210, M = (10200 + N) * 2, INF = 1e8;

int n, m, S, T;
int h[N], e[M], ne[M], f[M], l[M], idx;
int q[N], d[N], cur[N], A[N];

void add(int a, int b, int c, int d)
{
    e[idx] = b, ne[idx] = h[a], f[idx] = d - c, l[idx] = c, h[a] = idx ++;
    e[idx] = a, ne[idx] = h[b], f[idx] = 0, h[b] = idx ++;
}

bool bfs()
{
    memset(d, -1, sizeof d);
    q[0] = S, d[S] = 0, cur[S] = h[S];
    int hh = 0, tt = 0;
    while(hh <= tt)
    {
        int t = q[hh ++];
        for(int i = h[t]; ~i; i = ne[i])
        {
            int ver = e[i];
            if(d[ver] == -1 && f[i])
            {
                d[ver] = d[t] + 1;
                cur[ver] = h[ver];
                if(ver == T) return true;
                q[++ tt] = ver;
            }
        }
    }
    return false;
}

int find(int u, int limit)
{
    if(u == T) return limit;
    int flow = 0;
    for(int i = cur[u]; ~i && flow < limit; i = ne[i])
    {
        cur[u] = i;
        int ver = e[i];
        if(d[ver] == d[u] + 1 && f[i])
        {
            int t = find(ver, min(f[i], limit - flow));
            if(!t) d[ver] = -1;
            f[i] -= t, f[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

int dinic()
{
    int res = 0, flow;
    while(bfs())
        while(flow = find(S, INF))
            res += flow;
    return res;
}

int main()
{
    scanf("%d%d", &n, &m);
    S = 0, T = n + 1;
    memset(h, -1, sizeof h);
    for(int i = 1, a, b, c, d; i <= m; i ++)
    {
        scanf("%d%d%d%d", &a, &b, &c, &d);
        add(a, b, c, d);
        A[a] -=c, A[b] += c;
    }
    int tot = 0;
    for(int i = 1; i <= n; i ++)
        if(A[i] > 0) add(S, i, 0, A[i]), tot += A[i];
        else if(A[i] < 0) add(i, T, 0, -A[i]);
    if(dinic() != tot)puts("NO");
    else
    {
        puts("YES");
        for(int i = 0; i < m * 2; i += 2)
            printf("%d\n", f[i ^ 1] + l[i]);
    }
    return 0;
}

·

(2)有源汇上下界最大流

连接 \((t, s, ∞)\),先跑一遍上下界循环流。然后再把 \((t, s)\) 边断掉,再跑一遍 \(s\)\(t\)
最大流。
大致证明:首先我们求出了一组合法的流,然后在这组流的基础上增广。容易发现一定可以增广出最大流。

有源汇上下界最大流

code
const int N = 210, M = (N + 10000) * 2, INF = 1e8;

int n, m, s, t, S, T;
int h[N], e[M], ne[M], f[M], idx;
int q[N], d[N], cur[N], A[N];

void add(int a, int b, int c)
{
    e[idx] = b, ne[idx] = h[a], f[idx] = c ,h[a] = idx ++;
    e[idx] = a, ne[idx] = h[b], f[idx] = 0, h[b] = idx ++;
}

bool bfs()
{
    memset(d, -1, sizeof d);
    q[0] = S, d[S] = 0, cur[S] = h[S];
    int hh = 0, tt = 0;
    while(hh <= tt)
    {
        int t = q[hh ++];
        for(int i = h[t]; ~i; i = ne[i])
        {
            int ver = e[i];
            if(d[ver] == -1 && f[i])
            {
                d[ver] = d[t] + 1;
                cur[ver] = h[ver];
                if(ver == T) return true;
                q[++ tt] = ver;
            }
        }
    }
    return false;
}

int find(int u, int limit)
{
    if(u == T) return limit;
    int flow = 0;
    for(int i = cur[u]; ~i && flow < limit; i = ne[i])
    {
        int ver = e[i];
        if(d[ver] == d[u] + 1 && f[i])
        {
            int t = find(ver, min(f[i], limit - flow));
            if(!t) d[ver] = -1;
            f[i] -= t, f[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

int dinic()
{
    int res = 0, flow;
    while(bfs())
        while(flow = find(S, INF))
        res += flow;
    return res;
}

int main()
{
    scanf("%d%d%d%d", &n, &m, &s, &t);
    S = 0, T = n + 1;
    memset(h, -1, sizeof h);
    while(m --)
    {
        int a, b, c, d;
        scanf("%d%d%d%d", &a, &b, &c, &d);
        add(a, b, d - c);
        A[a] -=c, A[b] += c;
    }
    int tot = 0;
    for(int i = 1; i <= n; i ++)
        if(A[i] > 0) add(S, i, A[i]), tot += A[i];
        else if(A[i] < 0) add(i, T, -A[i]);
    add(t, s, INF);
    if(dinic() < tot)puts("No Solution");
    else
    {
        int res = f[idx - 1];
        S = s, T = t;
        f[idx - 1] = f[idx - 2] = 0;
        printf("%d\n", res + dinic());
    }
    return 0;
}

·

(3)有源汇上下界最小流

连接 \((t, s, ∞)\),先跑一遍上下界循环流。然后再把 \((t, s)\) 边断掉,再跑一遍 \(t\)\(s\)
最大流。
大致证明:首先我们求出了一组合法的流,然后在这组流的基础上退流。容易发现一定可以退出最小流。

有源汇上下界最小流

code
const int N = 500010, M = (N + 125010) * 2, INF = 2147483647;

int n, m, s, t, S, T;
int h[N], e[M], ne[M], f[M], idx;
int q[N], d[N], cur[N], A[N];

void add(int a, int b, int c)
{
    e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++;
    e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++;
}

bool bfs()
{
    memset(d, -1, sizeof d);
    q[0] = S, d[S] = 0, cur[S] = h[S];
    int hh = 0, tt = 0;
    while(hh <= tt)
    {
        int t = q[hh ++];
        for(int i = h[t]; ~i; i = ne[i])
        {
            int ver = e[i];
            if(d[ver] == -1 && f[i])
            {
                d[ver] = d[t] + 1;
                cur[ver] = h[ver];
                if(ver == T) return true;
                q[++ tt] = ver;
            }
        }
    }
    return false;
}

int find(int u, int limit)
{
    if(u == T) return limit;
    int flow = 0;
    for(int i = cur[u]; ~i && flow < limit; i = ne[i])
    {
        cur[u] = i;
        int ver = e[i];
        if(d[ver] == d[u] + 1 && f[i])
        {
            int t = find(ver, min(f[i], limit - flow));
            if(!t) cur[ver] = -1;
            f[i] -= t, f[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

int dinic()
{
    int res = 0, flow;
    while(bfs())
        while(flow = find(S, INF))
        res += flow;
    return res;
}

int main()
{
    memset(h, -1, sizeof h);
    scanf("%d%d%d%d", &n, &m ,&s, &t);
    S = 0, T = n + 1;
    for(int i = 1, a, b, c, d; i <= m; i ++)
    {
        scanf("%d%d%d%d", &a, &b, &c, &d);
        add(a, b, d - c);
        A[a] -= c, A[b] += c;
    }
    int tot = 0;
    for(int i = 1; i <= n; i ++)
        if(A[i] > 0) add(S, i, A[i]), tot += A[i];
        else if(A[i] < 0) add(i, T, -A[i]);
    add(t, s, INF);
    if(dinic() < tot) puts("No Solution");
    else
    {
        int res = f[idx - 1];
        S = t, T = s;
        f[idx - 1] = f[idx - 2] = 0;
        printf("%d\n", res - dinic());
    }
    return 0;
}

·

4、最小路径覆盖

给定一张 DAG,要求用最少的路径覆盖整张图。数据范围:n, m ≤ 100。

将每个点拆为两个点:入点和出点。
如果原图存在一条边 \((u, v)\),那么连一条从 \(u\) 的出点向 \(v\) 的入点的流量为 \(1\) 的边,
并且从 \(s\) 向所有出点连流量为 \(1\) 的边,从所有入点向 \(t\) 连流量为 \(1\) 的边。
\(n − flow\) 就是答案。

5、最大权闭合子图

给定一个有向图,点有点权。如果一个点 \(u\) 被选了,所有 \(u\) 的出边指向的点 \(v\) 也必须选。求最大收益。(点权可以为负数)

利用最小割来解决。先假设所有正点权都选。正点权连到 \(S\),表示放弃这个点,负点权连到 \(T\),表示选择这个点。原图中所有 \((u, v)\) 连接一条 \((u, v, ∞)\) 的边。

太空飞行计划问题

[CEOI2008] order
把原来的 \(inf\) 边改成租用的代价,来表示可以付出一定代价不选后面的点即可。

费用流

SPFA求最小费用最大流

【模板】最小费用最大流

code
const int N = 5010, M = 100010, INF = 1e8;

int n, m, S, T;
int h[N], e[M], f[M], w[M], ne[M], idx;
int q[N], d[N], pre[N], incf[N];
bool st[N];

void add(int a, int b, int c, int d)
{
    e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx ++;
    e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx ++;
}

bool spfa()
{
    int hh = 0, tt = 1;
    memset(d, 0x3f, sizeof d);
    memset(incf, 0, sizeof incf);
    q[0] = S, d[S] = 0, incf[S] = INF;
    while(hh != tt)
    {
        int t = q[hh ++];
        if(hh == N) hh = 0;
        st[t] = false;
        for(int i = h[t]; ~i; i = ne[i])
        {
            int ver = e[i];
            if(f[i] && d[ver] > d[t] + w[i])
            {
                d[ver] = d[t] + w[i];
                pre[ver] = i;
                incf[ver] = min(f[i], incf[t]);
                if(!st[ver])
                {
                    q[tt ++] = ver;
                    if(tt == N) tt = 0;
                    st[ver] = true;
                }
            }
        }
    }
    return incf[T] > 0;
}

void EK(int &flow, int &cost)
{
    flow = cost = 0;
    while(spfa())
    {
        int t = incf[T];
        flow += t, cost += t * d[T];
        for(int i = T; i != S; i = e[pre[i] ^ 1])
        {
            f[pre[i]] -= t;
            f[pre[i] ^ 1] += t;
        }
    }
}

·

posted @ 2022-07-21 22:03  kroyosh  阅读(66)  评论(0)    收藏  举报