网络流学习笔记

前言

本人已做完网络流24题。

网络流难点在建模而不是代码(可以理解为图论版的dp),所以刚入门不必纠结于Dinic的写法(甚至硬背下来都没关系),重点在学建模上。

 

基础:

Dinic-最大流/最小割

typedef long long ll;
const int N = 1e4 + 5;
const int M = 1e6 + 5;
const ll inf = 1e18;
int n, m, s, t, tot, head[N], to[M], nxt[M];
ll val[M], d[N];
inline void link(int u, int v, ll w)
{
    to[tot] = v;
    nxt[tot] = head[u];
    val[tot] = w;
    head[u] = tot++;
}
inline void add(int u, int v, ll w)
{
    link(u, v, w);
    link(v, u, 0);
}
queue<int> q;
inline bool bfs()
{
    q = queue<int>();
    memset(d, 0, sizeof(d));
    d[s] = 1;
    q.emplace(s);
    while (!q.empty())
    {
        int tmp = q.front();
        q.pop();
        for (int i = head[tmp]; ~i; i = nxt[i])
            if (!d[to[i]] && val[i])
            {
                d[to[i]] = d[tmp] + 1;
                q.emplace(to[i]);
                if (to[i] == t)
                    return true;
            }
    }
    return false;
}
inline ll dfs(ll x, ll flow)
{
    if (x == t)
        return flow;
    ll rest = flow;
    for (int i = head[x]; ~i && rest; i = nxt[i])
        if (d[to[i]] == d[x] + 1 && val[i])
        {
            ll rs = dfs(to[i], min(rest, val[i]));
            if (!rs)
                d[to[i]] = 0;
            else
            {
                rest -= rs;
                val[i] -= rs;
                val[i ^ 1] += rs;
            }
        }
    if (rest == flow)
        d[x] = 0;
    return flow - rest;
}
inline ll Dinic()
{
    ll res = 0;
    while (bfs())
        res += dfs(s, inf);
    return res;
}

 

Dinic-费用流

把bfs换成spfa即可。

typedef long long ll;
const int N = 1e5 + 5;
const int M = 2e6 + 5;
const ll inf = 0x3f3f3f3f3f3f3f3f;
int n, m, s, t, tot, head[N], to[M], nxt[M];
ll val[M], c[M], d[N], flw, cst;
bool vis[N];
inline void link(int u, int v, ll w, ll cost)
{
    to[tot] = v;
    nxt[tot] = head[u];
    val[tot] = w;
    c[tot] = cost;
    head[u] = tot++;
}
inline void add(int u, int v, ll w, ll cost)
{
    link(u, v, w, cost);
    link(v, u, 0, -cost);
}
queue<int> q;
inline bool spfa()
{
    memset(d, 0x3f, sizeof(d));
    q.emplace(s);
    d[s] = 0;
    vis[s] = true;
    while (!q.empty())
    {
        int tmp = q.front();
        q.pop();
        vis[tmp] = false;
        for (int i = head[tmp]; ~i; i = nxt[i])
            if (val[i] && d[to[i]] > d[tmp] + c[i])
            {
                d[to[i]] = d[tmp] + c[i];
                if (!vis[to[i]])
                {
                    vis[to[i]] = true;
                    q.emplace(to[i]);
                }
            }
    }
    return d[t] != inf;
}
inline ll dfs(ll x, ll flow)
{
    if (x == t)
        return flow;
    vis[x] = true;
    ll rest = flow;
    for (int i = head[x]; ~i && rest; i = nxt[i])
        if (!vis[to[i]] && val[i] && d[to[i]] == d[x] + c[i])
        {
            ll rs = dfs(to[i], min(rest, val[i]));
            if (!rs)
                d[to[i]] = inf;
            else
            {
                rest -= rs;
                val[i] -= rs;
                val[i ^ 1] += rs;
                cst += rs * c[i];
            }
        }
    if (rest == flow)
        d[x] = inf;
    vis[x] = false;
    return flow - rest;
}
inline void MCMF()
{
    while (spfa())
        flw += dfs(s, inf);
    cout << cst;
}

 

正经人谁用HLPP。Dinic太强大了。

相同分组增加贡献

例题P1361 P1646。

先Dinic计算最小割,总和-最小割即为答案。

连边:

s→i[1,n],边权为i在第一组的贡献。

i[1,n]→t,边权为i在第二组的贡献。

如果第j个输入表示点集$S$都在第一组时会额外增加k的回报,连边:

s→j,边权为k;

j→$S$,边权为inf;

否则(如果在第二组):

j→t,边权为k;

$S$→j,边权为inf;

答案即为总边权(除去inf)减最小割。

P1361样例:

共用消耗,完成增加贡献

例题P2762 P4174。

分层连边:

对于每个任务i,设需要的工具集为$S$,做完可以获得k的回报:

s→i,边权为k;

i→$S$,对于$S$的每个点,边权为inf。

对于每个工具i:

i→t,边权为i的花费。

答案即为每个任务回报和减最小割。

P2762样例:

棋盘选点问题

例题P2774 P3355。

将点分为两组(直接用(x+y)&1分类即可)。

对于第一组点集$S$,连边$S$→t,边权为所求结果。

对于另外一组$S$,连边s→$S$,边权为所求结果;对于每个点u,连边u→$V$,$V$为和u相邻的点集,边权inf。

答案依旧为总和减最小割。

P2774样例:

分配问题 Basic

例题P4014 P4015。(P2763 P3254等其实也差不多)

连边:

s→i[1,n],流量1,费用0,表示每个人。

i[n+1,n*2]→t,流量1,费用0,表示每个任务。

i[1,n]->j[n+1,n*2],流量1,费用$c_{i,j}$。

其中P4014第二问还要求跑最大费用最大流,费用建负数即可。

边太多了,图就不放了。

分配问题 Plus

例题P2053 P2050。(P2050稍难,要求动态加点,相当于Ultimate版本

其实就是模型4的升级版,将模型4里面每个人拆成m个人,即第i个人拆成第j时段的第i人即可。

重新建图/残量网络

例题P4013 P4014 P4015。

如果一题有很多问,而一问又可以转化为上一问的图+某些新的边(比如P4013第二问就是第一问再加上任意两点连边inf),那么就会涉及到这个。

如果是最大流(有这种题,但是我忘了),跑完Dinic后把答案存起来,在残量网络上建新的边,再跑一遍,两次答案之和即为第二次的流量。

但如果是费用流,就要重新建图了。P4013 P4014都是重新建图的例子。跑完后把链式前向星的head数组、链式前向星的tot、MaxFlow值、MinCost/MaxCost值全部初始化,再建图即可。

上下界网络流

上下界最大流

懒得画图了。放代码吧。

int s1 = n + m + 1, t1 = n + m + 2, s2 = n + m + 3, t2 = n + m + 4;
s = s2, t = t2;
for (int i = 1; i <= m; i++)
{
    ll x;
    cin >> x;
    in[t1] += x;
    out[n + i] += x;
    add(n + i, t1, inf - x);
}
for (int i = 1; i <= n; i++)
{
    ll x, y, T, l, r;
    cin >> x >> y;
    add(s1, i, y);
    for (int j = 1; j <= x; j++)
    {
        cin >> T >> l >> r;
        T++;
        in[n + T] += l;
        out[i] += l;
        add(i, n + T, r - l);
    }
}
ll maxFlow = 0;
for (int i = 1; i <= n + m + 2; i++)
    if (in[i] > out[i])
        add(s, i, in[i] - out[i]), maxFlow += in[i] - out[i];
    else
        add(i, t, out[i] - in[i]);
add(t1, s1, inf);
if (Dinic() != maxFlow)
{
    cout << "-1\n\n";
    continue;
}
int ans = val[tot - 1];
val[tot - 1] = val[tot - 2] = 0;
s = s1, t = t1;
cout << ans + Dinic() << "\n\n";

 

题目

- 网络流24题
- 志愿者招募
- 石头剪刀布
- Nanami's Power Plant

写在最后

网络流24题里确实还有一些经典建模本文没有涉及到。因为本人很懒。

posted @ 2022-11-22 17:44  creation_hy  阅读(128)  评论(1编辑  收藏  举报