网络流专题练习

最大流

1、最大流之二分图匹配

  • 网络流求二分图最大匹配
    最 大 流 = 最 大 匹 配 数 最大流=最大匹配数 =
    建图:
    设二分图的两个点集分别是 V 1 , V 2 V_1,V_2 V1,V2
    从源点 S S S V 1 V_1 V1内的点连容量是 1 1 1边。
    V 2 V_2 V2内的点向汇点 T T T连容量是 1 1 1的边。
    V 1 V_1 V1 V 2 V_2 V2内部的边都不变,容量按题意来设定。

  • 网络流求二分图的多重匹配
    最 大 流 = 多 重 匹 配 最大流=多重匹配 =
    建图:
    设二分图的两个点集分别是 V 1 , V 2 V_1,V_2 V1,V2
    从源点 S S S V 1 V_1 V1内的点连容量是其权值的边。
    V 2 V_2 V2内的点向汇点 T T T连容量是其权值的边。
    V 1 V_1 V1 V 2 V_2 V2内部的边都不变,容量都设为 1 1 1

AcWing 2175. 飞行员配对方案问题

sol:
英国飞行员是一个点集,外籍飞行员是另一个点集,所有的边都是由外籍飞行员连向英国飞行员的,求最大匹配。
输出方案时就直接遍历原图中的所有边,如果它在流网络中是满流的,表明该边两端的点是一个匹配,输出即可。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 210, M = (N + 10000) * 2;

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

void add(int u, int v, int w)
{
    e[idx] = v, c[idx] = w, ne[idx] = head[u], head[u] = idx ++;
    e[idx] = u, c[idx] = 0, ne[idx] = head[v], head[v] = idx ++;
}

bool bfs()
{
    memset(d, -1, sizeof d);
    int hh = 0, tt = -1;
    q[++ tt] = S, d[S] = 0, cur[S] = head[S];
    while(hh <= tt)
    {
        int u = q[hh ++];
        for(int i = head[u]; ~i; i = ne[i])
        {
            int v = e[i];
            if(d[v] == -1 && c[i])
            {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if(v == T)  return true;
                q[++ tt] = v;
            }
        }
    }
    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 v = e[i];
        cur[u] = i;
        if(d[v] == d[u] + 1 && c[i])
        {
            int t = find(v, min(c[i], limit - flow));
            if(!t)  d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

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

int main()
{
#ifdef LOCAL
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif

    memset(head, -1, sizeof head);
    scanf("%d%d", &m, &n);
    S = 0, T = n + 1;
    for(int i = 1; i <= m; i ++)    add(S, i, 1);
    for(int i = m + 1; i <= n; i ++)    add(i, T, 1);

    int i, j;
    while(scanf("%d%d", &i, &j), i != -1)   add(i, j, 1);

    printf("%d\n", dinic());
    for(int i = 0; i < idx; i += 2)
        if(e[i] > m && e[i] <= n && c[i] == 0)
            printf("%d %d\n", e[i ^ 1], e[i]);

    return 0;
}
AcWing 2179. 圆桌问题

sol:
m m m个单位是一个点集,单位人数就是该点的权值, n n n个餐座是另一个点集,餐座容量是该点的权值。直接建图求多重匹配,因为一个单位只能向同一个餐座上派一个人,所以原图内部所有边的容量都是 1 1 1
最后判断是否存在合法方案时就看最大流,也就是最大匹配数是不是等于所有范围人数总和,等于就存在合法方案,否则不存在。
输出方案和上一题类似,遍历原图所有边,看是否满流。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 450, M = (150 * 270 + N) * 2;

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

void add(int u, int v, int w)
{
    e[idx] = v, c[idx] = w, ne[idx] = head[u], head[u] = idx ++;
    e[idx] = u, c[idx] = 0, ne[idx] = head[v], head[v] = idx ++;
}

bool bfs()
{
    memset(d, -1, sizeof d);
    int hh = 0, tt = -1;
    q[++ tt] = S, d[S] = 0, cur[S] = head[S];
    while(hh <= tt)
    {
        int u = q[hh ++];
        for(int i = head[u]; ~i; i = ne[i])
        {
            int v = e[i];
            if(d[v] == -1 && c[i])
            {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if(v == T)  return true;
                q[++ tt] = v;
            }
        }
    }
    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 v = e[i];
        cur[u] = i;
        if(d[v] == d[u] + 1 && c[i])
        {
            int t = find(v, min(c[i], limit - flow));
            if(!t)  d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

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

int main()
{
#ifdef LOCAL
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif

    scanf("%d%d", &m, &n);
    S = 0, T = n + m + 1;

    memset(head, -1, sizeof head);
    int tot = 0;
    for(int i = 1; i <= m; i ++)
    {
        int r; scanf("%d", &r);
        add(S, i, r);
        tot += r;
    }
    for(int i = 1; i <= n; i ++)
    {
        int c; scanf("%d", &c);
        add(m + i, T, c);
    }
    for(int i = 1; i <= m; i ++)
        for(int j = 1; j <= n; j ++)
            add(i, m + j, 1);
    
    if(tot != dinic())  puts("0");
    else
    {
        puts("1");
        for(int i = 1; i <= m; i ++)
        {
            for(int j = head[i]; ~j; j = ne[j])
                if(e[j] > m && e[j] <= m + n && !c[j])
                    printf("%d ", e[j] - m); 
            puts("");           
        }
    }

    return 0;
}

2、最大流之关键边

  • 关键边定义为 :
    可以通过增加该边的容量使得网络的最大流。
    也就是对于一个已经跑完一遍最大流后的流网络(已经不存在增广路),可以通过增加某一条边的容量使得可以重新找到一条增广路。
AcWing 2236. 伊基的故事 I - 道路重建

sol:
将原图建成一个流网络后,跑一遍最大流。然后把此时流网络中从源点 S S S沿着剩余流量大于 0 0 0的边可以走到的点标记上,从汇点 T T T沿着剩余流量大于 0 0 0的边可以走到的点标记上。
接下来,遍历原图所有的边并且该边的剩余流量是 0 0 0,如果该边的一端是源点 S S S可以走到的,另一端是汇点 T T T可以走到的,那么该边就是关键边。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 510, M = (5000 + N) * 2;

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

void add(int u, int v, int w)
{
    e[idx] = v, c[idx] = w, ne[idx] = head[u], head[u] = idx ++;
    e[idx] = u, c[idx] = 0, ne[idx] = head[v], head[v] = idx ++;
}

bool bfs()
{
    memset(d, -1, sizeof d);
    int hh = 0, tt = -1;
    q[++ tt] = S, d[S] = 0, cur[S] = head[S];
    while(hh <= tt)
    {
        int u = q[hh ++];
        for(int i = head[u]; ~i; i = ne[i])
        {
            int v = e[i];
            if(d[v] == -1 && c[i])
            {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if(v == T)  return true;
                q[++ tt] = v;
            }
        }
    }
    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 v = e[i];
        cur[u] = i;
        if(d[v] == d[u] + 1 && c[i])
        {
            int t = find(v, min(c[i], limit - flow));
            if(!t)  d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

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

bool vis[2][N];

void dfs(int u, int t)
{
    vis[t][u] = true;
    for(int i = head[u]; ~i; i = ne[i])
    {
        int j = i ^ t, v = e[i];
        //如果是从T出发,那么判断流量时其实时判断反向边的流量
        if(c[j] > 0 && !vis[t][v])
            dfs(v, t);
    }
}

int main()
{
#ifdef LOCAL
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif

    scanf("%d%d", &n, &m);
    S = 0, T = n - 1;

    memset(head, -1, sizeof head);
    for(int i = 0; i < m; i ++)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    dinic();
    dfs(S, 0);
    dfs(T, 1);

    int ans = 0;
    for(int i = 0; i < idx; i += 2)
        if(!c[i] && vis[0][e[i ^ 1]] && vis[1][e[i]])
            ans ++;
    
    printf("%d\n", ans);

    return 0;
}

3、最大流之最大流判定

AcWing 2277. 秘密挤奶机

sol:
容易发现答案具有二段性,因此二分最长边的长度。
设当前二分值为 m i d mid mid,那么原图中的所有长度大于 m i d mid mid的边都不能走,直接将流网络中的这种边的容量都设为 0 0 0就可以实现这一点,然后其他面的容量都是 1 1 1,表示该边只能走一次,然后求从 S S S T T T的最大流,最大流的值就是在该限制条件下最多的往返次数。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 210, M = 80010;

int n, m, k, S, T;
int idx, head[N], e[M], c[M], w[M], ne[M];
int q[N], d[N], cur[N];

void add(int u, int v, int _w)
{
    e[idx] = v, w[idx] = _w, ne[idx] = head[u], head[u] = idx ++;
    e[idx] = u, w[idx] = _w, ne[idx] = head[v], head[v] = idx ++;
}

bool bfs()
{
    memset(d, -1, sizeof d);
    int hh = 0, tt = -1;
    q[++ tt] = S, d[S] = 0, cur[S] = head[S];
    while(hh <= tt)
    {
        int u = q[hh ++];
        for(int i = head[u]; ~i; i = ne[i])
        {
            int v = e[i];
            if(d[v] == -1 && c[i])
            {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if(v == T)  return true;
                q[++ tt] = v;
            }
        }
    }
    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 v = e[i];
        cur[u] = i;
        if(d[v] == d[u] + 1 && c[i])
        {
            int t = find(v, min(c[i], limit - flow));
            if(!t)  d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

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

bool check(int mid)
{
    for(int i = 0; i < idx; i ++)
        if(w[i] > mid)  c[i] = 0;
        else    c[i] = 1;
    return dinic() >= k;
}

int main()
{
#ifdef LOCAL
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif

    scanf("%d%d%d", &n, &m, &k);
    S = 1, T = n;

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

    int l = 0, r = 1e6 + 1;
    while(l + 1 < r)
    {
        int mid = l + r >> 1;
        if(check(mid))  r = mid;
        else    l = mid;
    }

    printf("%d\n", r);

    return 0;
}
AcWing 2187. 星际转移问题

sol:
先做一个规定:
原题的地球编号是 0 0 0,月球是 − 1 -1 1,中间的太空站是 1 1 1~ n n n
这里做一个偏移,让地球是 1 1 1,中间太空站是 2 2 2~ n + 1 n+1 n+1,月球是 n + 2 n+2 n+2
然后让 n + = 2 n+=2 n+=2 n n n就是总点数。

按天数进行拆点,分层。
第一层的点有第一天的 1 , 2... , n 1,2...,n 1,2...,n,第二层有第二天的 1 , 2 , . . . , n 1,2,...,n 1,2,...,n,…….
然后从 1 1 1开始枚举天数 d a y day day
建图方法:
从源点 S S S先第一题的 1 1 1号点,连接容量是总人数的边。

从第 d a y day day天的 n n n号点向汇点 T T T连接容量是 + ∞ +\infty +的边。

从第 i i i天的 1 1 1号点向第 i + 1 i+1 i+1天的 1 1 1号点连接容量是 + ∞ +\infty +的边,类似的还有 2 2 2 2 2 2连,……, n n n n n n连,因为人可以选择继续留在原地。

最后一种边是太空飞船的航线边,边的容量是太空飞船的容量,这种边连的时候要判断在这一天时,太空船到了哪一个站点。

建完图后跑最大流,如果最大流等于总人数,那么该 d a y day day就是答案。
否则, d a y + + day++ day++,然后重新建图判断,(然而实际写代码没必须要重新建图,在上一次的流网络中重新加边即可,见代码)。

有一些需要注意的地方:
因为是死循环枚举,知道找到终点才停止,所以对于无解的情况坑定是单独判断。
可以发现,如果起点和终点可以联通,那么一定会有答案。
并且本题数据范围比较小,答案应该不会很大,所以该方法可过。
否则就无解。
(判断起点和终点是否联通有很多方法吗,这里用并查集)

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 20010, M = 40010;

int n, m, tot, S, T;
struct Node { int h, r, id[30]; } ship[22];//太空船
int idx, head[N], e[M], c[M], ne[M];
int q[N], d[N],cur[N];

void add(int u, int v, int w)
{
    e[idx] = v, c[idx] = w, ne[idx] = head[u], head[u] = idx ++;
    e[idx] = u, c[idx] = 0, ne[idx] = head[v], head[v] = idx ++;
}

bool bfs()
{
    memset(d, -1, sizeof d);
    int hh = 0, tt = -1;
    q[++ tt] = S, d[S] = 0, cur[S] = head[S];
    while(hh <= tt)
    {
        int u = q[hh ++];
        for(int i = head[u]; ~i; i = ne[i])
        {
            int v = e[i];
            if(d[v] == -1 && c[i])
            {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if(v == T)  return true;
                q[++ tt] = v;
            }
        }
    }
    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 v = e[i];
        cur[u] = i;
        if(d[v] == d[u] + 1 && c[i])
        {
            int t = find(v, min(c[i], limit - flow));
            if(!t)  d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

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

int pre[N];
int find(int x) { return pre[x] == x ? x : pre[x] = find(pre[x]); }

int get(int day, int x) //第day天的第x个点的编号
{
    return n * day + x + 1;
}

int main()
{
#ifdef LOCAL
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif

    memset(head, -1, sizeof head);

    scanf("%d%d%d", &n, &m, &tot);
    for(int i = 0; i <= n + 2; i ++)    pre[i] = i;

    for(int i = 0; i < m; i ++)
    {
        scanf("%d%d", &ship[i].h, &ship[i].r);
        for(int j = 0; j < ship[i].r; j ++)
        {
            int id;
            scanf("%d", &id);
            if(id == -1)    id = n + 1;
            ship[i].id[j] = ++ id;
            if(j)   pre[find(ship[i].id[j - 1])] = find(id);
        }
    }

    if(find(1) != find(n + 2))  puts("0"); //不连通
    else
    {
        n += 2;
        S = 0, T = 1;
        add(S, get(0, 1), tot);
        add(get(0, n), T, INF);

        int day = 1, res = 0;
        while(true)
        {
            add(get(day, n), T, INF);
            for(int i = 1; i <= n; i ++)
                add(get(day - 1, i), get(day, i), INF);
            for(int i = 0; i < m; i ++)
            {
                int r = ship[i].r;
                int a = ship[i].id[(day - 1) % r], b = ship[i].id[day % r];
                add(get(day - 1, a), get(day, b), ship[i].h);
            }

            res += dinic();
            if(res >= tot)  break;
            day ++;
        }

        printf("%d\n", day);
    }

    return 0;
}

4、最大流之拆点

如果题目对点有限制,一般就使用拆点的方法,比如如果有些题目要求某个点只能被使用(或被选) x x x次,那么就可以把这个点拆成一个入点与出点,然后在入点与出点之间连一条容量是 x x x的边。

AcWing 2240. 餐饮

sol:
题目一共给了三个点集,食物点集,饮料点集,奶牛点集。没头奶牛会向某些食物和某些饮料连边,每个食物和饮料都只有一份,没头奶牛也只能选择一个食物和一份饮料,求能满足的最多的奶牛的数量,满足是指该奶牛分到了一个饮料和一个食物。
建图:
从源点 S S S向所有的食物点集连容量是 1 1 1的边。

从所有的饮料点集向汇点 T T T连容量是 1 1 1的边。

奶牛作为连接它们的桥梁放在食物与饮料之间。从食物向所有它可以分配到的奶牛连边,从奶牛向所有它可以拥有的饮料连边,容量都是 1 1 1

如果就这样连会出现一个问题,就是奶牛这个点可能会被使用多次,也就是该奶牛可能会拥有多个食物或饮料,为了限制奶牛这个点,吧每一个奶牛点拆分成一个入点与一个出点,从入点向出点连接一条容量是 1 1 1的边,这样就可以保证奶牛只会被经过 1 1 1次。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 410, M = 41000;;

int n, F, D, S, T;
int idx, head[N], e[M], c[M], ne[M];
int q[N], d[N], cur[N];

void add(int u, int v, int w)
{
    e[idx] = v, c[idx] = w, ne[idx] = head[u], head[u] = idx ++;
    e[idx] = u, c[idx] = 0, ne[idx] = head[v], head[v] = idx ++;
}

bool bfs()
{
    memset(d, -1, sizeof d);
    int hh = 0, tt = -1;
    q[++ tt] = S, d[S] = 0, cur[S] = head[S];
    while(hh <= tt)
    {
        int u = q[hh ++];
        for(int i = head[u]; ~i; i = ne[i])
        {
            int v = e[i];
            if(d[v] == -1 && c[i])
            {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if(v == T)  return true;
                q[++ tt] = v;
            }
        }
    }
    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 v = e[i];
        cur[u] = i;
        if(d[v] == d[u] + 1 && c[i])
        {
            int t = find(v, min(c[i], limit - flow));
            if(!t)  d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

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

int main()
{
#ifdef LOCAL
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif

    scanf("%d%d%d", &n, &F, &D);
    S = 0, T = 2 * n + F + D + 1;
    
    memset(head, -1, sizeof head);
    for(int i = 1; i <= F; i ++)    add(S, i, 1);
    for(int i = 1; i <= D; i ++)    add(F + 2 * n + i, T, 1);

    for(int i = 1; i <= n; i ++)
    {
        int a, b, c;
        scanf("%d%d", &a, &b);
        add(F + i, F + n + i, 1);
        while(a --)
        {
            scanf("%d", &c);
            add(c, F + i, 1);
        }
        while(b --)
        {
            scanf("%d", &c);
            add(F + n + i, F + 2 * n + c, 1);
        }
    }

    printf("%d\n", dinic());

    return 0;
}
P2766 最长不下降子序列问题

sol:
先求出最长不下降子序列长度 k k k f [ i ] f[i] f[i]表示一点 i i i结尾的最长不下降子序列的长度。

对于第二问,每个点只能被使用 1 1 1次,所以将每个点拆成入点与出点,从入点向出点连接一条容量是 1 1 1的边。然后建图,建图方法是:
从源点 S S S向所有 f [ i ] = 1 f[i]=1 f[i]=1的点连容量是 1 1 1的边。
从所有 f [ i ] = k f[i]=k f[i]=k的点向汇点 T T T连接容量是 1 1 1的边。
对于任意的 i , j i,j i,j i < j i<j i<j,如果 f [ i ] + 1 = f [ j ] f[i]+1=f[j] f[i]+1=f[j]并且 a [ j ] < = a [ i ] a[j]<=a[i] a[j]<=a[i],那么从 i i i j j j连一条容量是 1 1 1的边。
然后跑最大流就是第二问的答案。

对于第三问,由于点 1 1 1与点 n n n可以多次使用,不在受限,那么就把它们的入点向出点连接的边以及和这两个点相关的一些边的容量设为 + ∞ +\infty +
然后跑最大流就是第三问的答案。

sol:

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 1100, M = 53000;

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

void add(int u, int v, int w)
{
    e[idx] = v, c[idx] = w, ne[idx] = head[u], head[u] = idx ++;
    e[idx] = u, c[idx] = 0, ne[idx] = head[v], head[v] = idx ++;
}

bool bfs()
{
    memset(d, -1, sizeof d);
    int hh = 0, tt = -1;
    q[++ tt] = S, d[S] = 0, cur[S] = head[S];
    while(hh <= tt)
    {
        int u = q[hh ++];
        for(int i = head[u]; ~i; i = ne[i])
        {
            int v = e[i];
            if(d[v] == -1 && c[i])
            {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if(v == T)  return true;
                q[++ tt] = v;
            }
        }
    }
    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 v = e[i];
        cur[u] = i;
        if(d[v] == d[u] + 1 && c[i])
        {
            int t = find(v, min(c[i], limit - flow));
            if(!t)  d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

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

int main()
{
#ifdef LOCAL
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif

    scanf("%d", &n);
    for(int i = 1; i <= n; i ++)    scanf("%d", &a[i]);
    S = 0, T = 2 * n + 1;
    
    int k = 0;
    memset(head, -1, sizeof head);
    for(int i = 1; i <= n; i ++)
    {
        f[i] = 1;
        for(int j = i - 1; j > 0; j --)
            if(a[j] <= a[i])    f[i] = max(f[i], f[j] + 1);
        
        add(i, n + i, 1);
        k = max(k, f[i]);
    }
    
    printf("%d\n", k);
    if(k == 1)
    {
        printf("%d\n%d\n", n, n);
        return 0;
    }

    for(int i = 1; i <= n; i ++)
    {
        if(f[i] == 1)   add(S, i, 1);
        if(f[i] == k)   add(n + i, T, 1);
        for(int j = 1; j < i; j ++)
            if(a[j] <= a[i] && f[j] + 1 == f[i])    add(n + j, i, 1);
    }
    int ans = dinic();
    printf("%d\n", ans);

    for(int i = 0; i < idx; i += 2)
    {
        int u = e[i ^ 1], v = e[i];
        if(u == S && v == 1)    c[i] = INF;
        else if(u == 1 && v == n + 1)   c[i] = INF;
        else if(u == n && v == n + n)   c[i] = INF;
        else if(u == n + n && v == T)   c[i] = INF;
    }
    printf("%d\n", ans + dinic());

    return 0;
}
POJ3498企鹅游行

sol:
首先每一个浮冰代表一个点,每个浮冰都有一个承载的跳跃次数,也就是每个点都有一个被选择次数的限制,所以需要拆点。
建图:
对每一个点进行拆点,从入点向出点连接一条容量是其跳跃次数的边。
从源点 S S S先所有有企鹅的浮冰连接容量是企鹅数量的边。
由于最终不知道企鹅会汇聚在哪些点,所以要枚举汇点,如果枚举的汇点可行,答案 + 1 +1 +1

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 210, M = 21000;
const double eps = 1e-8;

int n, m, S, T, x[N], y[N];
double D;
int idx, head[N], e[M], c[M], ne[M];
int q[N], d[N], cur[N];

void add(int u, int v, int w)
{
    e[idx] = v, c[idx] = w, ne[idx] = head[u], head[u] = idx ++;
    e[idx] = u, c[idx] = 0, ne[idx] = head[v], head[v] = idx ++;
}

bool bfs()
{
    memset(d, -1, sizeof d);
    int hh = 0, tt = -1;
    q[++ tt] = S, d[S] = 0, cur[S] = head[S];
    while(hh <= tt)
    {
        int u = q[hh ++];
        for(int i = head[u]; ~i; i = ne[i])
        {
            int v = e[i];
            if(d[v] == -1 && c[i])
            {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if(v == T)  return true;
                q[++ tt] = v;
            }
        }
    }
    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 v = e[i];
        cur[u] = i;
        if(d[v] == d[u] + 1 && c[i])
        {
            int t = find(v, min(c[i], limit - flow));
            if(!t)  d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

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

bool check(int a, int b)
{
    double dx = x[a] - x[b], dy = y[a] - y[b];
    return dx * dx + dy * dy < D * D + eps;
}

void solve()
{
    memset(head, -1, sizeof head);
    idx = 0;

    scanf("%d%lf", &n, &D);
    S = 0;
    int tot = 0;
    for(int i = 1; i <= n; i ++)
    {
        int a, b;
        scanf("%d%d%d%d", &x[i], &y[i], &a, &b);
        add(S, i, a);
        add(i, n + i, b);
        tot += a;
    }

    for(int i = 1; i <= n; i ++)
        for(int j = i + 1; j <= n; j ++)
            if(check(i, j))
            {
                add(n + i, j, INF);
                add(n + j, i, INF);
            }
    
    int cnt = 0;
    for(T = 1; T <= n; T ++)
    {
        for(int j = 0; j < idx; j += 2)
        {
            c[j] += c[j ^ 1];
            c[j ^ 1] = 0;
        }

        if(dinic() == tot)
        {
            printf("%d ", T - 1);
            cnt ++;
        }
    }

    puts(cnt ? "" : "-1");
}

int main()
{
#ifdef LOCAL
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif

    int t; scanf("%d", &t);
    while(t --) solve();

    return 0;
}

最小割

1、最小割之直接应用

AcWing 2279.网络战争

sol:
∑ e ∈ C w e ∣ C ∣ \frac{\sum_{e \in C}w_e}{|C|} CeCwe的最小值, 01 01 01分数规划问题。二分枚举答案 m i d mid mid
如果 ∑ e ∈ C w e ∣ C ∣ < = m i d \frac{\sum_{e \in C}w_e}{|C|}<=mid CeCwe<=mid r = m i d r=mid r=mid;否则 l = m i d l=mid l=mid
∑ e ∈ C w e ∣ C ∣ < = m i d \frac{\sum_{e \in C}w_e}{|C|}<=mid CeCwe<=mid 等价于 ∑ e ∈ C ( w e − m i d ) < = 0 \sum_{e \in C}(w_e-mid)<=0 eC(wemid)<=0
判断该式子是不是大于等于零就就是求一个最小的割,看它是否小于0,。
需要注意的是,对于每一个枚举的 m i d mid mid,原图中的所有边的容量都要减去 m i d mid mid,如果某些边减去 m i d mid mid之后小于 0 0 0,那么直接将其加上就行,然后从流网络中去除,因为容量不能为负。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 110, M = 1000;
const double eps = 1e-8;

int n, m, S, T;
int idx, head[N], e[M], ne[M], w[M];
double c[M];
int q[N], d[N], cur[N];

void add(int u, int v, int _w)
{
    e[idx] = v, w[idx] = _w, ne[idx] = head[u], head[u] = idx ++;
    e[idx] = u, w[idx] = _w, ne[idx] = head[v], head[v] = idx ++;
}

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

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

double dinic(double mid)
{
    double res = 0;
    for(int i = 0; i < idx; i += 2)
        if(w[i] <= mid)
        {
            res += w[i] - mid;
            c[i] = c[i ^ 1] = 0;
        }
        else    c[i] = c[i ^ 1] = w[i] - mid;

    double r = 0, flow;
    while(bfs())    while((flow = find(S, INF)) > 0)  r += flow;
    return r + res;
}

int main()
{
#ifdef LOCAL
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif

    scanf("%d%d%d%d", &n, &m, &S, &T);
    memset(head, -1, sizeof head);
    for(int i = 0; i < m; i ++)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    double l = 0, r = 1e7;
    while(r - l > eps)
    {
        double mid = (l + r) / 2;
        if(dinic(mid) < 0)    r = mid;
        else    l = mid;
    }

    printf("%.2lf\n", r);

    return 0;
}
AcWing2280.最优标号

首先,直接求每一个点的值不好求,联系到本题要求的是异或运算,而异或运算都是按位操作的,对于所有的数,不同二进制位上的值是不会互相影响的,所以我们可以求出来每个数的每一位上是 0 0 0还是 1 1 1,求出来所有的位后,计算该位的贡献。

再来看如何求某一位上的值是 0 0 0还是 1 1 1。因为只有 0 0 0 1 1 1两个取值,所以等价于将这些点划分成两个集合,由异或的性质不难发现,位于同一个集合里的点异或后的值都是 0 0 0,产生贡献的边一定都是为与两个集合之间的边。本问题是求最小值,也就是要使得位于两个集合之间的边尽可能少。
类比最小割,最小割也是将所有点集划分成两个集合 S S S T T T,割的容量是位于两个集合之间的边的容量,且值是最小的。
建图:
对于原图上已经有值的点,如果该位上的值是 0 0 0,就从源点 S S S向该点连一条容量是 + ∞ +\infty +的边,因为该边不能被割去;否则就从该点向汇点 T T T连一条容量是 + ∞ +\infty +的边。
然后对于原图中的边,其容量都是 1 1 1,若果这种边被割去,表示这该边两端的顶点被划分到了不同的集合。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 510, M = 7000;

int n, m, S, T, p[N];
struct Node { int u, v; } edges[M];
int idx, head[N], e[M], c[M], ne[M];
int q[N], d[N], cur[N];

void add(int u, int v, int w1, int w2)
{
    e[idx] = v, c[idx] = w1, ne[idx] = head[u], head[u] = idx ++;
    e[idx] = u, c[idx] = w2, ne[idx] = head[v], head[v] = idx ++;
}

bool bfs()
{
    memset(d, -1, sizeof d);
    int hh = 0, tt = -1;
    q[++ tt] = S, d[S] = 0, cur[S] = head[S];
    while(hh <= tt)
    {
        int u = q[hh ++];
        for(int i = head[u]; ~i; i = ne[i])
        {
            int v = e[i];
            if(d[v] == -1 && c[i])
            {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if(v == T)  return true;
                q[++ tt] = v;
            }
        }
    }
    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 v = e[i];
        cur[u] = i;
        if(d[v] == d[u] + 1 && c[i])
        {
            int t = find(v, min(c[i], limit - flow));
            if(!t)  d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

int dinic(int k)
{
    memset(head, -1, sizeof head); idx = 0;
    for(int i = 0; i < m; i ++)
    {
        int u = edges[i].u, v = edges[i].v;
        add(u, v, 1, 1);
    }
    for(int i = 1; i <= n; i ++)
        if(p[i] != -1)
        {
            if(p[i] >> k & 1)   add(i, T, INF, 0);
            else    add(S, i, INF, 0);
        }

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

int main()
{
#ifdef LOCAL
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif

    scanf("%d%d", &n, &m);
    S = 0, T = n + 1;
    for(int i = 0; i < m; i ++)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        edges[i] = {u, v};
    }
    int k; scanf("%d", &k);
    memset(p, -1, sizeof p);
    while(k --)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        p[a] = b;
    }

    LL ans = 0;
    for(int i = 30; i >= 0; i --)   ans += 1LL*dinic(i) << i;
    printf("%lld\n", ans);

    return 0;
}
AcWing381. 有线电视网络

sol:
给定一张无向图,求最少去掉多少个点,可以使得该图不连通。
最小割是最少去掉多少边,可以使得源点与汇点不连通。

考虑如何转化:最小割问题首先要有源点与汇点,在本问题里,可以枚举源点与汇点,求使得该源点与汇点不连通需要去掉的最少的点。

将点转换成边,可以进行拆点,从入点向出点连接一条容量是 1 1 1的边,这样的话,如果割去这一条边,就代表这去掉该点,而对于原图中的边,将其容量设置为 + ∞ +\infty +,这样原图的边就不会被割去了。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 110, M = 5200;

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

void add(int u, int v, int w)
{
	e[idx] = v, c[idx] = w, ne[idx] = head[u], head[u] = idx ++;
	e[idx] = u, c[idx] = 0, ne[idx] = head[v], head[v] = idx ++;
}

bool bfs()
{
	int hh = 0, tt = -1;
	memset(d, -1, sizeof d);
	q[++ tt] = S, d[S] = 0, cur[S] = head[S];
	while(hh <= tt)
	{
		int u =q[hh ++];
		for(int i = head[u]; ~i; i = ne[i])
		{
			int v = e[i];
			if(d[v] == -1 && c[i])
			{
				d[v] = d[u] + 1;
				cur[v] = head[v];
				if(v == T)	return true;
				q[++ tt] = v;
			}
		}
	}
	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 v = e[i];
		if(d[v] == d[u] + 1 && c[i])
		{
			int t = find(v, min(c[i], limit - flow));
			if(!t)	d[v] = -1;
			c[i] -= t, c[i ^ 1] += t, flow += t;
		}
	}
	return flow;
}

int dinic()
{
	for(int i = 0; i < idx; i += 2)
	{
		c[i] += c[i ^ 1];
		c[i ^ 1] = 0;
	}
	int r = 0, flow;
	while(bfs())	while(flow = find(S, INF))	r += flow;
	return r;
}

int get(int x, int y)	{ return (x - 1) * m + y; }

int main()
{
#ifdef LOCAL
	freopen("in.in", "r", stdin);
	freopen("out.out", "w", stdout);
#endif

	while(cin >> n >> m)
	{
		memset(head, -1, sizeof head);
		idx = 0;

		for(int i = 1; i <= m; i ++)
		{
			int a, b;
			scanf(" (%d,%d)", &a, &b);
			add(a + n, b, INF);
			add(b + n, a, INF);
		}
		for(int i = 0; i < n; i ++)	add(i, n + i, 1);

		int ans = n;
		for(int i = 0; i < n; i ++)
			for(int j = i + 1; j < n; j ++)
			{
				S = n + i, T = j;
				ans = min(ans, dinic());
			}
		
		cout << ans << endl;
	}

	return 0;
}

2、最小割之最大权闭合图

AcWing961. 最大获利

sol:
最大权闭合子图板子题。
M M M个用户组,选择某个用户组则必须选择他需要的两个中转站,每个中转站会有一个花费 P P P,然后就会获得 C C C的收益。
求最大的收益。
建图:
最大权闭合子图的建图方法。
从源点 S S S向所有正点权连一条容量是其权值的边,在本题中,正点权就是用户组。
从所有负点权向汇点 T T T连接一条容量是其权值绝对值的边,在本题中,负点权就是中转站。
然后是题目中给的边,容量都设为 + ∞ +\infty +

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 55010, M = 320010;

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

void add(int u, int v, int w)
{
    e[idx] = v, c[idx] = w, ne[idx] = head[u], head[u] = idx ++;
    e[idx] = u, c[idx] = 0, ne[idx] = head[v], head[v] = idx ++;
}

bool bfs()
{
    memset(d, -1, sizeof d);
    int hh = 0, tt = -1;
    q[++ tt] = S, d[S] = 0, cur[S] = head[S];
    while(hh <= tt)
    {
        int u = q[hh ++];
        for(int i = head[u]; ~i; i = ne[i])
        {
            int v = e[i];
            if(d[v] == -1 && c[i])
            {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if(v == T)  return true;
                q[++ tt] = v;
            }
        }
    }
    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 v = e[i];
        cur[u] = i;
        if(d[v] == d[u] + 1 && c[i])
        {
            int t = find(v, min(c[i], limit - flow));
            if(!t)  d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

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

int main()
{
#ifdef LOCAL
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif
    memset(head, -1, sizeof head);
    scanf("%d%d", &n, &m);
    S = 0, T = n + m + 1;
    int tot = 0;
    for(int i = 1; i <= n; i ++)
    {
        int p; scanf("%d", &p);
        add(m + i, T, p);
    }
    for(int i = 1; i <= m; i ++)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(S, i, c); tot += c;
        add(i, m + a, INF);
        add(i, m + b, INF);
    }

    printf("%d\n", tot - dinic());

    return 0;
}

3、最小割之最大密度子图

AcWing2324.生活的艰辛

sol:
给定一个无向图,求出它的一个子图,使得 边 数 点 数 \frac{边数}{点数} 最大。
最大密度子图板子题。
建图:
偏移量一般取边数 m m m即可。
每次二分时,设当前二分值是 g g g
从源点 S S S向所有点连接一条容量是 m m m的边。
从所有点向汇点 T T T连接一条容量是 2 g − d e g r e e v 2g-degree_v 2gdegreev的边。
原图中的所有边的容量设为 1 1 1

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 110, M = 3000;

int n, m, S, T, dg[N];
struct Node { int a, b; } edges[M];
int idx, head[N], e[M], ne[M];
double c[M];
int q[N], d[N], cur[N];

void add(int u, int v, double w1, double w2)
{
    e[idx] = v, c[idx] = w1, ne[idx] = head[u], head[u] = idx ++;
    e[idx] = u, c[idx] = w2, ne[idx] = head[v], head[v] = idx ++;
}

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

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

double dinic(double mid)
{
    memset(head, -1, sizeof head); idx = 0;
    for(int i = 1; i <= n; i ++)
    {
        add(S, i, m, 0);
        add(i, T, m + 2 * mid - dg[i], 0);
    }
    for(int i = 0; i < m; i ++) add(edges[i].a, edges[i].b, 1, 1);

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

int main()
{
#ifdef LOCAL
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif

    scanf("%d%d", &n, &m);
    S = 0, T = n + 1;
    for(int i = 0; i < m; i ++)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        dg[a] ++, dg[b] ++;
        edges[i] = {a, b};
    }

    double l = 0, r = m;
    while(r - l > 1e-8)
    {
        double mid = (l + r) / 2;
        if(n * m - dinic(mid) > 0) l = mid;
        else    r = mid;
    }
    dinic(l);
    //printf("%.2lf\n", l);
    vector<int> ans;
    for(int i = 1; i <= n; i ++)
        if(d[i] != -1)  ans.push_back(i);
    
    if(ans.size() == 0) puts("1\n1");
    else
    {
        printf("%d\n", ans.size());
        for(auto x: ans)    printf("%d\n", x);
    }

    return 0;
}

3、最小割之最小点权覆盖集

POJ2125 有向图破坏

sol:
目标是去掉所有的边,对于一条边而言,若它要想被去掉,那么它两端的点至少有一个要被选中。而对于每一个点 v v v,有两个选择,所以需要将一个点拆成两个点 v v v n + v n+v n+v,选择点 v v v表示移除所有从点 v v v射出的边,选择 n + v n+v n+v表示移除所有射入点 v v v的边。
建图:
从源点 S S S向所有第一类点连一条容量是其代价的边。
从所有的第二类点向汇点 T T T连接容量是其代价的边。
原图中的所有边都在这两类点集之间相连,容量都是 + ∞ +\infty +
不难发现这是一个二分图。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 210, M = 11000;

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

void add(int u, int v, int w)
{
    e[idx] = v, c[idx] = w, ne[idx] = head[u], head[u] = idx ++;
    e[idx] = u, c[idx] = 0, ne[idx] = head[v], head[v] = idx ++;
}

bool bfs()
{
    memset(d, -1, sizeof d);
    int hh = 0, tt = -1;
    q[++ tt] = S, d[S] = 0, cur[S] = head[S];
    while(hh <= tt)
    {
        int u = q[hh ++];
        for(int i = head[u]; ~i; i = ne[i])
        {
            int v = e[i];
            if(d[v] == -1 && c[i])
            {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if(v == T)  return true;
                q[++ tt] = v;
            }
        }
    }
    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 v = e[i];
        cur[u] = i;
        if(d[v] == d[u] + 1 && c[i])
        {
            int t = find(v, min(c[i], limit - flow));
            if(!t)  d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

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

int main()
{
#ifdef LOCAL
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif

    scanf("%d%d", &n, &m);
    S = 0, T = 2 * n + 1;
    
    memset(head, -1, sizeof head);
    for(int i = 1; i <= n; i ++)
    {
        int w; scanf("%d", &w);
        add(n + i, T, w);
    }
    for(int i = 1; i <= n; i ++)
    {
        int w; scanf("%d", &w);
        add(S, i, w);
    }
    for(int i = 0; i < m; i ++)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, n + b, INF);
    }

    printf("%d\n", dinic());
    int cnt = 0;
    for(int i = 0; i < idx; i += 2)
    {
        int a = e[i ^ 1], b = e[i];
        if(d[a] != -1 && d[b] == -1)    cnt ++; //割边
    }
    printf("%d\n", cnt);
        for(int i = 0; i < idx; i += 2)
    {
        int a = e[i ^ 1], b = e[i];
        if(d[a] != -1 && d[b] == -1)
        {
            if(a == S)  printf("%d -\n", b);
            if(b == T)  printf("%d +\n", a - n);
        }
    }

    return 0;
}

4、最小割之最大点权独立集

AcWing 2326.王者之剑

sol:

  • 性质:只有第偶数秒才可以拿到宝石。
    反正法:假设拿到宝石的时候是奇数秒,那么意味着进入该宝石的格子之前是奇数秒,而在奇数秒时,与值相连的格子中的报数都会消失,所以等奇数秒来到这个格子的时候,里面的宝石已经消失了。
  • 推论:相邻格子中的宝石不能都被拿到。
    如果存在都被拿到的情况,一定是一个在奇数秒被拿到,一个在偶数秒被拿到,由上面的性质知道,不存在这样的情况。

联系图论,可以发现这是类似于独立集的性质。把格子看作点,相邻的关系看作边,就抽象出了图论模型,由于我们要求的是最大值,所以就是最大点权独立集的模型。

可以证明:给定一个点独立集,总是存在一个合法的行走方案,使得点独立集内的宝石都被拿到。(这里就不证明了)

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 10010, M = 80010;

int n, m, S, T, g[110][110];
int idx, head[N], e[M], c[M], ne[M];
int q[N], d[N], cur[N];

void add(int u, int v, int w)
{
	e[idx] = v, c[idx] = w, ne[idx] = head[u], head[u] = idx ++;
	e[idx] = u, c[idx] = 0, ne[idx] = head[v], head[v] = idx ++;
}

bool bfs()
{
	int hh = 0, tt = -1;
	memset(d, -1, sizeof d);
	q[++ tt] = S, d[S] = 0, cur[S] = head[S];
	while(hh <= tt)
	{
		int u =q[hh ++];
		for(int i = head[u]; ~i; i = ne[i])
		{
			int v = e[i];
			if(d[v] == -1 && c[i])
			{
				d[v] = d[u] + 1;
				cur[v] = head[v];
				if(v == T)	return true;
				q[++ tt] = v;
			}
		}
	}
	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 v = e[i];
		if(d[v] == d[u] + 1 && c[i])
		{
			int t = find(v, min(c[i], limit - flow));
			if(!t)	d[v] = -1;
			c[i] -= t, c[i ^ 1] += t, flow += t;
		}
	}
	return flow;
}

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

int get(int x, int y)	{ return (x - 1) * m + y; }

int main()
{
#ifdef LOCAL
	freopen("in.in", "r", stdin);
	freopen("out.out", "w", stdout);
#endif
    
    memset(head, -1, sizeof head);
	scanf("%d%d", &n, &m);
	S = 0, T = n * m + 1;
	for(int i = 1; i <= n; i ++)
		for(int j = 1; j <= m; j ++)	scanf("%d", &g[i][j]);
	
	int sum = 0;
	int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
	
	for(int i = 1; i <= n; i ++)
		for(int j = 1; j <= m; j ++)
		{
			sum += g[i][j];
			if((i + j) & 1)	
			{
			    add(S, get(i, j), g[i][j]);
			    for(int k = 0; k < 4; k ++)
			    {
			        int nx = i + dx[k], ny = j + dy[k];
			        if(nx < 1 || nx > n || ny < 1 || ny > m)    continue;
			        add(get(i, j), get(nx, ny), INF);
			    }
			}
			else	add(get(i, j), T, g[i][j]);
		}
	
	printf("%d\n", sum - dinic());

	return 0;
}

费用流

1、费用流之直接应用

AcWing 2192.运输问题

sol:
建图
从源点 S S S m m m个仓库连接一条容量是仓库容量花费是 0 0 0的边
n n n商店先汇点 T T T连接一条容量是商店需求量花费是 0 0 0的边
将原图的边容量设为 + ∞ +\infty +花费是运输费用的边

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 200, M = (N + 5000) * 2;

int n, m, S, T;
int idx, head[N], e[M], c[M], w[M], ne[M];
int d[N], pre[N], incf[N];
bool vis[N];

void add(int u, int v, int _c, int _w)
{
	e[idx] = v, c[idx] = _c, w[idx] = _w, ne[idx] = head[u], head[u] = idx ++;
	e[idx] = u, c[idx] = 0, w[idx] = -_w, ne[idx] = head[v], head[v] = idx ++;
}

bool spfa()
{
	memset(d, 0x3f, sizeof d);
	memset(incf, 0, sizeof incf);
	queue<int> q;
	q.push(S), vis[S] = true;
	d[S] = 0, incf[S] = INF;
	while(q.size())
	{
		int u = q.front();
		q.pop(), vis[u] = false;
		for(int i = head[u]; ~i; i = ne[i])
		{
			int v = e[i];
			if(c[i] && d[v] > d[u] + w[i])
			{
				d[v] = d[u] + w[i];
				incf[v] = min(incf[u], c[i]);
				pre[v] = i;
				if(!vis[v])	q.push(v), vis[v] = true;
			}
		}
	}
	return incf[T] > 0;
}

int EK()
{
	int cost = 0;
	while(spfa())
	{
		int t = incf[T]; //流量
		cost += t * d[T]; //流量*花费
		for(int i = T; i != S; i = e[pre[i] ^ 1])
			c[pre[i]] -= t, c[pre[i] ^ 1] += t;
	}
	return cost;
}

int main()
{
#ifdef LOCAL
	freopen("in.in", "r", stdin);
	freopen("out.out", "w", stdout);
#endif

	scanf("%d%d", &m, &n);
	S = 0, T = n + m + 1;
	memset(head, -1, sizeof head);
	for(int i = 1; i <= m; i ++)
	{
		int x; scanf("%d", &x);
		add(S, i, x, 0);
	}
	for(int i = 1; i <= n; i ++)
	{
		int x; scanf("%d", &x);
		add(i + m, T, x, 0);
	}

	for(int i = 1; i <= m; i ++)
		for(int j = 1; j <= n; j ++)
		{
			int x; scanf("%d", &x);
			add(i, m + j, INF, x);
		}
	
 	printf("%d\n", EK()); //最小费用
	for(int i = 0; i < idx; i += 2) //将边权取反,求最大费用
	{
		c[i] += c[i ^ 1], c[i ^ 1] = 0;
		w[i] = -w[i], w[i ^ 1] = -w[i ^ 1];
	}
	printf("%d\n", -EK()); //最大费用

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值