模拟费用流

模拟费用流

费用流常用结论:

  • 费用流的凸性:增广路的长度会不断增长,费用是关于流量的凸函数。
  • 需要考虑的负环(或增广路)不会多次经过同一个点(否则可以拆出一个环,正环不优,负环可以下次考虑)。
  • 若每次消去权和最小的负环,则不存在一对被消去的负环经过一条边的方向相反(否则可以合并)。

常见模型:

  • 最大流模型:无费用,也可以转化为最小割分析。
  • 费用流增广模型:每次增广最短路,当单次费用为正时停止(任意流),或每次强制增广最短路(最大流)。
  • 增量费用任意流模型:不断加点或边,只考虑所有新的负增广路与负环的贡献。
  • 任意流消圈模型:求出任意一组最大流,然后消负环。

联系:反悔贪心中的反悔操作实际就是模拟费用流中的退流操作。

最大流模型

CF1368H2 Breadboard Capacity (hard version)

给出一张 \(n \times m\) 的电路板,边界有 \(2(n + m)\) 个接口,颜色为红色或蓝色,内部有 \(n \times m\) 个节点。

定义一条合法的电线连接当前仅当其满足:

  • 两端点为异色接口。
  • 除了在节点处可以拐弯,任何部分电线必须时水平或竖直的。
  • 除了在节点处可以相交,其他部分不能与别的电线相交。

求能放置的最大电线数(H1)。

\(q\) 次修改,每次反转一段边界接口的颜色,并求能放置的最大电线数(H2)。

\(n, m, q \leq 10^5\)

先不考虑修改,考虑建立网络流模型:

  • 源点连红接口,流量为 \(1\)
  • 蓝接口连汇点,流量为 \(1\)
  • 内部节点向四周连容量为 \(1\) 的无向边。

最大流即为答案。

考虑观察性质,最大流是不好观察的,考虑最小割,则确定了 \(n \times m\) 个节点的颜色后割即为异色点之间的边数。可以观察到:

  • 存在一组最优解,使得每条割边都不成环。
    • 证明:翻转环中点的颜色显然不劣。
  • 存在一组最优解,使得割边形成的路径不连接同一边界或相邻边界。
    • 证明:翻转内部点的颜色显然不劣。
  • 存在一组最优解,使得任何一条割路径不转弯。
    • 证明:翻转弯里面的点的颜色显然不劣。

因此存在一组最优解,使得割边形成的路径全为水平或全为竖直。

\(f_{i, 0/1}\) 表示考虑了前 \(i\) 列,第 \(i\) 列染红/蓝的最小割,转移不难做到线性,需要横竖都做一遍,即可通过 H1。

注意到 \(f\) 的转移可以写成矩阵的形式,线段树维护每个点是否翻转的矩阵,时间复杂度 \(O(n \log n + m \log m)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e5 + 7;

char L[N], R[N], U[N], D[N];

int n, m, q;

struct SGT {
    int s[N << 2], len[N << 2], tag[N << 2];

    inline int ls(int x) {
        return x << 1;
    }

    inline int rs(int x) {
        return x << 1 | 1;
    }

    inline void spread(int x) {
        s[x] = len[x] - s[x], tag[x] ^= 1;
    }

    inline void pushdown(int x) {
        if (tag[x])
            spread(ls(x)), spread(rs(x)), tag[x] = 0;
    }

    void build(int x, int l, int r, char *str) {
        len[x] = r - l + 1, tag[x] = 0;

        if (l == r) {
            s[x] = (str[l] == 'B');
            return;
        }

        int mid = (l + r) >> 1;
        build(ls(x), l, mid, str), build(rs(x), mid + 1, r, str);
        s[x] = s[ls(x)] + s[rs(x)];
    }

    void update(int x, int nl, int nr, int l, int r) {
        if (l <= nl && nr <= r) {
            spread(x);
            return;
        }

        pushdown(x);
        int mid = (nl + nr) >> 1;

        if (l <= mid)
            update(ls(x), nl, mid, l, r);

        if (r > mid)
            update(rs(x), mid + 1, nr, l, r);

        s[x] = s[ls(x)] + s[rs(x)];
    }
};

struct Matrix {
    int a[2][2];

    inline Matrix() {
        memset(a, inf, sizeof(a));
    }

    inline Matrix operator * (const Matrix &rhs) const {
        Matrix res;

        for (int i = 0; i < 2; ++i)
            for (int j = 0; j < 2; ++j)
                for (int k = 0; k < 2; ++k)
                    res.a[i][k] = min(res.a[i][k], a[i][j] + rhs.a[j][k]);

        return res;
    }
};

struct SMT {
    Matrix s[N << 2][4];
    int tag[N << 2];

    inline int ls(int x) {
        return x << 1;
    }

    inline int rs(int x) {
        return x << 1 | 1;
    }

    inline void pushup(int x) {
        for (int i = 0; i < 4; ++i)
            s[x][i] = s[ls(x)][i] * s[rs(x)][i];
    }

    inline void spread(int x, int k) {
        vector<Matrix> vec(s[x], s[x] + 4);

        for (int i = 0; i < 4; ++i)
            s[x][i ^ k] = vec[i];

        tag[x] ^= k;
    }

    inline void pushdown(int x) {
        if (tag[x])
            spread(ls(x), tag[x]), spread(rs(x), tag[x]), tag[x] = 0;
    }

    void build(int x, int l, int r, char *a, char *b, int n) {
        tag[x] = 0;

        if (l == r) {
            for (int i = 0; i < 4; ++i) {
                int k = ((a[l] == 'B') ^ (i >> 1 & 1)) + ((b[l] == 'B') ^ (i & 1));
                s[x][i].a[0][0] = k, s[x][i].a[0][1] = n + 2 - k;
                s[x][i].a[1][0] = n + k, s[x][i].a[1][1] = 2 - k;
            }

            return;
        }

        int mid = (l + r) >> 1;
        build(ls(x), l, mid, a, b, n), build(rs(x), mid + 1, r, a, b, n);
        pushup(x);
    }

    void update(int x, int nl, int nr, int l, int r, int k) {
        if (l <= nl && nr <= r) {
            spread(x, k);
            return;
        }

        pushdown(x);
        int mid = (nl + nr) >> 1;

        if (l <= mid)
            update(ls(x), nl, mid, l, r, k);

        if (r > mid)
            update(rs(x), mid + 1, nr, l, r, k);

        pushup(x);
    }
};

struct Solver {
    SGT L, R;
    SMT smt;

    int n, m;

    inline void prework(int _n, int _m, char *_L, char *_R, char *_U, char *_D) {
        n = _n, m = _m, L.build(1, 1, n, _L), R.build(1, 1, n, _R), smt.build(1, 1, m, _U, _D, n);
    }

    inline void flipL(int l, int r) {
        L.update(1, 1, n, l, r);
    }

    inline void flipR(int l, int r) {
        R.update(1, 1, n, l, r);
    }

    inline void flipU(int l, int r) {
        smt.update(1, 1, m, l, r, 2);
    }

    inline void flipD(int l, int r) {
        smt.update(1, 1, m, l, r, 1);
    }

    inline int query() {
        Matrix f;
        f.a[0][0] = L.s[1], f.a[0][1] = n - L.s[1];
        f = f * smt.s[1][0];
        return min(f.a[0][0] + R.s[1], f.a[0][1] + n - R.s[1]);
    }
} solver1, solver2;

signed main() {
    scanf("%d%d%d%s%s%s%s", &n, &m, &q, L + 1, R + 1, U + 1, D + 1);
    solver1.prework(n, m, L, R, U, D), solver2.prework(m, n, U, D, L, R);
    printf("%d\n", min(solver1.query(), solver2.query()));

    while (q--) {
        char op[2];
        int l, r;
        scanf("%s%d%d", op, &l, &r);

        if (op[0] == 'L')
            solver1.flipL(l, r), solver2.flipU(l, r);
        else if (op[0] == 'R')
            solver1.flipR(l, r), solver2.flipD(l, r);
        else if (op[0] == 'U')
            solver1.flipU(l, r), solver2.flipL(l, r);
        else
            solver1.flipD(l, r), solver2.flipR(l, r);

        printf("%d\n", min(solver1.query(), solver2.query()));
    }

    return 0;
}

CF1895G Two Characters, Two Colors

给定 01 串 \(s_{1 \sim n}\) ,每个位置保留则获得 \(r_i\) 的收益,删去则获得 \(b_i\) 的收益,最大化收益和减去剩余元素的逆序对数。

\(n \leq 4 \times 10^5\)

对于一对逆序对,将其视为同时保留会产生 \(-1\) 的贡献。由于是 01 串,于是一个很好的性质是负贡献一定在一对 \(0, 1\) 之间产生。

先令总收益为 \(\sum r_i + b_i\) ,则需要最小化负收益,考虑建立最小割模型:

  • 对每个 \(s_i = 0\) 的位置,连边 \((S, i, b_i), (i, T, r_i)\)
  • 对每个 \(s_i = 1\) 的位置,连边 \((S, i, r_i), (i, T, b_i)\)
  • 对每个 \(s_i = 1\) 的位置,向后面 \(s_j = 0\) 的位置连 \(i \to j\) 流量为 \(1\) 的边。

最小割即为负收益。

接下来考虑模拟最大流,显然有流量下界 \(\sum \min(r_i, b_i)\) ,然后考虑剩余的流量:

  • 对于一个 \(1\) ,若 \(r_i > b_i\) ,则还有 \(r_i - b_i\) 的流量可以流到后面的 \(0\)
  • 对于一个 \(0\) ,若 \(r_i > b_i\) ,则还能接受来自前面 \(1\)\(r_i - b_i\) 的流量。

于是可以倒序枚举位置,每次遇到一个 \(r_i > b_i\)\(0\) 就把 \(r_i - b_i\) 加入集合,遇到一个 \(r_i > b_i\)\(1\) 就把集合前 \(r_i - b_i\) 大元素减去 \(1\) ,然后把减到 \(0\) 的元素删掉即可。

使用 fhq-Treap 维护即可做到 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef unsigned int uint;
typedef long long ll;
using namespace std;
const int N = 4e5 + 7;

ll r[N], b[N];
char str[N];

int n;

namespace fhqTreap {
ll val[N];
uint dat[N];
int lc[N], rc[N], siz[N], tag[N];

mt19937 myrand(time(0));
int tot, root;

inline int newnode(ll k) {
    dat[++tot] = myrand(), val[tot] = k, lc[tot] = rc[tot] = tag[tot] = 0, siz[tot] = 1;
    return tot;
}

inline void spread(int x, int k) {
    val[x] += k, tag[x] += k;
}

inline void pushdown(int x) {
    if (tag[x]) {
        if (lc[x])
            spread(lc[x], tag[x]);

        if (rc[x])
            spread(rc[x], tag[x]);

        tag[x] = 0;
    }
}

inline void pushup(int x) {
    siz[x] = siz[lc[x]] + siz[rc[x]] + 1;
}

void split_siz(int x, int k, int &a, int &b) {
    if (!x) {
        a = b = 0;
        return;
    }

    pushdown(x);

    if (siz[lc[x]] + 1 <= k)
        a = x, split_siz(rc[x], k - siz[lc[x]] - 1, rc[a], b);
    else
        b = x, split_siz(lc[x], k, a, lc[b]);

    pushup(x);
}

void split_val(int x, ll k, int &a, int &b) {
    if (!x) {
        a = b = 0;
        return;
    }

    pushdown(x);

    if (val[x] <= k)
        a = x, split_val(rc[x], k, rc[a], b);
    else
        b = x, split_val(lc[x], k, a, lc[b]);

    pushup(x);
}

int merge(int a, int b) {
    if (!a || !b)
        return a | b;

    pushdown(a), pushdown(b);

    if (dat[a] > dat[b])
        return rc[a] = merge(rc[a], b), pushup(a), a;
    else
        return lc[b] = merge(a, lc[b]), pushup(b), b;
}

inline void insert(ll k) {
    int a, b;
    split_val(root, k, a, b);
    root = merge(merge(a, newnode(k)), b);
}

inline ll querymin(int x) {
    while (lc[x])
        pushdown(x), x = lc[x];

    return val[x];
}

inline ll querymax(int x) {
    while (rc[x])
        pushdown(x), x = rc[x];

    return val[x];
}

inline void update(int k) {
    int a, b;
    split_siz(root, siz[root] - k, a, b);

    if (querymax(a) < querymin(b))
        spread(b, -1), root = merge(a, b);
    else {
        ll v = querymax(a);
        int c, d;
        split_val(a, v - 1, a, c), split_val(b, v, b, d);
        spread(b, -1), spread(d, -1);
        root = merge(merge(a, b), merge(c, d));
    }

    split_val(root, 0, a, root);
}
} // namespace fhqTreap

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%s", &n, str + 1);

        for (int i = 1; i <= n; ++i)
            scanf("%lld", r + i);

        for (int i = 1; i <= n; ++i)
            scanf("%lld", b + i);

        ll ans = 0;

        for (int i = 1; i <= n; ++i)
            ans += max(r[i], b[i]); // r + b - min(r, b) = max(r, b)

        fhqTreap::root = fhqTreap::tot = 0;

        for (int i = n; i; --i) {
            if (r[i] <= b[i])
                continue;

            if (str[i] == '0')
                fhqTreap::insert(r[i] - b[i]);
            else {
                int k = min((ll)fhqTreap::siz[fhqTreap::root], r[i] - b[i]);
                ans -= k, fhqTreap::update(k);
            }
        }

        printf("%lld\n", ans);
    }

    return 0;
}

CF1408H Rainbow Triples

给定序列 \(p_{1 \sim n}\) ,选出若干三元组 \((a_i, b_i, c_i)\) 满足:

  • \(1 \leq a_i < b_i < c_i \leq n\)
  • \(a_i = c_i = 0\)\(b_i \neq 0\)
  • \(p_{b_i}\) 互不相同。
  • 所有 \(a_i, b_i, c_i\) 互不相同。

最大化选出的三元组数量。

\(n \leq 5 \times 10^5\)

\(m\)\(0\) 的数量,显然答案 \(\leq \lfloor \frac{m}{2} \rfloor\) 。考虑将所有 \(0\) 均分为左右两段,则不难发现存在一组最优解满足 \(a_i\) 为左半边的 \(0\)\(c_i\) 为右半边的 \(0\) ,这可以通过调整法证明。

进一步分析可以得到,若 \(b_i\) 在左半边,则右半边一定可以匹配,因此只要考虑 \(a_i\) 的选取即可,同理 \(b_i\) 在右半边时只要考虑 \(c_i\) 的选取即可。因此对于每个 \(p_i \neq 0\) 的位置只要在左右各保留一个即可,贪心保留最靠近中心的。

考虑网络流建模:

  • \(S\) 向每个非 \(0\) 值对应点连边,边权为 \(1\)
  • 对位置建点 \(1 \sim n\) ,左半边连成递减的链,右半边连成递增的链(即中心向两边的方向),边权均为 \(+ \infty\)
  • 每个非 \(0\) 值对应点向对应的左右两个位置连边(若存在),边权为 \(1\)
  • 将所有 \(0\) 位置向 \(T\) 连边,边权为 \(1\)

最大流即为答案,数据范围启发我们考虑模拟费用流。

尝试最小割模型,若某颜色的边不割,则必须割掉一段前缀 \(0\) 与一段后缀 \(0\) ,因此最小割一定形如:一段前缀 \(0\) 、一段后缀 \(0\) 、一部分颜色。

考虑枚举后缀 \(0\) 的割掉的位置,用线段树维护一段前缀 \(0\) 和一部分颜色的割边数量即可,时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7;

int a[N], pre[N], suf[N], L[N], R[N];

int n;

namespace SMT {
int mn[N << 2], tag[N << 2];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

inline void spread(int x, int k) {
    mn[x] += k, tag[x] += k;
}

inline void pushdown(int x) {
    if (tag[x])
        spread(ls(x), tag[x]), spread(rs(x), tag[x]), tag[x] = 0;
}

void build(int x, int l, int r) {
    tag[x] = 0;

    if (l == r) {
        mn[x] = l;
        return;
    }

    int mid = (l + r) >> 1;
    build(ls(x), l, mid), build(rs(x), mid + 1, r);
    mn[x] = min(mn[ls(x)], mn[rs(x)]);
}

void update(int x, int nl, int nr, int l, int r, int k) {
    if (l <= nl && nr <= r) {
        spread(x, k);
        return;
    }

    pushdown(x);
    int mid = (nl + nr) >> 1;

    if (l <= mid)
        update(ls(x), nl, mid, l, r, k);

    if (r > mid)
        update(rs(x), mid + 1, nr, l, r, k);

    mn[x] = min(mn[ls(x)], mn[rs(x)]);
}
} // namespace SMT

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d", &n);
        set<int> st;

        for (int i = 1; i <= n; ++i)
            scanf("%d", a + i), st.emplace(a[i]);

        st.erase(0), pre[0] = 0;

        for (int i = 1; i <= n; ++i)
            pre[i] = pre[i - 1] + !a[i];

        suf[n + 1] = 0;

        for (int i = n; i; --i)
            suf[i] = suf[i + 1] + !a[i];

        int m = pre[n] / 2, mid = upper_bound(pre + 1, pre + n + 1, m) - pre - 1;
        memset(L + 1, 0, sizeof(int) * n), memset(R + 1, 0, sizeof(int) * n);

        for (int i = 1; i <= mid; ++i)
            L[a[i]] = i;

        for (int i = n; i > mid; --i)
            R[a[i]] = i;

        int ans = min((int)st.size(), m);
        SMT::build(1, 0, m), SMT::spread(1, st.size());

        for (int it : st)
            if (!R[it])
                SMT::update(1, 0, m, pre[L[it]], m, -1), ans = min(ans, SMT::mn[1]);

        for (int i = n; i > mid; --i)
            if (a[i] && R[a[i]] == i)
                SMT::update(1, 0, m, pre[L[a[i]]], m, -1), ans = min(ans, SMT::mn[1] + suf[i]);

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

    return 0;
}

费用流增广模型

P1484 种树

\(n\) 个数中选出至多 \(k\) 个两两不相邻的数的最大和。

\(n \leq 3 \times 10^5\)

\(n - 1\) 个间隔建点,则选一个位置可以转化为相邻间隔的匹配,建立费用流模型:

  • \(S\) 向奇数间隔连流量为 \(1\) ,费用为 \(0\) 的边。
  • 偶数间隔向 \(T\) 连流量为 \(1\) ,费用为 \(0\) 的边。
  • 奇数间隔向相邻偶数间隔连流量为 \(1\) ,费用为两个间隔之间的数的边。

流量限制为 \(k\) ,最大费用任意流即为答案。

由于费用流的凸性,因此可以 wqs 二分配合 DP 解决,这并不重要。

考虑一次增广,会流过若干奇偶间隔之间的连续边,其表示将这一段的状态都取反,即将一段 0101...010 变为 1010...101

用堆维护这样选取状态交替改变的连续段,设当前选出的最大的点为 \(x\) ,它左右两边的点分别是 \(y, z\) ,就删掉这三个点并新建一个点权为 \(a_y + a_z - a_x\) 的,这样下次选出这个点时就实现了退流操作,用链表维护相邻点即可。

时间复杂度 \(O(n \log n)\)

类似的题目:

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 5e5 + 7;

struct List {
    int pre, nxt;
    ll val;
} a[N];

priority_queue<pair<ll, int> > q;

bool vis[N];

int n, k;

signed main() {
    scanf("%d%d", &n, &k);
    
    for (int i = 1; i <= n; ++i) {
        scanf("%lld", &a[i].val), q.emplace(a[i].val, i);
        a[i].pre = i - 1, a[i].nxt = i + 1;
    }
    
    ll ans = 0;
    
    while (k--) {
        while (vis[q.top().second])
            q.pop();
        
        ll res = q.top().first;
        int x = q.top().second, y = a[x].pre, z = a[x].nxt;
        q.pop();
        
        if (res <= 0)
            break;
        
        ans += res, vis[y] = vis[z] = true;
        a[x].val = a[y].val + a[z].val - a[x].val;
        a[a[x].pre = a[y].pre].nxt = a[a[x].nxt = a[z].nxt].pre = x;
        q.emplace(a[x].val, x);
    }
    
    printf("%lld", ans);
    return 0;
}

P5470 [NOI2019] 序列

给定 \(a_{1 \sim n}, b_{1 \sim n}\) ,需要分别对两个序列各指定恰好 \(k\) 个下标,要求至少有 \(l\) 个下标在两个序列中都被指定,最大化这 \(2k\) 个下标在序列中对应的元素的总和。

多测,\(\sum n \leq 10^6\)

考虑费用流建模。至少 \(l\) 个下标相等是不好用流量表示限制的,考虑限制至多 \(k - l\) 个下标不相等,这可以通过建立虚点 \(C, D\) 实现。

  • \(S\) 向每个 \(a_i\) 代表的点连流量为 \(1\) 、费用为 \(a_i\) 的边。
  • 每个 \(b_i\) 代表的点向 \(T\) 连流量为 \(1\) 、费用为 \(b_i\) 的边。
  • 每个 \(a_i\) 代表向每个 \(b_i\) 代表的点连流量为 \(1\) 、费用为 \(0\) 的边。
  • 每个 \(a_i\) 代表向 \(C\) 连流量为 \(1\) 、费用为 \(0\) 的边。
  • \(C\)\(D\) 连流量为 \(k - l\) 、费用为 \(0\) 的边。
  • \(D\) 向每个 \(b_i\) 代表的点连流量为 \(1\) 、费用为 \(0\) 的边。

源点流量限制为 \(k\) ,最大费用流即为答案。

考虑对 \(k\) 个流量依次找增广路:

  • \(S \to A_i \to B_i \to T\) :匹配 \((a_i, b_i)\)
  • \(S \to A_i \to C \to D \to B_j \to T\) :匹配 \((a_ i, b_j)\)\(C \to D\) 流量减少 \(1\)
  • \(S \to A_i \to B_i \to D \to C \to A_j \to B_j \to T\) :将 \((a_j, b_i)\) 调整为 \((a_i, b_i), (a_j, b_j)\)\(C \to D\) 流量增加 \(1\)
  • \(S \to A_i \to B_i \to D \to B_k \to T\) :将 \((a_j, b_i)\) 调整为 \((a_i, b_i), (a_j, b_k)\)
  • \(S \to A_i \to C \to A_k \to B_k \to T\) :将 \((a_k, b_j)\) 调整为 \((a_i, b_j), (a_k, b_k)\)

权值均为最大值时优先选 \(C \to D\) 流量增加的方案,其次选 \(C \to D\) 流量不变的方案,最后选 \(C \to D\) 流量减少的方案。这是因为当出现 \((a_i, b_j), (a_j, b_i)\) 时显然调整为 \((a_i, b_i), (a_j, b_j)\) 是更优的。

用堆维护,时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e5 + 7;

int tag[N];

struct Heap {
    priority_queue<pair<int, int> > q;

    int op1, op2;

    inline Heap(int _op1 = -1, int _op2 = -1) : op1(_op1), op2(_op2) {}

    inline void clear() {
        while (!q.empty())
            q.pop();
    }

    inline void maintain() {
        while (!q.empty() && tag[q.top().second] != op1 && tag[q.top().second] != op2)
            q.pop();
    }

    inline bool empty() {
        return maintain(), q.empty();
    }

    inline pair<int, int> top() {
        return maintain(), q.top();
    }

    inline void emplace(int k, int x) {
        q.emplace(k, x);
    }
} qa(0, 2), qb(0, 1), q(0), qab(2), qba(1);

int a[N], b[N];

int n, k, m;

inline void update(int x) {
    if (!tag[x])
        qa.emplace(a[x], x), qb.emplace(b[x], x), q.emplace(a[x] + b[x], x);
    else if (tag[x] == 1)
        qb.emplace(b[x], x), qba.emplace(b[x], x);
    else if (tag[x] == 2)
        qa.emplace(a[x], x), qab.emplace(a[x], x);
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d%d", &n, &k, &m);

        for (int i = 1; i <= n; ++i)
            scanf("%d", a + i);

        for (int i = 1; i <= n; ++i)
            scanf("%d", b + i);

        qa.clear(), qb.clear(), q.clear(), qab.clear(), qba.clear();

        for (int i = 1; i <= n; ++i)
            tag[i] = 0, update(i);

        m = k - m;
        ll ans = 0;

        while (k--) {
            int res1 = -inf, res2 = -inf, res3 = -inf, res4 = -inf, res5 = -inf;

            if (!q.empty())
                res1 = q.top().first;

            if (m && !qa.empty() && !qb.empty())
                res2 = qa.top().first + qb.top().first;

            if (!qab.empty() && !qba.empty())
                res3 = qab.top().first + qba.top().first;

            if (!qab.empty() && !qb.empty())
                res4 = qab.top().first + qb.top().first;

            if (!qa.empty() && !qba.empty())
                res5 = qa.top().first + qba.top().first;

            int res = max({res1, res2, res3, res4, res5});
            ans += res;

            if (res3 == res) {
                int x = qab.top().second, y = qba.top().second;
                ++m, tag[x] |= 1, update(x), tag[y] |= 2, update(y);
            } else if (res1 == res) {
                int x = q.top().second;
                tag[x] = 3, update(x);
            } else if (res4 == res) {
                int x = qab.top().second, y = qb.top().second;
                tag[x] |= 1, update(x), tag[y] |= 2, update(y);
            } else if (res5 == res) {
                int x = qa.top().second, y = qba.top().second;
                tag[x] |= 1, update(x), tag[y] |= 2, update(y);
            } else {
                int x = qa.top().second, y = qb.top().second;
                --m, tag[x] |= 1, update(x), tag[y] |= 2, update(y);
            }
        }

        printf("%lld\n", ans);
    }

    return 0;
}

Pretty Boxes

\(n\) 个箱子,每个箱子有大小 \(s_i\) 和价值 \(p_i\)

定义一对箱子 \((a, b)\) 的价值为:

  • \(s_a > s_b\) :获得 \(p_a - p_b\) 的价值。
  • \(s_a <s_b\) :获得 \(p_b - p_a\) 的价值。
  • \(s_a = s_b\) :获得 \(|p_a - p_b|\) 的价值。

对于 \(k = 1, 2, \cdots, \lfloor \frac{n}{2} \rfloor\) ,求至多选 \(k\) 对互不相同(需要选 \(2k\) 个互不相同的箱子)的箱子的最大价值和。

\(n \leq 2 \times 10^5\)

考虑将所有箱子按 \(s\) 为第一关键字、\(p\) 为第二关键字降序排序,则问题转化为选出 \(k\) 对括号匹配,其中左括号处权值为正,右括号处权值为负。

由于是至多选 \(k\) 对,因此考虑钦定自己可以和自己匹配,这样没有贡献,问题转化为恰好选 \(k\) 对。

考虑建立费用流模型,除了 \(1 \sim n\)\(S, T\) 外额外建 \(n\) 个后缀点 \(n + 1 \sim n + n\)

  • \(S\)\(i = 1, 2, \cdots, n\) 连边,流量为 \(1\) ,费用为 \(p_i\) :表示该点可以作为一个左括号。
  • \(i = 1, 2, \cdots, n\)\(T\) 连边,流量为 \(1\) ,费用为 \(-p_i\) :表示该点可以作为一个右括号。
  • \(i\)\(n + i\) 连边,流量为 \(1\) ,费用为 \(0\) :表示该点可以向后匹配。
  • \(n + i\)\(n + i + 1\) 连边,流量为 \(+ \infty\) ,费用为 \(0\) :表示该点可以继承前面的左括号。
  • \(n + i\)\(i\) 连边:表示该点可以接受前面的左括号。

每次分配 \(1\) 的流量跑,最大费用最大流即为答案。考虑优化,观察到增广路只有两种形式:

  • 选择两个未匹配点 \(a < b\) 匹配,价值为 \(p_a - p_b\)
  • 选择两个未匹配点 \(a < b\) 匹配,价值为 \(p_b - p_a\) ,要求 \(b\) 能通过退流边到达 \(a\)

第一个情况不难用线段树处理,考虑表示第二个情况的限制。

对于 \(n - 1\) 条后缀点的边,记 \(c_i\) 表示该边可以被退流几次,则选出一个增广路之后相当于区间 \(\pm 1\) 。限制条件即为 \(\min_{i = a}^{b - 1} c_i > 0\) ,即按 \(c_i > 0\) 的部分分段后每段内部可以选。

但是直接维护 \(c_i > 0\) 的段不好做区间修改,由于 \(c_i \geq 0\) ,考虑引入 \(c_n= 0\) ,并将分段方式改为 \(c_i > \min_{j = L}^R c_j\) ,这样就可以比较方便的合并区间信息了。

时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e5 + 7;

pair<int, int> a[N];

int n;

namespace SMT {
struct Node {
	tuple<int, int, int> ans1, ans2, ans3;
	pair<int, int> mn, mx, mnl, mnr, mxl, mxr;

	int mnc;

	// ans1 : 第一种匹配
	// ans2 : 第二种匹配,并考虑区间最小值的限制
	// ans3 : 第二种匹配,不考虑区间最小值的限制
	// mn, mx : 区间 p 的极值
	// mnl, mnr, mxl, mxr : 区间 p 的极值,并考虑区间最小值的限制
	// mnc : 区间最小退流边的可退流量

	inline friend Node operator + (const Node &a, const Node &b) {
		Node c;
		c.mnc = min(a.mnc, b.mnc), c.mn = min(a.mn, b.mn), c.mx = max(a.mx, b.mx);
		c.ans1 = max(max(a.ans1, b.ans1), make_tuple(a.mx.first - b.mn.first, a.mx.second, b.mn.second));
		c.ans3 = max(max(a.ans3, b.ans3), make_tuple(b.mx.first - a.mn.first, a.mn.second, b.mx.second));

		if (a.mnc == b.mnc) {
			c.mnl = a.mnl, c.mnr = b.mnr, c.mxl = a.mxl, c.mxr = b.mxr;
			c.ans2 = max(max(a.ans2, b.ans2), make_tuple(b.mxl.first - a.mnr.first, a.mnr.second, b.mxl.second));
		} else if (a.mnc < b.mnc) {
			c.mnl = a.mnl, c.mnr = min(a.mnr, b.mn), c.mxl = a.mxl, c.mxr = max(a.mxr, b.mx);
			c.ans2 = max(max(a.ans2, b.ans3), make_tuple(b.mx.first - a.mnr.first, a.mnr.second, b.mx.second));
		} else {
			c.mnl = min(a.mn, b.mnl), c.mnr = b.mnr, c.mxl = max(a.mx, b.mxl), c.mxr = b.mxr;
			c.ans2 = max(max(a.ans3, b.ans2), make_tuple(b.mxl.first - a.mn.first, a.mn.second, b.mxl.second));
		}

		return c;
	}
} nd[N << 2];

int tag[N << 2];

inline int ls(int x) {
	return x << 1;
}

inline int rs(int x) {
	return x << 1 | 1;
}

inline void spread(int x, int k) {
	nd[x].mnc += k, tag[x] += k;
}

inline void pushdown(int x) {
	if (tag[x])
		spread(ls(x), tag[x]), spread(rs(x), tag[x]), tag[x] = 0;
}

void build(int x, int l, int r) {
	if (l == r) {
		nd[x].ans1 = nd[x].ans2 = nd[x].ans3 = make_tuple(0, l, l);
		nd[x].mnc = 0, nd[x].mn = nd[x].mx = make_pair(a[l].second, l);
		nd[x].mnl = nd[x].mnr = make_pair(inf, 0), nd[x].mxl = nd[x].mxr = make_pair(-inf, 0);
		return;
	}

	int mid = (l + r) >> 1;
	build(ls(x), l, mid), build(rs(x), mid + 1, r);
	nd[x] = nd[ls(x)] + nd[rs(x)];
}

void update(int x, int nl, int nr, int l, int r, int k) {
	if (l <= nl && nr <= r) {
		spread(x, k);
		return;
	}

	pushdown(x);
	int mid = (nl + nr) >> 1;

	if (l <= mid)
		update(ls(x), nl, mid, l, r, k);

	if (r > mid)
		update(rs(x), mid + 1, nr, l, r, k);

	nd[x] = nd[ls(x)] + nd[rs(x)];
}

void modify(int x, int nl, int nr, int p) {
	if (nl == nr) {
		nd[x].ans1 = nd[x].ans2 = nd[x].ans3 = make_tuple(-inf, 0, 0);
		nd[x].mn = nd[x].mnl = nd[x].mnr = make_pair(inf, 0);
		nd[x].mx = nd[x].mxl = nd[x].mxr = make_pair(-inf, 0);
		return;
	}

	pushdown(x);
	int mid = (nl + nr) >> 1;

	if (p <= mid)
		modify(ls(x), nl, mid, p);
	else
		modify(rs(x), mid + 1, nr, p);

	nd[x] = nd[ls(x)] + nd[rs(x)];
}
} // namespace SMT

signed main() {
	scanf("%d", &n);

	for (int i = 1; i <= n; ++i)
		scanf("%d%d", &a[i].first, &a[i].second);

	sort(a + 1, a + n + 1, greater<pair<int, int> >());
	SMT::build(1, 1, n);
	ll ans = 0;

	for (int i = 1; i <= n / 2; ++i) {
		auto res = max(SMT::nd[1].ans1, SMT::nd[1].ans2);
		printf("%lld\n", ans += get<0>(res));

		if (get<1>(res) < get<2>(res))
			SMT::update(1, 1, n, get<1>(res), get<2>(res) - 1, res == SMT::nd[1].ans1 ? 1 : -1);

		SMT::modify(1, 1, n, get<1>(res)), SMT::modify(1, 1, n, get<2>(res));
	}

	return 0;
}

CF2029I Variance Challenge

给出 \(a_{1 \sim n}\) 与整数 \(k\) ,定义一次操作为选择一个区间加 \(k\) 。对于 \(p = 1, 2, \cdots, m\) ,求 \(p\) 次操作后 \(a\) 的方差乘 \(n^2\) 的最小值。

\(n, m \leq 5000\)\(nm \leq 2 \times 10^4\)

注意到函数 \(f(x) = \sum (a_i - x)^2\) 的最小值在 \(\overline{a}\) 处取到,为 \(a\) 的方差乘上 \(n\)

可以发现可能的 \(\overline{a}\) 只有 \(nm\) 种,考虑枚举 \(\overline{a} = a_0\) 计算 \(1 \leq p \leq m\)\(f(x_0)\) 的最小值。

建立费用流模型:

  • \(S\)\(i\) 连流量为 \(+ \infty\) 、费用为 \(0\) 的边,表示区间开始。
  • \(i\)\(T\) 连流量为 \(+ \infty\) 、费用为 \(0\) 的边,表示区间结束。
  • \(i\)\(i + 1\)\(m\) 条边,流量均为 \(1\) ,第 \(j\) 条边费用为 \((a_i + jk - x_0)^2 - (a_i + (j - 1)k - x_0)^2\)
    • 正确性:由于 \(f(x)\) 是下凸的,因此导数递增,每次都会优先流费用最小的边。

限制流量为 \(m\) ,直接模拟增广的流程,维护正向边和反向边,每次暴力遍历选出最小子段和即可,时间复杂度 \(O(n^2 m^2)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 5e3 + 7;

__int128 a[N], ans[N];
int f[N];

__int128 k;
int n, m;

inline void write(__int128 x) {
    if (x < inf)
        printf("%lld ", (ll)x);
    else
        printf("%lld%018lld ", (ll)(x / inf), (ll)(x % inf));
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d%d", &n, &m, &k), k *= n;
        __int128 S = 0;

        for (int i = 1; i <= n; ++i)
            scanf("%d", a + i), S += (a[i] *= n);

        fill(ans + 1, ans + m + 1, (__int128)inf * inf);

        for (int t = 1; t <= n * m; ++t) {
            __int128 average = (S + k * t) / n, res = 0;

            auto sq = [](__int128 x) {
                return x * x;
            };

            for (int i = 1; i <= n; ++i)
                res += sq(a[i] - average);

            memset(f + 1, 0, sizeof(int) * n);

            for (int p = 1; p <= m; ++p) {
                __int128 mx = -inf, sum = 0;
                int L = 0, R = 0, op = 0;

                for (int i = 1, j = 1; i <= n; ++i) {
                    sum += sq(a[i] + f[i] * k - average) - sq(a[i] + k * (f[i] + 1) - average);

                    if (sum > mx)
                        mx = sum, op = 1, L = j, R = i;

                    if (sum < 0)
                        sum = 0, j = i + 1;
                }

                sum = 0;

                for (int i = 1, j = 1; i <= n; ++i) {
                    sum += f[i] ? sq(a[i] + f[i] * k - average) - sq(a[i] + k * (f[i] - 1) - average) : -inf;

                    if (sum > mx)
                        mx = sum, op = -1, L = j, R = i;

                    if (sum < 0)
                        sum = 0, j = i + 1;
                }

                res -= mx;

                for (int i = L; i <= R; ++i)
                    f[i] += op;

                ans[p] = min(ans[p], res);
            }
        }

        for (int i = 1; i <= m; ++i)
            write(ans[i] / n);

        puts("");
    }

    return 0;
}

增量费用任意流模型

P4694 [PA 2013] Raper

需要生产 \(k\) 张光盘,每张光盘都进行先 A 厂、后 B 厂的加工。一共有 \(n\) 天,第 \(i\) 天两者加工费用分别为 \(a_i, b_i\)

每天可以先送一张光盘到 A 厂(或者不送),再送一张在 A 厂加工过的光盘到 B 厂(或者不送)。

每家工厂一天只能对一张光盘进行操作,同一张光盘在一天内生产出来是允许的。

求生产 \(k\) 张光盘的最小花费。

\(k \leq n \leq 5 \times 10^5\)

建立费用流模型:

  • \(S\) 向每天的 A 厂连边,流量为 \(1\) ,费用为 \(a_i\)
  • 每天的 B 厂向 \(T\) 连边,流量为 \(1\) ,费用为 \(b_i\)
  • 对于 \(i \leq j\) ,第 \(i\) 天的 A 厂向第 \(j\) 天的 B 厂连边,流量为 \(1\) ,费用为 \(0\)
  • 限制源点总流量为 \(k\)

由费用流的凸性,答案关于 \(k\) 是凸的,因此可以 wqs 二分,则无需考虑 \(k\) 的限制。

考虑增量最小费用任意流模型,按时间倒序加点,则每个加入的 A 都能与原来的 B 匹配,此时会出现:

  • 新的负增广路: \(S \to A \to B \to T\) ,对应当前 \(A\) 选一个最小的未匹配的 \(B\)

  • 新的负环:\(S \to A \to B \to A' \to S\) :对应当前 \(A\) 去替换之前一个较大的 \(A'\)

用堆维护新的负环即可做到 \(O(n \log n \log V)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 5e5 + 7;

ll a[N], b[N];

int n, k;

inline pair<ll, int> check(ll lambda) {
    priority_queue<ll> qa;
    priority_queue<ll, vector<ll>, greater<ll> > qb;
    pair<ll, int> ans = make_pair(0, 0);

    for (int i = n; i; --i) {
        qb.emplace(b[i]);
        ll res1 = inf, res2 = inf;

        if (!qa.empty())
            res1 = a[i] - qa.top();

        if (!qb.empty())
            res2 = a[i] + qb.top() - lambda;

        if (min(res1, res2) <= 0) {
            if (res2 <= res1)
                ans.first += res2, ++ans.second, qb.pop(), qa.emplace(a[i]);
            else
                ans.first += res1, qa.pop(), qa.emplace(a[i]);
        }
    }

    return ans;
}

signed main() {
    scanf("%d%d", &n, &k);

    for (int i = 1; i <= n; ++i)
        scanf("%lld", a + i);

    for (int i = 1; i <= n; ++i)
        scanf("%lld", b + i);

    ll l = -1e14, r = 1e14, ans = r;

    while (l <= r) {
        ll mid = (l + r) >> 1;

        if (check(mid).second >= k)
            ans = mid, r = mid - 1;
        else
            l = mid + 1;
    }

    printf("%lld", check(ans).first + 1ll * ans * k);
    return 0;
}

P6122 [NEERC 2016] Mole Tunnels

给出一棵满二叉树,其中 \(fa_i = \lfloor \frac{i}{2} \rfloor\) ,第 \(i\) 个点上有 \(c_i\) 个食物。有 \(m\) 只鼹鼠,第 \(i\) 只鼹鼠在 \(p_i\) 。对于所有 \(k \in [1, m]\) ,求前 \(k\) 只鼹鼠吃到食物的最小行动距离,其中每个食物只能供一只鼹鼠吃。

\(n, m \leq 10^5\)

建立费用流模型:

  • \(S\) 向所有食物连流量为 \(c_i\) 、费用为 \(0\) 的边。
  • 每个食物向相邻的食物连流量为 \(+ \infty\) 、费用为 \(1\) 的边。
  • 每个 \(p_i\)\(T\) 连流量为 \(1\) 、费用为 \(0\) 的边,出现多少次就连多少条。

考虑增量最小费用最大流,则每次需要找到一条最短的增广路。

对于一对父子关系 \((f, u)\) ,一开始的边是 \((u, f, + \infty, 1), (f, u, 0, -1), (f, u, + \infty, 1), (u, f, 0, -1)\) ,因此每次找增广路时优先走反向边。

考虑维护每个点的从上到下的流量 \(f_i\) ,当其为正时表示父亲到儿子只能走正向边,儿子到父亲优先选反向边,为负时相反,为 \(0\) 时都只能走正向边。再维护 \(g_i\) 表示 \(i\) 到子树内距离最短的点。

新加入点 \(p\) 时,不断跳父亲 \(u\)\(p \to u \to g_u\) 最短的增广路,然后暴力向上更新即可。

时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e5 + 7;

pair<int, int> g[N];

int a[N], f[N];

int n, m;

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

inline void pushup(int x) {
    g[x] = a[x] ? make_pair(0, x) : make_pair(inf, 0);

    if (ls(x) <= n)
        g[x] = min(g[x], make_pair(g[ls(x)].first + (f[ls(x)] < 0 ? -1 : 1), g[ls(x)].second));

    if (rs(x) <= n)
        g[x] = min(g[x], make_pair(g[rs(x)].first + (f[rs(x)] < 0 ? -1 : 1), g[rs(x)].second));
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    for (int i = n; i; --i)
        pushup(i);

    ll ans = 0;

    while (m--) {
        int x;
        scanf("%d", &x);
        pair<int, int> cur = make_pair(inf, 0);

        for (int u = x, d = 0; u; d += (f[u] > 0 ? -1 : 1), u >>= 1)
            cur = min(cur, make_pair(g[u].first + d, u));

        printf("%lld ", ans += cur.first);

        for (int u = x; u != cur.second; u >>= 1)
            --f[u], pushup(u);

        --a[g[cur.second].second];

        for (int u = g[cur.second].second; u != cur.second; u >>= 1)
            ++f[u], pushup(u);

        for (int u = cur.second; u; u >>= 1)
            pushup(u);
    }

    return 0;
}

P3826 [NOI2017] 蔬菜

\(n\) 种蔬菜,第 \(i\) 种单价为 \(a_i\) ,初始数量为 \(c_i\)

初次出售第 \(i\) 种蔬菜时会获得 \(s_i\) 的奖励,若不出售则会有 \(x_i\) 数量会变质。

每天最多出售 \(m\) 数量蔬菜,\(k\) 次询问总天数为 \(p_i\) 时的最大收益。

\(n, p_i \leq 10^5\)\(m \leq 10\)

首先最优情况下每天一定优先出售当天将要变质的蔬菜,可以理解为按照过期时间将其分类。

根据经验,删除时不好处理的,考虑时光倒流转化为加入。每天都会进货一些菜,菜可以无限期保存。

由此可以建立费用流模型,对第 \(j\) 天进货的第 \(i\) 种蔬菜建点 \((i, j)\) ,对第 \(i\) 天出售建点 \(h_i\)

  • \(S \to (i, j)\) ,流量为进货数量,费用为 \(a_i\)
  • \((i, j) \to h_i\) ,流量为 \(+ \infty\) ,费用为 \(0\)
  • \((i, j) \to (i, j - 1)\) ,流量为 \(+ \infty\) ,费用为 \(0 \)
  • \(h_i \to T\) ,流量为 \(m\) ,费用为 \(0\)
  • \(i\) 第一次进货时,将 \(S \to (i, j)\) 的流量分一个单位出来,费用为 \(a_i + s_i\)

由于有时序的限制,考虑增量费用流模型,按时间升序加入出售点 \(h_i\)

  • 增广路:\(S \to (k, j) \to \cdots \to (i, j) \to h_i \to T\) ,根据 \(k\)\(i\) 的大小关系可以分为三类。
  • 环:由于只有 \(S\) 出边有权,因此只要考虑 \(S\) 所在的环 \(S \to (i_1, j_1) \to h_{i_1} \to T \to h_{i_2} \to (i_2, j_2) \to S\) ,这显然不如增广路,因为 \(T \to S\) 的费用 \(\leq 0\)

因此可以得到 \(S\) 的出边不会退流,因此前一天的出售方案是当前出售方案的子集。

考虑先求出前 \(p\) 天的最优方案,推到前 \(p - 1\) 天的最优方案,只要保留最贵的 \((p - 1) \times m\) 个,因此只要排序一遍即可。问题转化为求前 \(p\) 天的最优方案。此时无需考虑时序的限制,可以按照时间倒序加入菜和出售点,则增广路只有两种:卖掉今天的菜、卖掉之间加入的菜。

用堆维护这个过程,时间复杂度 \(O(m \times \max \{ p_i \} \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7;

struct Vegetable {
    int a, s, c, x;
} p[N];

vector<int> ins[N];

int qry[N], sold[N];

int n, m, k;

signed main() {
    scanf("%d%d%d", &n, &m, &k);

    for (int i = 1; i <= n; ++i)
        scanf("%d%d%d%d", &p[i].a, &p[i].s, &p[i].c, &p[i].x);

    for (int i = 1; i <= k; ++i)
        scanf("%d", qry + i);

    int tim = *max_element(qry + 1, qry + k + 1);

    for (int i = 1; i <= n; ++i) {
        if (p[i].x)
            ins[min((p[i].c + p[i].x - 1) / p[i].x, tim)].emplace_back(i);
        else
            ins[tim].emplace_back(i);
    }

    priority_queue<pair<int, int> > q;
    vector<int> surplus;

    for (int i = tim; i; --i) {
        for (int it : surplus)
            q.emplace(p[it].a + (sold[it] ? 0 : p[it].s), it);

        surplus.clear();

        for (int it : ins[i])
            q.emplace(p[it].a + (sold[it] ? 0 : p[it].s), it);

        int cnt = m;

        while (!q.empty() && cnt) {
            int x = q.top().second;

            if (sold[x]) {
                int now = min(p[x].c - 1ll * p[x].x * (i - 1) - sold[x], (ll)cnt);
                sold[x] += now, cnt -= now;

                if (p[x].c - 1ll * p[x].x * (i - 1) == sold[x]) {
                    q.pop();

                    if (p[x].x)
                        surplus.emplace_back(x);
                }
            } else {
                sold[x] = 1, --cnt, q.pop();

                if (p[x].c - 1ll * p[x].x * (i - 1) > 1)
                    q.emplace(p[x].a, x);
                else if (p[x].x)
                    surplus.emplace_back(x);
            }
        }
    }

    vector<ll> ans;

    for (int i = 1; i <= n; ++i)
        if (sold[i])
            ans.emplace_back(p[i].a + p[i].s), ans.insert(ans.end(), sold[i] - 1, p[i].a);
    
    sort(ans.begin(), ans.end(), greater<ll>());
    ans.insert(ans.begin(), 0);

    for (int i = 1; i < ans.size(); ++i)
        ans[i] += ans[i - 1];

    for (int i = 1; i <= k; ++i)
        printf("%lld\n", ans[min((int)ans.size() - 1, qry[i] * m)]);

    return 0;
}

任意流消圈模型

[AGC018C] Coins

\(x + y + z\) 个人,第 \(i\) 个人有 \(a_i\) 个金币、\(b_i\) 个银币、\(c_i\) 个铜币。

要选出 \(x\) 个人获得其金币、 \(y\) 个人获得其银币、 \(z\) 个人获得其铜币。

在不重复选人的情况下,最大化获得的币的总数。

\(x + y + z \leq 10^5\)

建立费用流模型:

  • \(S\) 向每个人连流量为 \(1\) 、费用为 \(0\) 的边。
  • 每个人向三个虚点分别连流量为 \(1\) 、费用分别为 \(a_i, b_i, c_i\) 的边。
  • 三个虚点向 \(T\) 分别连流量为 \(x, y, z\) 、费用为 \(0\) 的边。

最大费用流即为答案。

由于有 \(x, y, z\) 三个限制,因此考虑先求出任意流然后消圈。可以发现环一定建立在每个人和虚点之间,考虑其实际意义就是将一些人的选择互换,具体情况可以分为基本的五种:

  • \(A \to B, B \to C, C \to A\)
  • \(A \to C, C \to B, B \to A\)
  • \(A \to B, B \to A\)
  • \(A \to C, C \to A\)
  • \(B \to C, C \to B\)

开六个堆维护每一步转换的决策即可,时间复杂度 \(O(n \log n)\)

类似的题目:CF730I Olympiad in Programming and Sports

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 1e5 + 7;

int id[N];

struct Heap {
    priority_queue<pair<int, int> > q;
    
    int op;
    
    inline Heap(int _op) : op(_op) {}
    
    inline void maintain() {
        while (!q.empty() && id[q.top().second] != op)
            q.pop();
    }
    
    inline bool empty() {
        return maintain(), q.empty();
    }
    
    inline pair<int, int> top() {
        return maintain(), q.top();
    }
    
    inline void emplace(int x, int id) {
        q.emplace(x, id);
    }
} qab(1), qac(1), qbc(2), qba(2), qca(3), qcb(3);

int a[N], b[N], c[N];

int x, y, z;

inline void updateA(int x) {
    id[x] = 1, qab.emplace(b[x] - a[x], x), qac.emplace(c[x] - a[x], x);
}

inline void updateyB(int x) {
    id[x] = 2, qba.emplace(a[x] - b[x], x), qbc.emplace(c[x] - b[x], x);
}

inline void updateC(int x) {
    id[x] = 3, qca.emplace(a[x] - c[x], x), qcb.emplace(b[x] - c[x], x);
}

signed main() {
    scanf("%d%d%d", &x, &y, &z);

    for (int i = 1; i <= x + y + z; ++i)
        scanf("%d%d%d", a + i, b + i, c + i);

    ll ans = 0;

    for (int i = 1; i <= x; ++i)
        ans += a[i], updateA(i);

    for (int i = x + 1; i <= x + y; ++i)
        ans += b[i], updateyB(i);

    for (int i = x + y + 1; i <= x + y + z; ++i)
        ans += c[i], updateC(i);

    for (;;) {
        ll res = -inf, res1 = -inf, res2 = -inf, res3 = -inf, res4 = -inf, res5 = -inf;

        if (!qab.empty() && !qba.empty())
            res = max(res, res1 = qab.top().first + qba.top().first);

        if (!qbc.empty() && !qcb.empty())
            res = max(res, res2 = qbc.top().first + qcb.top().first);

        if (!qac.empty() && !qca.empty())
            res = max(res, res3 = qac.top().first + qca.top().first);

        if (!qab.empty() && !qbc.empty() && !qca.empty())
            res = max(res, res4 = qab.top().first + qbc.top().first + qca.top().first);

        if (!qac.empty() && !qcb.empty() && !qba.empty())
            res = max(res, res5 = qac.top().first + qcb.top().first + qba.top().first);

        if (res <= 0)
            break;

        ans += res;

        if (res1 == res) {
            int x = qab.top().second, y = qba.top().second;
            updateyB(x), updateA(y);
        } else if (res2 == res) {
            int y = qbc.top().second, z = qcb.top().second;
            updateC(y), updateyB(z);
        } else if (res3 == res) {
            int x = qac.top().second, z = qca.top().second;
            updateC(x), updateA(z);
        } else if (res4 == res) {
            int x = qab.top().second, y = qbc.top().second, z = qca.top().second;
            updateyB(x), updateC(y), updateA(z);
        } else {
            int x = qac.top().second, z = qcb.top().second, y = qba.top().second;
            updateC(x), updateyB(z), updateA(y);
        }
    }

    printf("%lld", ans);
    return 0;
}

[ARC168F] Up-Down Queries

对于长度为 \(n\) 的操作序列 \(x_{1 \sim n}\) ,定义 \(f(x_{1 \sim n})\) 为:有一个长度为 \(m\) 的全 \(0\) 序列 \(y_{1 \sim m}\) ,对 \(i = 1, 2, \cdots, n\) 一次进行如下操作:

  • 对于 \(1 \leq j \leq x_i\) ,令 \(y_j \gets \max(y_j - 1, 0)\)
  • 对于 \(x_i < j \leq m\) ,令 \(y_j \gets y_j + 1\)

所有操作进行后,定义 \(f(x_{1 \sim n}) = \sum_{i = 1}^m y_i\)

给定初始操作序列,\(q\) 次对操作序列的单点修改操作,每次修改后求 \(f(x_{1 \sim n})\)

\(n \leq 2.5 \times 10^5\)

先将一次操作为将 \(y_{x_i + 1 \sim m}\) 加上 \(2\) ,然后将全局非 \(0\) 的位置减去 \(1\)

不难发现 \(y_{1 \sim m}\) 始终单调不降,考虑记 \(c_i = y_i - y_{i - 1}\) (钦定 \(y_0 = 0\) ),则 \(f(x_{1 \sim n}) = \sum_{i = 1}^m c_i (m - i + 1)\) 。考虑将其视为每次往可重集 \(S\) 中放 \(c_i\)\(m - i + 1\) ,然后对所有数求和。考虑一次操作的影响:

  • \(y_{x_i + 1 \sim m}\) 加上 \(2\) :等价于将 \(c_{x_i + 1}\) 加上 \(2\) ,即向 \(S\) 中放两个 \(m - x_i\)
  • 全局非 \(0\) 的位置减去 \(1\) :将第一个非 \(0\)\(c\) 减去 \(1\) ,即删去集合中的最大值。

问题转化为:维护一个初始为空的可重集,每次加入两个 \(m - x_i\) ,然后删去集合最大值。考虑将删除最大值变成删除任意数,最小化删去的数的和。

建立费用流模型:

  • \((S, i, 2, m - x_i)\) :加入两个 \(m - x_i\)
  • \((i, T, 1, 0)\) :删去一个数。
  • \((i, i + 1, +\infty, 0)\) :当前数可以之后删去。

最小费用最大流即为答案。

考虑任意流消圈模型,先找到一个初始最大流:\(S \to i \to T\) ,然后不断找负环增广。

由于只有 \(S\) 所连的边有费用,因此只有两种负环:

  • \(S \to i \to i + 1 \to \cdots \to j - 1 \to j \to S\) :限制为存在边 \(S \to i\) 和边 \(j \to S\)
  • \(S \to j \to j - 1 \to \cdots \to i + 1 \to i \to S\) :限制为存在边 \(S \to j\) 和边 \(i \to S\) ,且 \(j \to i\) 路径上均存在反向边。

开两棵线段树,一棵维护与 \(S\) 连的边的情况,一棵维护链上的反向边。由于是单调修改,因此每次找到的负环一定包含修改的位置,且每次只会增广 \(O(1)\) 次,时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2.5e5 + 7;

int a[N], flow[N];

ll ans;
int n, m, q;

namespace SMT {
pair<ll, int> mn[N << 2], mx[N << 2];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

void build(int x, int l, int r) {
    mn[x] = make_pair(0, l), mx[x] = make_pair(0, r);

    if (l == r) {
        flow[l] = 1;
        return;
    }

    int mid = (l + r) >> 1;
    build(ls(x), l, mid), build(rs(x), mid + 1, r);
}

void update(int x, int nl, int nr, int p, int k) {
    if (nl == nr) {
        flow[nl] += k;
        mx[x] = (flow[nl] < 2 ? make_pair(a[nl], nl) : make_pair(-inf, 0));
        mn[x] = (flow[nl] ? make_pair(a[nl], nl) : make_pair(inf, 0));
        return;
    }

    int mid = (nl + nr) >> 1;

    if (p <= mid)
        update(ls(x), nl, mid, p, k);
    else
        update(rs(x), mid + 1, nr, p, k);

    mn[x] = min(mn[ls(x)], mn[rs(x)]), mx[x] = max(mx[ls(x)], mx[rs(x)]);
}

pair<int, int> querymin(int x, int nl, int nr, int l, int r) {
    if (l <= nl && nr <= r)
        return mn[x];

    int mid = (nl + nr) >> 1;

    if (r <= mid)
        return querymin(ls(x), nl, mid, l, r);
    else if (l > mid)
        return querymin(rs(x), mid + 1, nr, l, r);
    else
        return min(querymin(ls(x), nl, mid, l, r), querymin(rs(x), mid + 1, nr, l, r));
}

pair<int, int> querymax(int x, int nl, int nr, int l, int r) {
    if (l <= nl && nr <= r)
        return mx[x];

    int mid = (nl + nr) >> 1;

    if (r <= mid)
        return querymax(ls(x), nl, mid, l, r);
    else if (l > mid)
        return querymax(rs(x), mid + 1, nr, l, r);
    else
        return max(querymax(ls(x), nl, mid, l, r), querymax(rs(x), mid + 1, nr, l, r));
}
} // namespace SMT

inline pair<int, int> querymin(int l, int r) {
    return l <= r ? SMT::querymin(1, 1, n, l, r) : make_pair(inf, 0);
}

inline pair<int, int> querymax(int l, int r) {
    return l <= r ? SMT::querymax(1, 1, n, l, r) : make_pair(-inf, 0);
}

namespace SGT {
int mn[N << 2], tag[N << 2];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

inline void spread(int x, int k) {
    mn[x] += k, tag[x] += k;
}

inline void pushdown(int x) {
    if (tag[x])
        spread(ls(x), tag[x]), spread(rs(x), tag[x]), tag[x] = 0;
}

void update(int x, int nl, int nr, int l, int r, int k) {
    if (l <= nl && nr <= r) {
        spread(x, k);
        return;
    }

    pushdown(x);
    int mid = (nl + nr) >> 1;

    if (l <= mid)
        update(ls(x), nl, mid, l, r, k);

    if (r > mid)
        update(rs(x), mid + 1, nr, l, r, k);

    mn[x] = min(mn[ls(x)], mn[rs(x)]);
}

int searchl(int x, int nl, int nr, int l, int r) {
    if (r < nl || l > nr || mn[x])
        return 0;

    if (nl == nr)
        return nl;

    pushdown(x);
    int mid = (nl + nr) >> 1, res = searchl(ls(x), nl, mid, l, r);
    return res ? res : searchl(rs(x), mid + 1, nr, l, r);
}

int searchr(int x, int nl, int nr, int l, int r) {
    if (r < nl || l > nr || mn[x])
        return 0;

    if (nl == nr)
        return nl;

    pushdown(x);
    int mid = (nl + nr) >> 1, res = searchr(rs(x), mid + 1, nr, l, r);
    return res ? res : searchr(ls(x), nl, mid, l, r);
}
} // namespace SGT

inline void update1(int x) {
    if (flow[x] == 2)
        return;

    int p = (x > 1 ? SGT::searchr(1, 1, n, 1, x - 1) : 0);
    auto res = min(querymin(x + 1, n), querymin(p + 1, x - 1));

    if (res.first >= a[x])
        return;

    SMT::update(1, 1, n, x, 1), SMT::update(1, 1, n, res.second, -1);
    ans -= a[x] - a[res.second];

    if (res.second < x)
        SGT::update(1, 1, n, res.second, x - 1, -1);
    else
        SGT::update(1, 1, n, x, res.second - 1, 1);
}

inline void update2(int x) {
    if (!flow[x])
        return;

    int p = SGT::searchl(1, 1, n, x, n);
    auto res = max(querymax(1, x - 1), querymax(x + 1, p));

    if (res.first <= a[x])
        return;

    SMT::update(1, 1, n, x, -1), SMT::update(1, 1, n, res.second, 1);
    ans -= a[res.second] - a[x];

    if (res.second < x)
        SGT::update(1, 1, n, res.second, x - 1, 1);
    else
        SGT::update(1, 1, n, x, res.second - 1, -1);
}

signed main() {
    scanf("%d%d%d", &n, &m, &q);
    SMT::build(1, 1, n);

    for (int i = 1; i <= n; ++i) {
        scanf("%d", a + i), a[i] = m - a[i];
        SMT::update(1, 1, n, i, 0), ans += a[i] * (2 - flow[i]);
        update1(i), update1(i);
    }

    while (q--) {
        int x, k;
        scanf("%d%d", &x, &k);
        k = m - k, swap(a[x], k);
        SMT::update(1, 1, n, x, 0), ans += (a[x] - k) * (2 - flow[x]);

        if (a[x] > k)
            update1(x), update1(x);
        else
            update2(x), update2(x);

        printf("%lld\n", ans);
    }

    return 0;
}
posted @ 2025-05-28 19:00  wshcl  阅读(128)  评论(0)    收藏  举报