DP 凸优化

DP 凸优化

wqs 二分

wqs 二分主要用来处理一类恰好选 \(k\) 个的问题,这类题目若不限制选的个数,那么很容易求出最优方案。

使用前提:原问题具有凸性。设 \(g_i\) 表示选 \(i\) 个物品的答案,那么所有 \((i, g_i)\) 点组成一个凸包,满足 \(g'(k)\) 单调。

先不考虑恰好选 \(k\) 个的限制,考虑二分附加权值。判定是每选一个就减去附加权值,则选取的次数越多,附加权值影响越大,进而影响选的数量。根据选的数量来调整 \(mid\) ,最后调整到恰好选 \(k\) 个时减掉 \(k\) 倍的附加权值即为答案。

本质就是二分斜率,使得凸包切线切点在 \(x = k\) 处,检查函数返回的是截距。

可能出现一条切线切多个点的情况,因此最后要减掉的是 \(k\) 倍的附加权值。

P5633 最小度限制生成树

给定一张无向图,求 \(deg_s = k\) 的情况下的最小生成树,或报告无解。

\(n \leq 5 \times 10^4\)\(m \leq 5 \times 10^5\)\(k \leq 100\)

二分附加权值 \(mid\) ,与 \(s\) 连接的边的边权都减去这个 \(mid\) ,则最终MST中与 \(s\) 连接的边的数量受 \(mid\) 影响,于是可以调整使得最终 \(deg_s =k\)

时间复杂度 \(O(m \log m \log V)\)

P2619 [国家集训队] Tree I 做法也是类似的,把附加权值放在白色边上即可。

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

struct Edge {
    int u, v, w, c;
    
    inline bool operator < (const Edge &rhs) const {
        return w == rhs.w ? c > rhs.c : w < rhs.w;
    }
} e[M];

struct DSU {
    int fa[N];

    inline void prework(int n) {
        iota(fa + 1, fa + 1 + n, 1);
    }

    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];

        return x;
    }

    inline void merge(int x, int y) {
        fa[find(y)] = find(x);
    }
} dsu;

int n, m, s, k, tot;

inline pair<ll, int> Kruskal() {
    sort(e + 1, e + 1 + m), dsu.prework(n);
    pair<ll, int> ans = make_pair(0ll, 0);
    
    for (int i = 1; i <= m; ++i)
        if (dsu.find(e[i].u) != dsu.find(e[i].v))
            dsu.merge(e[i].u, e[i].v), ans.first += e[i].w, ans.second += e[i].c;
    
    return ans;
}

inline pair<ll, int> check(int k) {
    for (int i = 1; i <= m; ++i)
        if (e[i].c)
            e[i].w -= k;
    
    pair<ll, int> ans = Kruskal();

    for (int i = 1; i <= m; ++i)
        if (e[i].c)
            e[i].w += k;
    
    return ans;
}

signed main() {
    scanf("%d%d%d%d", &n, &m, &s, &k);
    dsu.prework(n);
    
    for (int i = 1; i <= m; ++i) {
        scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
        e[i].c = (e[i].u == s || e[i].v == s), dsu.merge(e[i].u, e[i].v);
    }

    for (int i = 2; i <= n; ++i)
        if (dsu.find(i) != dsu.find(1))
            return puts("Impossible"), 0;
    
    if (check(-inf).second > k || check(inf).second < k)
        return puts("Impossible"), 0;
        
    int l = -inf, r = inf, ans = 0;
    
    while (l <= r) {
        int mid = (l + r) >> 1;
        
        if (check(mid).second >= k)
            ans = mid, r = mid - 1;
        else
            l = mid + 1;
    }
    
    printf("%lld", check(ans).first + 1ll * k * ans);
    return 0;
}

Maximum Deviation Spanning Tree

定义可重集 \(S\) 的权值为 \(f(S) = \min_x (\sum_{i \in S} |i - x|)\)

给定一张无向图,边带边权,求一棵生成树满足边集的权值最大。

\(n \leq 2 \times 10^5\)\(m \leq 5 \times 10^5\)\(w \in [1, 10^9]\)

先观察权值的性质,显然 \(x\) 会取到中位数。

考虑拆开绝对值,当 \(n - 1\) 为偶数时,考虑钦定一半的数为 \(+\) ,剩余一半的数为 \(-\) ,最大化带符号和。将边 \((u, v, w)\) 拆为一条正边 \((u, v, w)\) 和一条负边 \((u, v, -w)\) ,恰好一半的限制只要做 wqs 二分即可。

再考虑 \(n - 1\) 为奇数的情况,问题等价于钦定 \(\frac{n - 2}{2}\) 条正边和负边以及一条 \(0\) 边,只要在 Kruskal 过程中加入 \(n - 2\) 条边然后退出即可。

时间复杂度 \(O(m \alpha(n) \log V)\)

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

struct DSU {
    int fa[N];
    
    inline void prework(int n) {
        iota(fa + 1, fa + n + 1, 1);
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline void merge(int x, int y) {
        fa[find(y)] = find(x);
    }
} dsu;

struct Edge {
    ll w;
    int u, v, op;

    inline bool operator < (const Edge &rhs) const {
        return w == rhs.w ? op > rhs.op : w > rhs.w;
    }
} e1[M], e2[M], e[M << 1];

int n, m;

inline pair<ll, int> calc(ll lambda) {
    for (int i = 1; i <= m; ++i)
        e1[i].w -= lambda;

    merge(e1 + 1, e1 + m + 1, e2 + 1, e2 + m + 1, e + 1);

    for (int i = 1; i <= m; ++i)
        e1[i].w += lambda;

    dsu.prework(n);
    pair<ll, int> ans = make_pair(0ll, 0);

    for (int i = 1, cnt = 0; i <= m * 2 && cnt < (n - 1) >> 1 << 1; ++i) {
        int u = e[i].u, v = e[i].v;

        if (dsu.find(u) != dsu.find(v))
            ++cnt, ans.first += e[i].w, ans.second += (e[i].op == 1), dsu.merge(u, v);
    }

    return ans;
}

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

    for (int i = 1; i <= m; ++i) {
        scanf("%d%d%lld", &e1[i].u, &e1[i].v, &e1[i].w);
        e1[i].op = 1, e2[i] = e1[i], e2[i].w *= -1, e2[i].op *= -1;
    }

    sort(e1 + 1, e1 + m + 1), sort(e2 + 1, e2 + m + 1);
    ll l = -2e9, r = 2e9, ans = r;

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

        if (calc(mid).second >= (n - 1) / 2)
            ans = mid, l = mid + 1;
        else
            r = mid - 1;
    }

    printf("%lld", calc(ans).first + ans * ((n - 1) / 2));
    return 0;
}

P4983 忘情

定义一段序列 \(a_{l \sim r}\) 的价值为:

\[((\sum_{i = l}^r a_i) + 1)^2 \]

\(a_{1 \sim n}\) 分为 \(m\) 段,求每段价值和的最小值。

\(n \leq 10^5\)

首先由于 \((a + b)^2 \geq a^2 + b^2\) ,所以要分的段数要越多越好,答案关于 \(m\) 增加而减小。且因为先选减小值更小的地方分开更优,具有凸性。

于是可以 wqs 二分,判定部分可以斜率优化,注意斜率优化的判断要加入选取数量为第二关键字,尽量多选。

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

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

ll s[N], f[N];
int a[N], q[N], g[N];

int n, m;

inline double slope(int i, int j) {
    return ((f[i] + s[i] * s[i]) - (f[j] + s[j] * s[j])) / (s[i] - s[j]);
}

inline pair<ll, int> check(ll k) {
    for (int i = 1, head = 0, tail = 0; i <= n; ++i) {
        while (head < tail && (slope(q[head], q[head + 1]) < (s[i] + 1) * 2 ||
            (slope(q[head], q[head + 1]) == (s[i] + 1) * 2 && g[q[head + 1]] > g[q[head]])))
            ++head;
        
        f[i] = f[q[head]] + (s[i] - s[q[head]] + 1) * (s[i] - s[q[head]] + 1) - k;
        g[i] = g[q[head]] + 1;
        
        while (head < tail && (slope(q[tail - 1], q[tail]) > slope(q[tail], i) || (
            slope(q[tail - 1], q[tail]) == slope(q[tail], i) && g[q[tail - 1]] > g[q[tail]])))
            --tail;
        
        q[++tail] = i;
    }
    
    return make_pair(f[n], g[n]);
}

signed main() {
    scanf("%d%d", &n, &m);
    
    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i), s[i] = s[i - 1] + a[i];
    
    ll l = -1e18, r = 1e18, ans = 0;
    
    while (l <= r) {
        ll mid = (l + r) >> 1;
        
        if (check(mid).second >= m)
            ans = mid, r = mid - 1;
        else
            l = mid + 1;
    }
    
    printf("%lld", check(ans).first + ans * m);
    return 0;
}

CF802O April Fools' Problem (hard)

\(n\) 道题,第 \(i\) 天可以:

  • 花费 \(a_i\) 准备一道题,上限一次。
  • 花费 \(b_i\) 打印一道题,上限一次。

二者可以同时选择,准备的题可以留到以后打印。

求准备并打印 \(k\) 道题的最小花费。

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

若不考虑 \(k\) 的限制,则是一道反悔贪心模板。

加入 \(k\) 的限制后,只要在外面套一个 wqs 二分即可。

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

ll a[N], b[N];

int n, k;

inline pair<ll, int> check(ll k) {
    pair<ll, int> res = make_pair(0, 0);
    priority_queue<pair<ll, int> > q;
    
    for (int i = 1; i <= n; ++i)
        b[i] -= k;
    
    for (int i = 1; i <= n; ++i) {
        q.emplace(-a[i], 1);
        
        if (-b[i] > -q.top().first) {
            res.first += b[i] - q.top().first, res.second += q.top().second;
            q.pop(), q.emplace(b[i], 0);
        }
    }
    
    for (int i = 1; i <= n; ++i)
        b[i] += k;
    
    return res;
}

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 = -1e18, r = 1e18, 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 + ans * k);
    return 0;
}

CF739E Gosha is hunting

现有 \(n\) 只神奇宝贝,你有 \(a\) 个宝贝球和 \(b\) 个超级球,宝贝球抓到第 \(i\) 只神奇宝贝的概率是 \(p_i\),超级球抓到的概率则是 \(u_i\)

不能在同一只神奇宝贝上使用超过一个同种球,但是可以往同一只上既使用宝贝球又使用超级球(都抓到算一个)。

求合理分配下抓到神奇宝贝的总个数期望的最大值。

\(n \leq 2000\)

首先球全部用完一定最优。考虑 wqs 二分斜率 \(\lambda\) ,每用一个超级球答案就减去 \(\lambda\) ,DP 出超级球选 \(b\) 个的方案。

直接 DP 是 \(O(n^2 \log V)\) 的,但是可以进一步优化,考虑每个位置的四种选择:

  • 用宝贝球和超级球:\(p + u - pu - \lambda\)
  • 用宝贝球:\(p\)
  • 用超级球: \(u - \lambda\)
  • 不用球:\(0\)

先钦定所有位置都不用宝贝球,考虑一个位置从不用宝贝球到用宝贝球,答案增加量就是:

\[\max(p + u - pu - \lambda, p) - \max(u - \lambda, 0) \]

对这个增加量排序后贪心即可,时间复杂度 \(O(n \log n \log V)\)

#include <bits/stdc++.h>
using namespace std;
const double eps = 1e-9;
const int N = 2e3 + 7;

struct Node {
    double x;
    int ka, kb;

    inline bool operator < (const Node &rhs) const {
        return x > rhs.x;
    }
} nd[N];

double p[N], u[N];

int n, a, b;

inline pair<double, int> check(double k) {
    pair<double, int> res = make_pair(0, 0);

    for (int i = 1; i <= n; ++i) {
        if (p[i] + u[i] - p[i] * u[i] - k > p[i])
            nd[i].ka = 1, nd[i].x = p[i] + u[i] - p[i] * u[i] - k;
        else
            nd[i].ka = 0, nd[i].x = p[i];

        if (u[i] - k > 0)
            nd[i].kb = 1, nd[i].x -= u[i] - k, res.first += u[i] - k;
        else
            nd[i].kb = 0;
    }

    sort(nd + 1, nd + 1 + n);

    for (int i = 1; i <= a; ++i)
        res.first += nd[i].x, res.second += nd[i].ka;

    for (int i = a + 1; i <= n; ++i)
        res.second += nd[i].kb;

    return res;
}

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

    for (int i = 1; i <= n; ++i)
        scanf("%lf", p + i);

    for (int i = 1; i <= n; ++i)
        scanf("%lf", u + i);

    double l = 0, r = 1, ans = r;

    while (r - l > eps) {
        double mid = (l + r) / 2;

        if (check(mid).second >= b)
            l = mid;
        else
            ans = r = mid;
    }

    printf("%.6lf", check(ans).first + ans * b);
    return 0;
}

P4383 【八省联考2018】林克卡特树

给定一棵 \(n\) 个点的树,边带权。在树上选出 \(k + 1\) 条互不相交链,最大化权值和。

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

根据 wqs 二分的经典套路,二分斜率 \(\lambda\) ,下面考虑没有 \(k\) 的限制怎么做。

\(f_{u, 0/1/2}\) 表示考虑到 \(u\)\(deg_u = 0/1/2\) 时的答案,具体的:

  • \(deg = 0\) :这个点没有连边。
  • \(deg = 1\) :这个点连着一条未完成的链,该链还未计入答案。
  • \(deg = 2\) :这个点连着一条连接两个不同子树的链。

首先约定在每个节点的全部转移结束时,进行一次更新:

\[g_u = \max(f_{u, 0}, f_{u, 1} + \lambda, f_{u, 2}) \]

这样就把 \(u\) 的全部最优解统计了出来,答案即为 \(g_1\)

则有转移方程:

\[f_{u, 2} = \max \begin{Bmatrix} f_{u, 2} + g_v \\ f_{u, 1} + w(u, v) + f_{v, 1} \\ f_{u, 2} \end{Bmatrix} \]

第一行表示 \(u\) 不接到 \(v\) 上,直接继承 \(v\) 的最优解。

第二行表示把 \(u, v\) 两条未完成的链拼起来,得到一条完成的链。

\[f_{u, 1} = \max \begin{Bmatrix} f_{u, 1} + g_v \\ f_{u, 0} + w(u, v) + f_{v, 1} \\ f_{u, 1} \end{Bmatrix} \]

第一行表示 \(u\) 不接到 \(v\) 上,直接继承 \(v\) 的最优解。

第二行表示 \(u\) 接到 \(v\) 上,继承 \(v\) 一条未完成的链,得到一条未完成的链

\[f_{u, 0} = \max \begin{Bmatrix} f_{u, 0} + g_v \\ f_{u, 0} \end{Bmatrix} \]

这里 \(u\) 必须不接 \(v\) ,只能取 \(v\) 的最优解。

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

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

struct Graph {
    vector<pair<int, int> > e[N];
    
    inline void insert(int u, int v, int w) {
        e[u].emplace_back(v, w);
    }
} G;

pair<ll, int> f[N][3], g[N];

int n, k;

template <class T = int>
inline T read() {
    char c = getchar();
    bool sign = (c == '-');
    
    while (c < '0' || c > '9')
        c = getchar(), sign |= (c == '-');
    
    T x = 0;
    
    while ('0' <= c && c <= '9')
        x = (x << 1) + (x << 3) + (c & 15), c = getchar();
    
    return sign ? (~x + 1) : x;
}

inline pair<ll, int> operator + (const pair<ll, int> &a, const pair<ll, int> &b) {
    return make_pair(a.first + b.first, a.second + b.second);
}

inline pair<ll, int> operator + (const pair<ll, int> &a, const ll &b) {
    return make_pair(a.first + b, a.second);
}

void dfs(int u, int fa, const pair<ll, int> lambda) {
    f[u][0] = f[u][1] = f[u][2] = make_pair(0, 0);
    f[u][2] = max(f[u][2], lambda);

    for (auto it : G.e[u]) {
        int v = it.first, w = it.second;

        if (v == fa)
            continue;

        dfs(v, u, lambda);
        f[u][2] = max(f[u][2], max(f[u][2] + g[v], f[u][1] + w + f[v][1] + lambda));
        f[u][1] = max(f[u][1], max(f[u][1] + g[v], f[u][0] + w + f[v][1]));
        f[u][0] = max(f[u][0], f[u][0] + g[v]);
    }

    g[u] = max(f[u][0], max(f[u][1] + lambda, f[u][2]));
}

inline pair<ll, int> check(ll lambda) {
    return dfs(1, 0, make_pair(-lambda, 1)), g[1];
}

signed main() {
    n = read(), k = read() + 1;

    for (int i = 1; i < n; ++i) {
        int u = read(), v = read(), w = read();
        G.insert(u, v, w), G.insert(v, u, w);
    }

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

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

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

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

P6246 [IOI2000] 邮局 加强版 加强版

\(n\) 个村庄,需要放 \(m\) 个邮局,求每个村庄到最近邮局的距离和的最小值。

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

\(f_{i, j}\) 表示前 \(i\) 个村庄放 \(j\) 个邮局的最小距离和,\(w(l, r)\) 表示在 \([l, r]\) 范围村庄放一个邮局的最小距离和,则有:

\[f_{i, j} = \min_{k = 0}^{i - 1} \{ f_{k, j - 1} + w(k + 1, i) \} \]

决策单调性优化做到 \(O(n^2)\)

考虑用 wqs 二分规避 \(j\) 的限制,于是得到一个1D/1D 的 DP,并且也有决策单调性,可以二分队列做到 \(O(n \log n \log V)\)

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

struct Node {
    int j, l, r;
} q[N];

ll s[N], f[N];
int a[N], g[N];

int n, m;

inline ll w(int l, int r) {
    int mid = (l + r) >> 1;
    return (s[r] - s[mid]) - 1ll * a[mid] * (r - mid) + 1ll * a[mid] * (mid - l + 1) - (s[mid] - s[l - 1]);
}

inline ll calc(int i, int j) {
    return f[j] + w(j + 1, i);
}

inline int BinarySearch(int l, int r, int i, int j) {
    int ans = r + 1;

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

        if (calc(mid, i) <= calc(mid, j))
            ans = mid, r = mid - 1;
        else
            l = mid + 1;
    }

    return ans;
}

inline pair<ll, int> check(ll lambda) {
    int head = 1, tail = 0;
    q[++tail] = (Node) {0, 1, n};

    for (int i = 1; i <= n; ++i) {
        if (q[head].r == i - 1)
            ++head;

        f[i] = calc(i, q[head].j) + lambda, g[i] = g[q[head].j] + 1;
        int pos = n + 1;

        while (head <= tail) {
            if (calc(q[tail].l, i) <= calc(q[tail].l, q[tail].j))
                pos = q[tail--].l;
            else {
                pos = BinarySearch(q[tail].l, q[tail].r, i, q[tail].j);
                q[tail].r = pos - 1;
                break;
            }
        }

        if (pos != n + 1)
            q[++tail] = (Node) {i, pos, n};
    }

    return make_pair(f[n], g[n]);
}

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

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

    sort(a + 1, a + 1 + n);

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

    ll l = 0, r = 1e12, ans = 0;

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

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

    printf("%lld", check(ans).first - ans * m);
    return 0;
}

P5308 [COCI 2018/2019 #4] Akvizna

你面临 \(n\) 名参赛者的挑战,最终要将他们全部战胜。

每轮可以选择淘汰一些选手,同时获得该轮淘汰人数除以该轮总人数的奖金。

需要举办 \(k\) 轮比赛使得最终只剩一个人,最大化奖金和。

\(n \leq 10^5\)

可以发现淘汰的顺序不影响答案,因此不妨每次将淘汰的人视为一段。

套路的,先用 wqs 二分规避 \(k\) 的限制。考虑倒序 DP,设 \(f_i\) 表示从后往前数某轮还剩 \(i\) 个人的最大奖金,有:

\[f_i = \max_{j < i} \left( f_j + \frac{i - j}{i} \right) \]

假设对于 \(k < j\)\(j\) 优于 \(k\) ,则:

\[\begin{align} f_j + \frac{i - j}{i} &> f_k + \frac{i - k}{i} \\ \frac{f_j - f_k}{j - k} &> \frac{1}{i} \end{align} \]

直接斜率优化即可,时间复杂度 \(O(n \log V)\)

#include <bits/stdc++.h>
typedef long double ld;
using namespace std;
const ld eps = 1e-12;
const int N = 1e5 + 7;

ld f[N];
int g[N], q[N];

int n, k;

inline ld slope(int i, int j) {
    return (f[i] - f[j]) / (i - j);
}

inline int check(ld k) {
    int head = 1, tail = 0;
    q[++tail] = 0;

    for (int i = 1; i <= n; ++i) {
        while (head < tail && (slope(q[head], q[head + 1]) > (ld)1 / i ||
            (slope(q[head], q[head + 1]) == 1.0 / i && g[q[head]] < g[q[head + 1]])))
            ++head;

        f[i] = f[q[head]] + (ld)(i - q[head]) / i - k, g[i] = g[q[head]] + 1;

        while (head < tail && (slope(q[tail - 1], q[tail]) < slope(q[tail], i) ||
            (slope(q[tail - 1], q[tail]) == slope(q[tail], i) && g[q[tail - 1]] < g[q[tail]])))
            --tail;

        q[++tail] = i;
    }

    return g[n];
}

signed main() {
    scanf("%d%d", &n, &k);
    ld l = -1e9, r = 1e9, ans = r;

    while (r - l > eps) {
        ld mid = (l + r) / 2;

        if (check(mid) >= k)
            ans = l = mid;
        else
            r = mid;
    }

    check(ans);
    printf("%.9LF", f[n] + ans * k);
    return 0;
}

Slope Trick

Slope Trick 通常用于维护连续、分段、一次、凸函数,满足每段斜率较小且每段斜率为整数。

考虑维护某个点 \(x_0\) 处的 \(f(x_0)\)\(k_{x_0}\) ,以及函数每个转折点(斜率突变的点)的集合。具体的,若函数在 \(x\) 处斜率增加了 \(\Delta k\) ,就在集合中插入 \(\Delta k\)\(x\) ,这样集合中相邻两个点的斜率差就为 \(1\)

事实上连续、分段、一次、凸函数的性质是很好的,可以支持很多操作,以下凸函数为例:

  • 相加:将 \(f(x_0)\)\(k_{x_0}\) 直接相加,转折点集合直接合并即可。
  • 取前缀 \(\min\) 或后缀 \(\min\) :去掉 \(k > 0\)\(k < 0\) 的部分。
  • 求全局 \(\min\) :仅保留 \(k = 0\) 的部分。
  • 整体平移:维护 \(f(x_0)\)\(k_{x_0}\) 的变化,转折点打全局平移标记。

P4597 序列 sequence

给定 \(a_{1 \sim n}\),每次可以将一个位置的数字加一或减一,求使得原序列不降的最少操作次数。

原题面还要求修改后的数列只能出现修改前的数,事实上归纳可以证明这个限制不会让答案变劣。

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

\(f_{i, j}\) 表示考虑前 \(i\) 个数,最后一个数为 \(j\) 的答案,则:

\[f_{i, j} = |j - a_i| + \min_{k \leq j} f_{i - 1, k} \]

\(F_i(j) = f_{i, j}\) ,显然 \(F_i\)\(F_{i - 1}\) 取前缀 \(\min\) 后加绝对值函数 \(y = |x - a_i|\) 得到,答案即为 \(\min F_n(i)\)

考虑用堆维护 \(F\) ,记当前最右侧函数为 \(y = kx + b\) ,则:

  • 先取一遍前缀 \(\min\) ,推平斜率 \(>0\) 的部分。
    • 对于转折点的维护,就直接弹出右边 \(k\) 个转折点即可。
    • 对于最右侧函数的维护,记当前要弹出的最右侧转折点为 \(x_0\) 。由于 \(x_0\) 同时满足左边和右边的一次函数,则 \(kx + b = (k - 1)x + b'\) ,因此 \(b' = b + x\)
  • 再加上一个绝对值函数 \(y = |x - a|\)
    • 先合并转折点,直接往堆中加入两个 \(a\) 即可。
    • 再维护最右侧函数,新的最右侧函数为 \(y = (kx + b) + (x - a) = (k + 1)x + (b - a)\)

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

几道类似的题目:

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;

priority_queue<int> q;

int n;

signed main() {
    scanf("%d", &n);
    int k = 0;
    ll b = 0;

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

        for (; k; --k)
            b += q.top(), q.pop();

        q.emplace(x), q.emplace(x);
        ++k, b -= x;
    }

    for (; k; --k)
        b += q.top(), q.pop();

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

[ABC217H] Snuketoon

有一个数轴上的游戏,\(0\) 时刻在 \(0\) 号节点。每个时刻可以选择将坐标 \(\pm 1\) ,或者不移动。

接下来按时间升序给出 \(n\) 个事件,每一个事件用 \(T_i, D_i, X_i\) 描述。假设 \(T_i\) 时刻在 \(p\)

  • \(D_i=0\),则会受到 \(\max(0, X_i - p)\) 的伤害。
  • \(D_i=1\),则会受到 \(\max(0, p - X_i)\) 的伤害。

最小化 \(n\) 次事件所受伤害量。

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

\(f_{i, j}\) 表示 \(T_i\) 时刻在 \(j\) 可能的最小伤害,\(t\) 为与上一次的时间间隔,则:

\[f_{i, j} = \left( \min_{k = j - t}^{j + t} f_{i - 1, k} \right) + [(j > X_i) = D_i] \times |j - X_i| \]

\([l, r]\) 为斜率为 \(0\) 的段,可以发现一次更新就是把 \(< l\) 的部分和 \(>r\) 的部分向两边平移 \(t\) ,再加上一个只有半边的绝对值函数。

用一个大根堆和一个小根堆维护斜率为 \(0\) 的段两边的转折点,那么平移操作可以通过打标记实现。然后就是插入一个拐点 \(X_i\) ,根据 \(X_i\) 在斜率为 \(0\) 的段的左侧、右侧、中间,分类讨论贡献即可。

直接这么做会导致插入的拐点不在定义域范围内,一个做法是往两个堆中插入 \(n\)\(0\) 。由于扩大定义域的操作是平移操作,那么即使加入了定义域外的转折点,也不会从堆顶取出。

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

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

priority_queue<int> ql;
priority_queue<int, vector<int>, greater<int> > qr;

int n, tagl, tagr;

signed main() {
    scanf("%d", &n);
    ll ans = 0;

    for (int i = 1; i <= n; ++i)
        ql.emplace(0), qr.emplace(0);

    for (int i = 1, lst = 0; i <= n; ++i) {
        int t, d, x;
        scanf("%d%d%d", &t, &d, &x);
        tagl -= t - lst, tagr += t - lst, lst = t;

        if (d) {
            if (x < ql.top() + tagl)
                ans += ql.top() + tagl - x, qr.emplace(ql.top() + tagl - tagr), ql.pop(), ql.emplace(x - tagl);
            else
                qr.emplace(x - tagr);
        } else {
            if (x > qr.top() + tagr)
                ans += x - qr.top() - tagr, ql.emplace(qr.top() + tagr - tagl), qr.pop(), qr.emplace(x - tagr);
            else
                ql.emplace(x - tagl);
        }
    }

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

CF1534G A New Beginning

在一个二维平面上,你一开始在 \((0,0)\) ,只能向上或向右移动。

给定 \(n\) 个关键点点,第 \(i\) 个点为 \((x_i,y_i)\) 。在点 \((X,Y)\) 对点 \((x,y)\) 打标记,需要花费 \(\max(|X-x|,|Y-y|)\) 的代价。

求标记所有点的最小代价。

\(n \leq 8 \times 10^5\)\(0 \leq x_i, y_i \leq 10^9\)

首先套路地把切比雪夫距离转化为曼哈顿距离,于是问题转化为每次向右上或右下走一格,代价变为曼哈顿距离。

将所有点按 \(x\) 坐标排序,设 \(f_{i, j}\) 表示标记到第 \(i\) 个点时 \(y\) 坐标为 \(j\) 的答案,则:

\[f_{i, j} = |j - y_i| + \min_{k = j - (x_i - x_{i - 1})}^{j + (x_i - x_{i - 1})} f_{i - 1, k} \]

直接开两个堆维护斜率为 \(0\) 的那段左右两边的转折点即可,时间复杂度 \(O(n \log n)\)

一道类似的题目:CF372C Watching Fireworks is Fun

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

struct Point {
    int x, y;

    inline bool operator < (const Point &rhs) const {
        return x < rhs.x;
    }
} p[N];

priority_queue<int> ql;
priority_queue<int, vector<int>, greater<int> > qr;

int n, tagl, tagr;

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

    for (int i = 1; i <= n; ++i) {
        int x, y;
        scanf("%d%d", &x, &y);
        p[i] = (Point) {x + y, x - y};
    }

    sort(p + 1, p + 1 + n);
    ll ans = 0;
    ql.emplace(p[1].y), qr.emplace(p[1].y);

    for (int i = 2; i <= n; ++i) {
        tagl -= p[i].x - p[i - 1].x, tagr += p[i].x - p[i - 1].x;

        if (p[i].y < ql.top() + tagl) {
            ans += ql.top() + tagl - p[i].y;
            qr.emplace(ql.top() + tagl - tagr);
            ql.pop(), ql.emplace(p[i].y - tagl), ql.emplace(p[i].y - tagl);
        } else if (p[i].y > qr.top() + tagr) {
            ans += p[i].y - (qr.top() + tagr);
            ql.emplace(qr.top() + tagr - tagl);
            qr.pop(), qr.emplace(p[i].y - tagr), qr.emplace(p[i].y - tagr);
        } else
            ql.emplace(p[i].y - tagl), qr.emplace(p[i].y - tagr);
    }

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

P3642 [APIO2016] 烟火表演

给定一棵以 \(1\) 为根的 \(n + m\) 个节点的带边权树,其中叶子为 \(n + 1 \sim n + m\) 。可以花费 \(|x - y|\) 的代价将一条边的边权从 \(x\) 修改至 \(y\) ,最小化代价使得所有叶子到根节点的距离相同。

\(1 \leq n + m \leq 3 \times 10^5\)

\(f_{u, i}\) 为以 \(u\) 为根的子树中到叶子的距离为 \(i\) 的最小代价,有:

\[f_{u, i} = \sum_{v \in son(u)} \min_{j \leq i} \{ f_{v, j} + |w(u, v) - (i - j)| \} \]

可以发现这是一个下凸函数,记 \(F_u(x) = \min_{i \leq x} \{ f_{u, i} + |w - x + i| \}\)\([l, r]\)\(f_u\) 中斜率为 \(0\) 的段,则:

\[F_u(x) = \begin{cases} f_{u, x} + w & (x < l) \\ f_{u, l} + w - x + l & (l \leq x < l + w) \\ f_{u, l} & (l + w \leq x \leq r + w) \\ f_{u, r} + x - r - w & (x > r + w) \end{cases} \]

具体的:

  • \(x < l\) :因为改变边权的代价函数斜率为 \(1\) ,而 \(f\) 函数 \(<l\) 的时候斜率一定 \(\leq -1\) ,因此把边权改为 \(0\) 一定不劣。
  • \(l \leq x < l + w\) :只要保证 \(x = l\) 就能取到函数的最小值,而 \(f\) 函数 \(<l\) 的时候斜率一定 \(\leq -1\) ,因此尽量从 \(f_u\) 的最小值转移一定不劣。
  • \(l + w \leq x \leq r + w\) :不用改变 \(w\) 就可以保证能取到最小值 \(f_{u, x - w} = f_{u, l}\)
  • \(x > r + w\) :与第一条类似。

考虑一次更新对 \(F\) 的变化:

  • \(x < l\) :向上平移 \(w\) 单位。
  • \(l \leq x < l + w\) :向上平移 \(w\) 后把斜率变为 \(-1\)
  • \(l + w \leq x \leq r + w\) :将原先 \([l, r]\) 的函数平移到 \([l + w, r + w]\)
  • \(x > r + w\) :斜率变为 \(1\)

接下来考虑维护转折点集合,可以发现只要把 \(>l\) 的转折点全部删除,再加入 \(l + w\)\(r + w\) 两个转折点即可。

由于有求和操作,需要合并转折点集合,故考虑用可并堆维护转折点。

接下来考虑维护每个点的函数值,一个取巧的方法是发现 \(F_1(0)\) 即为所有边权之和,因此最终可以将所有转折点拿出计算答案。

使用左偏树,时间复杂度 \(O((n + m) \log (n + m))\) ,注意左偏树要开两倍空间因为一个点会新增两个转折点。

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

int fa[N], deg[N], val[N];

int n, m;

template <class T = int>
inline T read() {
    char c = getchar();
    bool sign = (c == '-');
    
    while (c < '0' || c > '9')
        c = getchar(), sign |= (c == '-');
    
    T x = 0;
    
    while ('0' <= c && c <= '9')
        x = (x << 1) + (x << 3) + (c & 15), c = getchar();
    
    return sign ? (~x + 1) : x;
}

namespace LT {
ll val[N];
int fa[N], lc[N], rc[N], dist[N];

int tot;

inline int newnode(ll k) {
    return val[++tot] = k, tot;
}

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

    if (val[a] < val[b])
        swap(a, b);

    rc[a] = merge(rc[a], b);

    if (dist[lc[a]] < dist[rc[a]])
        swap(lc[a], rc[a]);

    dist[a] = dist[rc[a]] + 1;
    return a;
}

template<class ...Args>
inline int merge(int a, int b, Args ...args) {
    return merge(a, merge(b, args...));
}

inline void pop(int &x) {
    x = merge(lc[x], rc[x]);
}
} // namespace LT

signed main() {
    n = read(), m = read();
    ll ans = 0;

    for (int i = 2; i <= n + m; ++i)
        ++deg[fa[i] = read()], ans += (val[i] = read());

    for (int i = n + m; i > 1; --i) {
        ll l = 0, r = 0;

        if (i <= n) {
            for (int j = 1; j < deg[i]; ++j)
                LT::pop(LT::fa[i]);

            r = LT::val[LT::fa[i]], LT::pop(LT::fa[i]);
            l = LT::val[LT::fa[i]], LT::pop(LT::fa[i]);
        }

        LT::fa[fa[i]] = LT::merge(LT::fa[fa[i]], LT::fa[i], LT::newnode(l + val[i]), LT::newnode(r + val[i]));
    }

    while (deg[1]--)
        LT::pop(LT::fa[1]);

    while (LT::fa[1])
        ans -= LT::val[LT::fa[1]], LT::pop(LT::fa[1]);

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

CF280E Sequence Transformation

给定一个不降序列 \(x_{1 \sim n} \in [1, V]\) 与两个整数 \(A, B\)

求一个实数序列 \(y_{1 \sim n} \in [1, V]\) 满足 \(y_i - y_{i - 1} \in [A, B]\) ,最小化代价 \(\sum_{i = 1}^n (x_i - y_i)^2\)

\(n \leq 6000\)

\(f_{i, j}\) 表示前 \(i\) 个位置且 \(y_i = j\) 的最小代价,则:

\[f_{i, j} = (x_i - j)^2 + \min_{k = j - B}^{j - A} f_{i - 1, k} \]

不难发现 \(F_i(j)\) 是一个凸函数,设 \(F_{i - 1}\) 的最小值点为 \(u\) ,则:

\[F_i(j) = \begin{cases} F_{i - 1}(j - A) + (x_i - j)^2 & (j < u + A) \\ F_{i - 1}(u) + (x_i - j)^2 & (u + A \leq j \leq u + B) \\ F_{i - 1}(j - B) + (x_i - j)^2 & (j > u + B) \\ \end{cases} \]

但是二次函数的斜率不是整数,并不好直接用 Slope Trick 维护,考虑对其求导:

\[F_i'(j) = \begin{cases} F_{i - 1}'(j - A) + 2j - 2x_i & (j < u + A) \\ 2j - 2x_i & (u + A \leq j \leq u + B) \\ F_{i - 1}'(j - B) + 2j - 2x_i & (j > u + B) \\ \end{cases} \]

发现其等价于把 \(u\) 左边的部分向右平移 \(A\) 单位, \(u\) 右边的部分向右平移 \(B\) 单位,然后中间部分变为 \(0\) ,再给整体加上 \(2j - 2x_i\)

观察上述转移,每次需要动态找到一个极值点。如果每次都暴力找,时间复杂度 \(O(n^2)\) ,可以通过但不够优美。

由凸函数斜率单调的性质,考虑用平衡树维护所有斜率相同的段,每次找到跨过 \(0\) 点的段即可。然后,每次把左边的段打一个平移 \(A\) 的标记,右边的段打一个 \(B\) 标记,加入中间的 \(0\) 段,最后整体加一个一次函数就好了。

具体实现只要在 fhq-Treap 上维护平移量,和一个一次函数的标记即可,时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef unsigned int uint;
using namespace std;
const double eps = 1e-9;
const int N = 6e3 + 7;

double x[N], mnp[N], y[N];

double V, A, B;
int n;

template <class T = int>
inline T read() {
    char c = getchar();
    bool sign = (c == '-');
    
    while (c < '0' || c > '9')
        c = getchar(), sign |= (c == '-');
    
    T x = 0;
    
    while ('0' <= c && c <= '9')
        x = (x << 1) + (x << 3) + (c & 15), c = getchar();
    
    return sign ? (~x + 1) : x;
}

namespace fhqTreap {
const int S = 1e5 + 7;

struct Node {
    double k, b, l, r;
} nd[S];

struct Tag {
    double k, b, mv;
} tag[S];

uint dat[S];
int lc[S], rc[S];

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

inline int newnode(Node k) {
    dat[++tot] = myrand(), nd[tot] = k;
    return tot;
}

inline void spread(int x, Tag k) {
    tag[x].b += -k.mv * tag[x].k + k.b, nd[x].b += -k.mv * nd[x].k + k.b;
    tag[x].k += k.k, nd[x].k += k.k;
    tag[x].mv += k.mv, nd[x].l += k.mv, nd[x].r += k.mv;
}

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

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

    tag[x].k = tag[x].b = tag[x].mv = 0;
}

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

    pushdown(x);

    if (nd[x].k * nd[x].l + nd[x].b < 0)
        a = x, splitl(rc[x], rc[a], b);
    else
        b = x, splitl(lc[x], a, lc[b]);
}

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

    pushdown(x);

    if (nd[x].k * nd[x].r + nd[x].b > 0)
        b = x, splitr(lc[x], a, lc[b]);
    else
        a = x, splitr(rc[x], rc[a], b);
}

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

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

template <class ...Args>
inline int merge(int a, int b, Args ...args) {
    return merge(merge(a, b), args...);
}

inline int findl(int x) {
    while (lc[x])
        x = lc[x];

    return x;
}
} // namespace fhqTreap

using namespace fhqTreap;

signed main() {
    scanf("%d%lf%lf%lf", &n, &V, &A, &B);

    for (int i = 1; i <= n; ++i)
        scanf("%lf", x + i);

    root = newnode((Node){2, -2 * x[1], 1, V});

    for (int i = 2; i <= n; ++i) {
        int L, X, R;
        splitr(root, L, R), splitl(R, X, R);

        if (X) {
            mnp[i - 1] = -nd[X].b / nd[X].k;

            if (nd[X].l < mnp[i - 1])
                lc[X] = newnode((Node){nd[X].k, nd[X].b, nd[X].l, mnp[i - 1]}), spread(lc[X], (Tag) {0, 0, A});
            
            if (mnp[i - 1] < nd[X].r)
                rc[X] = newnode((Node){nd[X].k, nd[X].b, mnp[i - 1], nd[X].r}), spread(rc[X], (Tag) {0, 0, B});

            nd[X] = (Node){0, 0, mnp[i - 1] + A, mnp[i - 1] + B};
        } else
            mnp[i - 1] = nd[findl(R)].l, X = newnode((Node){0, 0, mnp[i - 1] + A, mnp[i - 1] + B});

        spread(L, (Tag){0, 0, A}), spread(R, (Tag){0, 0, B});
        spread(root = merge(L, X, R), (Tag){2, -2 * x[i], 0});
    }

    int L, X, R;
    splitr(root, L, R), splitl(R, X, R);
    double res = min(X ? -nd[X].b / nd[X].k : nd[findl(R)].l, V);

    for (int i = n; i; --i) {
        y[i] = res;

        if (res < mnp[i - 1] + A)
            res -= A;
        else if (res > mnp[i - 1] + B)
            res -= B;
        else
            res = mnp[i - 1];
    }

    double ans = 0;

    for (int i = 1; i <= n; ++i)
        printf("%lf ", y[i]), ans += (y[i] - x[i]) * (y[i] - x[i]);

    printf("\n%lf", ans);
    return 0;
}

闵可夫斯基和

定义:对于两个点集 \(A, B\) ,它们的闵可夫斯基和为点集 \(C = \{ a + b \mid a \in A, b \in B \}\)

考虑当 \(A, B\) 是凸包/凸壳的情况:

可以发现此时 \(C\) 也是凸包,并且 \(A, B\) 的边向量在 \(C\) 中恰好出现一次,即 \(C\) 的边就是把 \(A\)\(B\) 的边按斜率排序拼接而成的。

考虑 \(x \in \{1, 2, \cdots, n \}\) 的两个下凸壳 \(f, g\) ,其差分数组分别单调。于是可以用 \(O(|f| + |g|)\) 的时间归并差分数组,得到 \(f\)\(g\) 的闵可夫斯基和。

考虑 \((\min, +)\) 卷积,记 \(h_k = \min_{i + j = k} f_i + g_j\) 。由于 \(f, g\) 都是下凸壳,因此 \(h\) 实际上就是 \(f\)\(g\) 做闵可夫斯基和后的下凸包。因此采用闵可夫斯基和可以在线性时间内求出上/下凸包 \((\min, +)\) 卷积的结果。

[ABC383G] Bar Cover

给定权值 \(a_{1 \sim n}\) 和常数 \(k\) ,对于 \(i = 1, 2, \cdots \lfloor \frac{n}{k} \rfloor\) ,求选 \(i\) 个长度为 \(k\) 的不交区间后选出区间内权值和的最大值。

\(n \leq 2 \times 10^5\)\(k \leq \min(n, 5)\)

\(b_i = \sum_{j = i}^{i + k - 1} a_j\) ,问题转化为在 \(b\) 中选若干个数,相邻两个数之间至少间隔 \(k - 1\) 个数,最大化所选数的和。

可以发现答案关于所选的数量构成上凸壳。考虑分治,设 \(f_{i, j, t}, g_{i, j, t}\) 表示左/右区间选了 \(t\) 个数,其中左边空余至少 \(i\) 个,右边空余至少 \(j\) 个的最大和。转移就是对 \(f_{i, p}\)\(g_{k - 1 - p, j}\)\((\max, +)\) 卷积得到左右恰好空 \(i, j\) 个的答案,最后再更新一遍 \(f\) 即可。注意需要特殊转移左区间或右区间不选 \(b\) 的情况,直接做可以做到 \(O(n k^3 \log n)\) ,难以通过。

注意到若区间长度不超过 \(k\) ,则区间最多只能选一个数,因此该部分区间无需分治,直接求即可。此时分治树上只有 \(\lfloor \frac{n}{k} \rfloor\) 个叶子,而分治树的节点数与叶子数同阶,时间复杂度将为 \(O(n k^2 \log n)\)

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

vector<ll> f[LOGN][2][K][K];

ll b[N];
int a[N];

int n, k, m;

inline vector<ll> Minkowski(vector<ll> a, vector<ll> b) {
    vector<ll> c(a.size() + b.size() - 1);
    c[0] = a[0] + b[0];

    for (int i = a.size() - 1; i; --i)
        a[i] -= a[i - 1];

    for (int i = b.size() - 1; i; --i)
        b[i] -= b[i - 1];

    merge(a.begin() + 1, a.end(), b.begin() + 1, b.end(), c.begin() + 1, greater<ll>());

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

    return c;
}

inline void maintain(vector<ll> &a, vector<ll> b) {
    while (a.size() < b.size())
        a.emplace_back(-inf);

    for (int i = 0; i < b.size(); ++i)
        a[i] = max(a[i], b[i]);
}

void solve(int l, int r, int d, int op) {
    for (int i = 0; i < k; ++i)
        for (int j = 0; j < k; ++j)
            f[d][op][i][j] = {max(i, j) <= r - l + 1 ? 0 : -inf};

    if (r - l + 1 <= k) {
        for (int i = 0; i < k; ++i)
            for (int j = 0; j < k; ++j)
                if (l + i <= r - j)
                    f[d][op][i][j].emplace_back(*max_element(b + l + i, b + r - j + 1));

        return;
    }

    int mid = (l + r) >> 1;
    solve(l, mid, d + 1, 0), solve(mid + 1, r, d + 1, 1);

    for (int i = 0; i < k; ++i)
        for (int j = 0; j < k; ++j) {
            maintain(f[d][op][i][min(j + r - mid, k - 1)], f[d + 1][0][i][j]);
            maintain(f[d][op][min(i + mid - l + 1, k - 1)][j], f[d + 1][1][i][j]);
        }

    for (int i = 0; i < k; ++i)
        for (int j = 0; j < k; ++j)
            for (int p = 0; p < k; ++p)
                maintain(f[d][op][i][j], Minkowski(f[d + 1][0][i][p], f[d + 1][1][k - 1 - p][j]));

    for (int i = k - 1; ~i; --i)
        for (int j = k - 1; ~j; --j) {
            if (i)
                maintain(f[d][op][i - 1][j], f[d][op][i][j]);

            if (j)
                maintain(f[d][op][i][j - 1], f[d][op][i][j]);
        }
}

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

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

    m = n - k + 1;

    for (int i = 1; i <= m; ++i)
        for (int j = i; j <= i + k - 1; ++j)
            b[i] += a[j];

    solve(1, m, 0, 0);
    vector<ll> ans;

    for (int i = 0; i < k; ++i)
        for (int j = 0; j < k; ++j)
            maintain(ans, f[0][0][i][j]);

    for (int i = 1; i <= n / k; ++i)
        printf("%lld ", ans[i]);

    return 0;
}

CF2021E3 Digital Village (Extreme Version)

给出一张连通无向图,边带边权。

\(p\) 个关键点 \(s_{1 \sim p}\) ,需要选 \(k\) 个点作为据点。定义两点距离为两点间所有路径上最大边的最小值,最小化所有关键点到其距离最小的据点的距离和。

对每个 \(k = 1, 2, \cdots, n\) 求答案。

\(n, m \leq2 \times 10^5\)

“两点间所有路径上最大边的最小值”不难想到 Kruskal 重构树,该值即为两点 LCA 的权值。

考虑选出的 \(k\) 个据点,将其到根路径上的所有点染黑,则一个关键点的最小距离即为其第一个被染黑的父亲的权值,称其为该关键点的对应权值。

\(f_{u, i}\) 表示仅考虑 \(u\) 子树内、放了 \(i\) 个据点的最小距离和,记 \(siz_u\) 表示 \(u\) 子树内的关键点数量,分类讨论:

  • 左右子树都没据点:\(f_{u, 0} = 0\)
  • 左子树有据点,而右子树没有据点:此时右子树内的关键点的对应权值即为 \(val_u\) ,有 \(f_{u, i} \gets f_{lc_u, i} + val_u \times siz_{rc_u}\)
  • 右子树有据点,而左子树没有据点:此时左子树内的关键点的对应权值即为 \(val_u\) ,有 \(f_{u, i} \gets f_{lc_u, i} + val_u \times siz_{rc_u}\)
  • 左右子树都有据点:\(f_{u, i} \gets \min_{j + k = i} f_{lc_u, j} + f_{rc_u, k}\)

不难发现这是个 \((min, +)\) 卷积的形式,考虑闵可夫斯基和,用可并堆维护差分数组,时间复杂度 \(O(n \log n)\)

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

struct Edge {
    int u, v, w;

    inline bool operator < (const Edge &rhs) const {
        return w < rhs.w;
    }
} e[N];

struct DSU {
    int fa[N];
    
    inline void prework(int n) {
        iota(fa + 1, fa + 1 + n, 1);
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline void merge(int x, int y) {
        fa[find(y)] = find(x);
    }
    
    inline bool check(int x, int y) {
        return find(x) == find(y);
    }
} dsu;

int val[N], lc[N], rc[N], siz[N];

int n, m, p, ext;

inline void Kruskal() {
    memset(val + 1, 0, sizeof(int) * n);
    sort(e + 1, e + m + 1), dsu.prework(n * 2 - 1), ext = n;

    for (int i = 1; i <= m; ++i) {
        int fx = dsu.find(e[i].u), fy = dsu.find(e[i].v);

        if (fx == fy)
            continue;

        val[++ext] = e[i].w;
        dsu.merge(ext, lc[ext] = fx), dsu.merge(ext, rc[ext] = fy);
    }
}

namespace LT {
const int S = 1e6 + 7;

ll val[S];
int rt[N], lc[S], rc[S], dist[S];

int tot;

inline void clear(int n) {
    memset(rt + 1, 0, sizeof(int) * n), tot = 0;
}

inline int newnode(ll k) {
    return val[++tot] = k, lc[tot] = rc[tot] = dist[tot] = 0, tot;
}

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

    if (val[a] > val[b])
        swap(a, b);

    rc[a] = merge(rc[a], b);

    if (dist[lc[a]] < dist[rc[a]])
        swap(lc[a], rc[a]);

    dist[a] = dist[rc[a]] + 1;
    return a;
}

inline void insert(int x, ll k) {
    rt[x] = merge(rt[x], newnode(k));
}

inline ll top(int x) {
    return val[rt[x]];
}

inline void pop(int x) {
    rt[x] = merge(lc[rt[x]], rc[rt[x]]);
}
} //namespace LT

void dfs(int u) {
    if (u <= n) {
        for (int i = 1; i <= siz[u]; ++i)
            LT::insert(u, 0);

        return;
    }

    dfs(lc[u]), dfs(rc[u]), siz[u] = siz[lc[u]] + siz[rc[u]];

    if (LT::rt[lc[u]]) {
        ll x = 1ll * val[lc[u]] * siz[lc[u]] + LT::top(lc[u]);
        LT::pop(lc[u]), LT::insert(lc[u], x - 1ll * val[u] * siz[lc[u]]);
    }

    if (LT::rt[rc[u]]) {
        ll x = 1ll * val[rc[u]] * siz[rc[u]] + LT::top(rc[u]);
        LT::pop(rc[u]), LT::insert(rc[u], x - 1ll * val[u] * siz[rc[u]]);
    }

    LT::rt[u] = LT::merge(LT::rt[lc[u]], LT::rt[rc[u]]);
}

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

    while (T--) {
        scanf("%d%d%d", &n, &m, &p);
        memset(siz + 1, 0, sizeof(int) * n);

        for (int i = 1; i <= p; ++i) {
            int x;
            scanf("%d", &x);
            ++siz[x];
        }

        for (int i = 1; i <= m; ++i)
            scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);

        Kruskal(), LT::clear(ext), dfs(ext);
        ll ans = 1ll * val[ext] * siz[ext];

        for (int i = 1; i <= p; ++i)
            printf("%lld ", ans += LT::top(ext)), LT::pop(ext);

        for (int i = p + 1; i <= n; ++i)
            printf("0 ");

        puts("");
    }
    
    return 0;
}
posted @ 2024-08-10 11:15  wshcl  阅读(23)  评论(0)    收藏  举报