决策单调性优化

决策单调性优化

前置知识

决策单调性

决策单调性是在最优化 DP 中的可能出现的一种性质。

对于形如:

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

的转移方程,记 \(p_i\) 为令 \(f_i\) 取得最小值的 \(j\) 的值(最优决策点)。若 \(p\) 单调不降,则称 \(f\) 具有决策单调性。

trick:凸函数和普通函数做卷积是,决策点在普通函数上具有单调性。

四边形不等式

以上述转移方程为例,若对于任意 \(a \le b \le c \le d\) 均满足:

\[w(a, c) + w(b, d) \le w(a, d) + w(b, c) \]

则称 \(w\) 满足四边形不等式,简记为交叉优于包含。

证明时可以采用另一个形式:对于任意 \(i \le j\) 满足 \(w(i, j) + w(i + 1, j + 1) \le w(i, j + 1) + w(i + 1, j)\)

\(w\) 满足四边形不等式,则上述转移方程满足决策单调性。

四边形不等式的另一个形式为:若对于任意 \(i \le j\) 均满足:

\[w(i, j) + w(i + 1, j + 1) \le w(i, j + 1) + w(i + 1, j) \]

则称 \(w\) 满足四边形不等式。

区间包含单调性

以上述转移方程为例,若对于任意 \(a \le b \le c \le d\) 均满足: \(w(a, d) \ge w(b, c)\) ,则称 \(w\) 满足区间包含单调性。

单峰性转移的指针优化

对于某个状态的最优转移点,其左边的贡献向左单调递减,右边的贡献向右单调递减,则转移具有单峰性。若转移还满足决策单调性,则可以做到均摊 \(O(1)\) 转移。

考虑记录一个指针 \(p\) 表示决策点,由于决策单调性,\(p\) 不会往回跳。遍历每个位置时判断 \(p\) 的最优性,如果后面更优,则 \(p\) 向后跳,否则停住并转移。

根据单峰性这样子一定不会漏最优解。一般的决策单调性若不满足单峰性,使用该流程会局限在一个局部最优解中。

P10260 [COCI 2023/2024 #5] Rolete

\(n\) 个数,可以进行两种操作:

  • 选择一个 \(i\) ,令 \(a_i \gets a_i - 1\) ,花费 \(t\) 的代价。
  • 对于所有 \(i\) ,令 \(a_i \gets \max(a_i - 1, 0)\) ,代价为 \(s + k \times r\) ,其中 \(r\) 为操作前 \(a_i = 0\)\(i\) 的数量。

\(q\) 次询问,每次给出一个 \(h\) ,求所有 \(a_i \le h\) 的最小操作代价。

\(n, t, s, k, a_i, q, h_i \le 10^5\)

若固定了全局操作的次数,则最优决策一定是先全局后单点,由此可以预处理前缀和后 \(O(1)\) 算出 \(calc(h, x)\) 表示询问 \(h\) 时使用 \(x\) 次全局操作的答案。

注意到对于单个 \(h\)\(calc\) 是关于 \(x\) 的单峰性函数,且 \(h\) 增加时最优决策 \(x\) 单调不升,故可以使用上面的流程做到线性。

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

ll f[N], sum[N], ans[N];
int a[N], cnt[N];

int n, t, s, k, q;

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

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

    int m = *max_element(a + 1, a + n + 1);

    for (int i = 1, num = cnt[0]; i <= m; num += cnt[i++])
        f[i] = f[i - 1] + 1ll * num * k + s; // 一起拉 i 次

    for (int i = m; ~i; --i)
        sum[i] = sum[i + 1] + 1ll * cnt[i] * i, cnt[i] += cnt[i + 1];

    auto calc = [](int h, int x) {
        return f[x] + (sum[x + h] - 1ll * cnt[x + h] * (x + h)) * t;
    };

    for (int i = 0, j = m; i <= m; ++i) {
        while (j && calc(i, j - 1) < calc(i, j))
            --j;

        ans[i] = calc(i, j);
    }

    scanf("%d", &q);

    while (q--) {
        int h;
        scanf("%d", &h);
        printf("%lld ", ans[h]);
    }

    return 0;
}

二分队列优化

使用条件:向后枚举时,某种决策只会被更后的决策反超。形式化的,对于任意两个决策 \(j_1 < j_2\) ,存在一个 \(x\) 满足 \(i \le x\)\(j_1\) 优于 \(j_2\)\(i > x\)\(j_1\) 劣于 \(j_2\)

可以证明基于四边形不等式的转移方程同样满足该条件。

考虑建立一个队列维护决策点,队列中保存若干三元组 \((j, l, r)\) ,表示最优决策点为 \(j\) 的区间为 \([l, r]\)

遍历枚举 \(i\) ,执行以下操作:

  • 检查队头:设队头为 \((j_0, l_0, r_0)\) ,若 \(r_0 = i - 1\) ,则删除队头;否则令 \(l_0 \leftarrow i\)
  • 取队头保存最优决策点 \(j\) 进行转移求出 \(f_i\)
  • 尝试插入新决策 \(i\) ,步骤如下:
    • 取出队尾,记为 \((j_t, l_t, r_t)\)
    • 若对于 \(l_t\) 来说, \(i\) 决策优于 \(j_t\) 决策,记 \(pos = l_t\) ,删除队尾重新执行上一步。
    • 否则若对于 \(r_t\) 来说,\(i\) 决策优于 \(j_t\) 决策,则在 \([l_t, r_t]\) 上二分查找位置 \(pos\) ,满足 \([pos, n]\) 的最优决策点均为 \(i\)
    • 将三元组 \((i, pos, n)\) 插入队尾。

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

P1912 [NOI2009] 诗人小G

列出 DP 式:

\[f_i = \min (f_j + |(s_i - s_j) + (i - j) - (L + 1)|^P) \]

按上述方法优化即可。

#include <bits/stdc++.h>
typedef long double ld;
using namespace std;
const int N = 1e5 + 7, S = 3e1 + 7;

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

ld f[N];
int s[N], g[N];
char str[N][S];
bool ed[N];

int n, L, P;

inline ld mi(ld a, int b) {
    ld res = 1;
    
    for (; b; b >>= 1, a *= a)
        if (b & 1)
            res *= a;
    
    return res;
}

inline ld calc(int i, int j) {
    return f[j] + mi(abs(s[i] - s[j] - L), P);
}

signed main() {
    int T;
    scanf("%d", &T);
    
    while (T--) {
        scanf("%d%d%d", &n, &L, &P), ++L;
        
        for (int i = 1; i <= n; ++i)
            scanf("%s", str[i]), s[i] = s[i - 1] + strlen(str[i]) + 1;
        
        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, g[i] = q[head].j);

            if (++q[head].l > q[head].r)
                ++head;

            while (head <= tail && calc(q[tail].l, i) <= calc(q[tail].l, q[tail].j))
                --tail;

            if (head > tail)
                q[++tail] = (Node){i, i + 1, n};
            else {
                auto search = [](int l, int r, int i, int j) {
                    int pos = r + 1;

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

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

                int pos = search(q[tail].l, q[tail].r, i, q[tail].j);

                if (pos <= n)
                    q[tail].r = pos - 1, q[++tail] = (Node){i, pos, n};
            }
        }
        
        if (f[n] > 1e18)
            puts("Too hard to arrange");
        else {
            printf("%.0LF\n", f[n]);
            fill(ed + 1, ed + 1 + n, false);
            
            for (int i = n; i; i = g[i])
                ed[i] = true;
            
            for (int i = 1; i <= n; ++i)
                printf("%s%c", str[i], " \n"[ed[i]]);
        }
        
        puts("--------------------");
    }
    
    return 0;
}

P3515 [POI2011] Lightning Conductor

给出 \(a_{1 \sim n}\) ,对于每个 \(i \in [1, n]\) ,求一个最小的非负整数 \(p\) ,使得对于所有 \(j \in [1, n]\) 都有 \(a_j \le a_i + p - \sqrt{|i - j|}\)

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

转化限制条件为:

\[p + a_i \ge \max_{j = 1}^n \{ a_j + \sqrt{|i - j|} \} \]

将绝对值拆开,正反各做一次,得到:

\[p + a_i \ge \max_{j = 1}^{i - 1} \{ a_j + \sqrt{i - j} \} \]

\(w(j, i) = \sqrt{i - j}\) ,则 \(w(j, i)\) 满足四边形不等式,于是 \(f_i = \max_{j = 1}^{i - 1} \{ a_j + \sqrt{i - j} \}\) 满足决策单调性,直接上二分队列可以做到 \(O(n \log n)\)

事实上本题的反超点是可以 \(O(1)\) 计算的。设两个决策点 \(k < j\) ,反超点为 \(i\) ,则:

\[\begin{align} a_k + \sqrt{i - k} &< a_j + \sqrt{i - j} \\ \sqrt{i - k} - \sqrt{i - j} &< a_j - a_k \end{align} \]

\(a_j - a_k \le 0\) 时不等式恒不成立,令 \(d = a_j - a_k > 0\) ,化简得到:

\[\begin{align} \sqrt{i - k} &< d + \sqrt{i - j} \\ i - k &< d^2 + i - j + 2d \sqrt{i - j} \\ j - k - d^2 &< 2d \sqrt{i - j} \end{align} \]

\(j - k - d^2 \le 0\) 时上式恒成立,否则两边平方得到 \(i > j + \frac{(j - k - d^2)^2}{4d^2}\) ,于是得到反超点为 \(j + \lfloor \frac{(j - k - d^2)^2}{4d^2} \rfloor + 1\)

时间复杂度 \(O(n)\) (忽略预处理开根号的时间)。

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

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

double sq[N], f[N];
int a[N];

int n;

inline double calc(int i, int j) {
    return a[j] + sq[i - j];
}

inline void solve() {
    int head = 1, tail = 0;
    q[++tail] = (Node) {1, 2, n};

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

        f[i] = max(f[i], calc(i, q[head].j));

        if (++q[head].l > q[head].r)
            ++head;

        while (head <= tail && calc(q[tail].l, i) >= calc(q[tail].l, q[tail].j))
            --tail;

        if (head > tail)
            q[++tail] = (Node) {i, i + 1, n};
        else {
            ll d = a[i] - a[q[tail].j],
                k = i - q[tail].j - d * d,
                pos = (d <= 0 ? q[tail].r + 1 : i + ceil((double)k * k / (d * d * 4)));

            if (pos <= n)
                q[tail].r = pos - 1, q[++tail] = (Node) {i, pos, n};
        }
    }
}

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

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i), f[i] = a[i], sq[i] = sqrt(i);

    solve();
    reverse(a + 1, a + n + 1), reverse(f + 1, f + n + 1);
    solve();
    reverse(a + 1, a + n + 1), reverse(f + 1, f + n + 1);

    for (int i = 1; i <= n; ++i)
        printf("%d\n", (int)ceil(f[i] - a[i]));

    return 0;
}

二分栈优化

使用条件:向后枚举时,某种决策只会被更前的决策反超。形式化的,对于任意两个决策 \(j_1 < j_2\) ,存在一个 \(x\) 满足 \(i \le x\)\(j_1\) 劣于 \(j_2\)\(i > x\)\(j_1\) 优于 \(j_2\)

考虑用单调栈维护所有有用的决策,其中栈顶是当前最优决策。

每次加入一个点 \(i\) 时,记栈顶决策点为 \(j\) ,栈顶下面的决策点为 \(k\) ,若 \((j, k)\) 的反超点不超过 \((i, j)\) 的反超点,则说明 \(j\) 会先被 \(k\) 反超而没机会反超 \(i\) ,将其弹出。然后再一个个弹出已经被反超的决策即可。

计算完 \(f_i\) 后,考虑求决策点为 \(i\) 的后缀。由于决策单调性,所以可以二分。

具体地,每次将 \(i\) 与栈顶的决策比较,若栈顶的决策区间内 \(i\) 恒优则弹栈,否则求出分界点后修改栈顶决策区间并压入 \(i\) 及相关后缀。

P5504 [JSOI2011] 柠檬

将一个数列分成若干段,从每一段中选定一个数 \(s_0\) ,假设这个数有 \(t\) 个,则这一段价值为 \(s_0 t^2\) 。求每一段的价值和的最大值。

\(n \le 10^5\)

可以发现最优方案一定满足两端都是 \(s_0\) ,否则可以缩小区间获得更大值。

\(f_i\) 表示以 \(i\) 结尾的最大价值和,则:

\[f_i = \max_{j = 1}^{i} \{ f_{j - 1} + s_i \times (sum_i - sum_j + 1)^2 \} \]

固定 \(j\) 时,\(s_i \times (sum_i - sum_j + 1)^2\) 单调递增。故对于一个 \(j_1 < j_2\) ,存在一个分界点满足分界点前 \(j_2\) 更优,分界点后 \(j_1\) 更优。用二分栈优化即可做到 \(O(n \log n)\)

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

vector<int> vec[N];

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

int n;

inline ll calc(int j, int t) {
    return f[j - 1] + 1ll * a[j] * t * t;
}

inline int check(int x, int y) {
    int l = max(s[x], s[y]), r = n, pos = n + 1;

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

        if (calc(x, mid - s[x] + 1) >= calc(y, mid - s[y] + 1))
            pos = mid, r = mid - 1;
        else
            l = mid + 1;
    }

    return pos;
}

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

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

    for (int i = 1; i <= n; ++i) {
        vector<int> &sta = vec[a[i]];
        #define tp1 sta.back()
        #define tp2 sta[sta.size() - 2]

        while (sta.size() >= 2 && check(tp2, tp1) <= check(tp1, i))
            sta.pop_back();

        sta.emplace_back(i);

        while (sta.size() >= 2 && check(tp2, tp1) <= s[i])
            sta.pop_back();

        f[i] = calc(tp1, s[i] - s[tp1] + 1);
        #undef tp1
        #undef tp2
    }

    printf("%lld", f[n]);
    return 0;
}

整体二分优化

某些 DP 形式如下(相邻层之间转移):

\[f_{i, j} = \min_{k \le j} (f_{i - 1, k} + w(k, j)) \ \ \ \ (1 \le i \le n, 1 \le j \le m) \]

其中 \(i \in [1, n]\)\(j \in [1, m]\) ,共 \(n \times m\) 个状态,每个状态有 \(O(m)\) 个决策,暴力做时间复杂度 \(O(nm^2)\)

\(m_{i, j}\) 为,最优决策点若 \(\forall i, j, m_{i, j} \le m_{i, j + 1}\) ,则可以运用分治思想,递归得到 \(m\) 的上下界,就可以达到 \(O(nm \log m)\) 的时间复杂度。

\(w(l, r)\) 不好直接求,而使用莫队可以推出时,整体二分的结构可以保证移动指针的总复杂度为 \(O(nm \log n)\)

CF321E Ciel and Gondolas

\(n\) 个人,需要将他们分成 \(k\) 组,每组内人的编号连续。给出一个陌生值矩阵,定义一组的陌生值为每一对人的陌生值之和,求总陌生值的最小值。

\(n \le 4000\)\(k \le \min(n, 800)\)\(0 \le a_{i, j} \le 9\)

\(w(l, r)\) 表示将 \(l \sim r\) 的人分为一组的代价,即矩阵中左上角为 \((l, l)\) 右下角为 \((r, r)\) 的元素和的一半。则 \(w\) 满足决策单调性,使用整体二分优化可以做到 \(O(nk \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 4e3 + 7, K = 8e2 + 7;

int a[N][N], f[K][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 int w(int l, int r) {
    return a[r][r] - a[l - 1][r] - a[r][l - 1] + a[l - 1][l - 1];
}

void solve(int d, int l, int r, int L, int R) {
    if (L > R)
        return;

    int mid = (L + R) >> 1, p = 0;
    f[d][mid] = inf;

    for (int i = l; i <= min(mid, r); ++i) {
        int res = f[d - 1][i - 1] + w(i, mid);

        if (res <= f[d][mid])
            f[d][mid] = res, p = i;
    }

    solve(d, l, p, L, mid - 1), solve(d, p, r, mid + 1, R);
}

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

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

    memset(f[0], inf, sizeof(f[0])), f[0][0] = 0;

    for (int i = 1; i <= k; ++i)
        solve(i, 1, n, 1, n);

    printf("%d", f[k][n] / 2);
    return 0;
}

CF868F Yet Another Minimization Problem

给定一个序列 \(a_{1 \sim n}\) ,要把它分成 \(k\) 个子段。每个子段的费用是其中相同元素的对数,求所有子段的费用之和的最小值。

\(n \le 10^5\)\(k \le \min(n, 20)\)

\(f_{i, j}\) 表示前 \(i\) 个元素分为 \(j\) 段的最小费用,\(w(l, r)\) 表示 \([l, r]\) 的费用,则:

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

可以证明 \(w(l, r)\) 满足四边形不等式,故 \(f\) 每一层的转移都具有决策单调性。

solve(l, r, L, R) 表示 \(f_{i, L \sim R}\) 的决策点在 \([l, r]\) 中,则可以每次计算出 \(f_{i, mid}\) 的决策点,将序列分为两部分分治。

接下来考虑如何计算 \(f_{i, mid}\) 的决策点,直接计算是困难的,可以用莫队维护一个指针求解。

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

类似的题:CF833B The Bakery

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

ll f[K][N];
int a[N];

int n, k;

namespace MoAlgorithm {
int cnt[N];

ll result;
int l = 1, r;

inline void update(int x, int k) {
    result -= 1ll * cnt[x] * (cnt[x] - 1) / 2;
    cnt[x] += k;
    result += 1ll * cnt[x] * (cnt[x] - 1) / 2;
}

inline ll query(int L, int R) {
    while (l > L)
        update(a[--l], 1);

    while (r < R)
        update(a[++r], 1);

    while (l < L)
        update(a[l++], -1);

    while (r > R)
        update(a[r--], -1);

    return result;
}
} // namespace MoAlgorithm

void solve(int l, int r, int L, int R, int d) {
    if (L > R)
        return;

    int mid = (L + R) >> 1, p = l;
    f[d][mid] = f[d - 1][l - 1] + MoAlgorithm::query(l, mid);

    for (int i = l + 1; i <= min(mid, r); ++i) {
        ll res = f[d - 1][i - 1] + MoAlgorithm::query(i, mid);

        if (res < f[d][mid])
            f[d][mid] = res, p = i;
    }

    solve(l, p, L, mid - 1, d), solve(p, r, mid + 1, R, d);
}

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

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

    memset(f, inf, sizeof(f)), f[0][0] = 0;

    for (int i = 1; i <= k; ++i)
        solve(1, n, 1, n, i);

    printf("%lld", f[k][n]);
    return 0;
}

P5574 [CmdOI2019] 任务分配问题

给定排列 \(p_{1 \sim n}\) ,将 \(p\) 划分成 \(k\) 段,使每一段的顺序对个数和最小。

\(n \le 2.5 \times 10^4, k \le 25\)

\(f_{i, j}\) 表示前 \(i\) 个数分为 \(j\) 段的答案,\(w(l, r)\) 表示 \(p_{l \sim r}\) 的顺序对个数,则:

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

注意到这里的 \(w(l, r)\) 并不好求,于是考虑采用整体二分维护决策单调性配合莫队+树状数组求顺序对优化即可,时间复杂度 \(O(nk \log^2 n)\)

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

ll f[N][K];
int a[N];

int n, k;

namespace MoAlgorithm {
namespace BIT {
int c[N];

inline void update(int x, int k) {
    for (; x <= n; x += x & -x)
        c[x] += k;
}

inline int query(int x) {
    int res = 0;

    for (; x; x -= x & -x)
        res += c[x];

    return res;
}
} // namespace BIT

ll result;
int l = 1, r = 0;

inline ll calc(int ql, int qr) {
    while (l > ql) {
        --l;
        BIT::update(a[l], 1);
        result += (r - l + 1) - BIT::query(a[l]);
    }

    while (r < qr) {
        ++r;
        result += BIT::query(a[r]);
        BIT::update(a[r], 1);
    }

    while (l < ql) {
        result -= (r - l + 1) - BIT::query(a[l]);
        BIT::update(a[l], -1);
        l++;
    }

    while (r > qr) {
        BIT::update(a[r], -1);
        result -= BIT::query(a[r]);
        r--;
    }

    return result;
}
} // namespace MoAlgorithm

void solve(int l, int r, int L, int R, const int d) {
    if (L > R)
        return;

    int mid = (L + R) >> 1, mnpos = 0;
    f[mid][d] = inf;

    for (int i = l; i <= min(mid, r); ++i) {
        ll res = f[i - 1][d - 1] + MoAlgorithm::calc(i, mid);

        if (res < f[mid][d])
            f[mid][d] = res, pos = i;
    }

    solve(l, pos, L, mid - 1, d), solve(pos, r, mid + 1, R, d);
}

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

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

    for (int i = 1; i <= n; ++i)
        f[i][0] = inf;

    for (int i = 1; i <= k; ++i)
        solve(1, n, 1, n, i);

    printf("%lld", f[n][k]);
    return 0;
}

[ABC348G] Max (Sum - Max)

给定 \(a_{1 \sim n}\)\(b_{1 \sim n}\) ,对于所有 \(k \in [1, n]\) ,求从 \(1 \sim n\) 中选 \(k\) 个数,求 \((\sum a) - (\max b)\) 最大值。

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

先考虑只有一个 \(k\) 的情况,这是一个经典问题,按 \(b\) 升序排序后枚举 \(\max b\) ,则 \(\sum a\) 只要求前 \(k\) 大就行了。

\(f(k, i)\) 表示 \(k\)\(i\) 处决策( \(\max b = b_i\) )的答案。若对于 \(j < i\)\(f(k, j) < f(k, i)\) ,那么 \(k \to k + 1\) 时,由于 \(i\) 所选的 \(a\) 一定不小于 \(j\) 所选的 \(a\) ,故 \(f(k + 1, j) < f(k + 1, i)\) ,因此 \(f(k)\) 具有决策单调性。

用整体二分维护决策,主席树查询前 \(k\) 大即可做到 \(O(n \log n \log V)\)

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

struct Node {
    ll a, b;

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

ll f[N];

int n;

namespace SMT {
const int S = 3e7 + 7;

ll s[S];
int rt[N], lc[S], rc[S], cnt[S];

int tot;

int update(int x, int nl, int nr, int p) {
    int y = ++tot;
    lc[y] = lc[x], rc[y] = rc[x], cnt[y] = cnt[x] + 1, s[y] = s[x] + p;

    if (nl == nr)
        return y;

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

    if (p <= mid)
        lc[y] = update(lc[x], nl, mid, p);
    else
        rc[y] = update(rc[x], mid + 1, nr, p);

    return y;
}

ll query(int x, int nl, int nr, int k) {
    if (nl == nr)
        return 1ll * nl * k;

    int mid = (nl + nr) >> 1;
    return k > cnt[rc[x]] ? s[rc[x]] + query(lc[x], nl, mid, k - cnt[rc[x]]) : query(rc[x], mid + 1, nr, k);
}
} // namespace SMT

void solve(int l, int r, int L, int R) {
    if (L > R)
        return;

    if (l == r) {
        for (int i = L; i <= R; ++i)
            f[i] = SMT::query(SMT::rt[l], -1e9, 1e9, i) - nd[l].b;

        return;
    }

    int mid = (L + R) >> 1, p = 0;
    f[mid] = -inf;

    for (int i = max(mid, l); i <= r; ++i) {
        ll res = SMT::query(SMT::rt[i], -1e9, 1e9, mid) - nd[i].b;

        if (res > f[mid])
            f[mid] = res, p = i;
    }

    solve(l, p, L, mid - 1), solve(p, r, mid + 1, R);
}

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

    for (int i = 1; i <= n; ++i)
        scanf("%lld%lld", &nd[i].a, &nd[i].b);

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

    for (int i = 1; i <= n; ++i)
        SMT::rt[i] = SMT::update(SMT::rt[i - 1], -1e9, 1e9, nd[i].a);

    solve(1, n, 1, n);

    for (int i = 1; i <= n; ++i)
        printf("%lld\n", f[i]);

    return 0;
}

P5892 [IOI2014] holiday 假期

\(n\) 个城市,最开始在城市 \(s\) 。在 \(d\) 天内,每天可以选择移动到相邻的城市或参观所处城市。第一次参观城市 \(i\) 时会获得 \(a_i\) 的奖励,求最大奖励和。

\(n \le 10^5\)

可以发现最优情况一定是一直向一个方向走或先向一个方向走一段后再反方向走一段。

考虑固定了走的区间,设剩余天数为 \(k\) ,那么肯定选前 \(k\) 大的 \(a\) 的和作为答案最优,可以用主席树实现。

先处理掉只往一个方向走的情况,考虑处理先往右后往左的情况,先左后右是类似的。注意到左端点变大时,最优的右端点一定是不降的,于是可以用整体二分优化做到 \(O(n \log n \log V)\)

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

int a[N], b[N];

ll ans;
int n, s, d, m;

namespace SMT {
const int S = N << 5;

ll s[S];
int lc[S], rc[S], cnt[S];
int rt[N];

int tot;

int insert(int x, int nl, int nr, int p) {
    int y = ++tot;
    lc[y] = lc[x], rc[y] = rc[x], cnt[y] = cnt[x] + 1, s[y] = s[x] + b[p];

    if (nl == nr)
        return y;

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

    if (p <= mid)
        lc[y] = insert(lc[x], nl, mid, p);
    else
        rc[y] = insert(rc[x], mid + 1, nr, p);

    return y;
}

ll query(int x, int y, int nl, int nr, int k) {
    if (k >= cnt[y] - cnt[x])
        return s[y] - s[x];

    if (nl == nr)
        return b[nl] * k;

    int mid = (nl + nr) >> 1;
    return k <= cnt[rc[y]] - cnt[rc[x]] ? query(rc[x], rc[y], mid + 1, nr, k) :
        s[rc[y]] - s[rc[x]] + query(lc[x], lc[y], nl, mid, k - (cnt[rc[y]] - cnt[rc[x]]));
}
} // namespace SMT

inline ll calc(int l, int r) {
    int k = d - ((r - s) + (r - l));
    return k > 0 ? SMT::query(SMT::rt[l - 1], SMT::rt[r], 1, m, k) : 0;
}

void solve(int l, int r, int L, int R) {
    if (L > R)
        return;

    if (l == r) {
        for (int i = L; i <= R; ++i)
            ans = max(ans, calc(i, l));

        return;
    }

    int mid = (L + R) >> 1, p = l;
    ll res = calc(mid, l);

    for (int i = l + 1; i <= r; ++i) {
        ll now = calc(mid, i);

        if (now > res)
            res = now, p = i;
    }

    ans = max(ans, res);
    solve(l, p, L, mid - 1), solve(p, r, mid + 1, R);
}

inline void solve() {
    SMT::tot = 0;

    for (int i = 1; i <= n; ++i)
        SMT::rt[i] = SMT::insert(SMT::rt[i - 1], 1, m, a[i]);

    for (int i = 1; i <= s; ++i)
        ans = max(ans, calc(i, s));

    solve(s + 1, n, 1, s - 1);
}

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

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

    sort(b + 1, b + n + 1), m = unique(b + 1, b + n + 1) - b - 1;

    for (int i = 1; i <= n; ++i)
        a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;

    solve();
    s = n - s + 1, reverse(a + 1, a + n + 1);
    solve();
    printf("%lld", ans);
    return 0;
}

LOJ6039. 「雅礼集训 2017 Day5」珠宝 /「NAIPC2016」Jewel Thief

\(n\) 个物品,第 \(i\) 个物品体积为 \(c_i\) ,价值为 \(v_i\)

给定 \(k\) ,对于 \(i = 1, 2, \cdots, k\) ,求容量为 \(i\) 的背包能装的物品的最大价值和。

\(n \le 10^6\)\(k \le 5 \times 10^4\)\(c_i \le 300\)

考虑把 \(c\) 相同的物品归为一类,显然组内贪心按 \(v\) 降序选最优,记降序排序后的前缀和数组为 \(s_{i, j}\) ,其中 \(c = i\)

\(f_{i, j}\) 表示考虑了 \(c \le i\) 的物品、总体积为 \(j\) 的最大价值,转移就是 \(f_{i, j} \to f_{i + 1, j + k(i + 1)} + s_{i + 1, k}\)

考虑优化,对于 \(f_i \to f_{i + 1}\) 的转移,考虑将 \(j\)\(\bmod (i + 1)\) 的余数划分等价类,则转移只会在每个等价类内部转移,此时物品体积均可以等价于 \(1\) ,问题转化为求 \(g_j = \min_{k \le j} f_k + s_{i, j - k}\)

由于 \(s_i\) 具有凸性,而 \(f\) 不一定,因此决策点具有在普通函数上具有单调性,可以用整体二分求解做到 \(O(ck \log k)\)

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

vector<ll> vec[M];

ll F[N], s[N], f[N], g[N];

int n, k;

void solve(int l, int r, int L, int R) {
    if (L > R)
        return;

    int mid = (L + R) >> 1, p = l;
    f[mid] = g[l] + s[mid - l];

    for (int i = l + 1; i <= min(mid, r); ++i) {
        ll res = g[i] + s[mid - i];

        if (res > f[mid])
            f[mid] = res, p = i;
    }

    solve(l, p, L, mid - 1), solve(p, r, mid + 1, R);
}

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

    for (int i = 1; i <= n; ++i) {
        int c, v;
        scanf("%d%d", &c, &v);
        vec[c].emplace_back(v);
    }

    for (int i = 1; i < M; ++i) {
        if (vec[i].empty())
            continue;

        sort(vec[i].begin(), vec[i].end(), greater<ll>());

        for (int j = 1; j <= k / i + 1; ++j)
            s[j] = s[j - 1] + (j <= vec[i].size() ? vec[i][j - 1] : 0);

        for (int j = 0; j <= min(i - 1, k); ++j) {
            int len = 0;

            for (int l = j; l <= k; l += i)
                g[++len] = F[l], f[l] = 0;

            solve(1, (k - j) / i + 1, 1, (k - j) / i + 1);

            for (int l = j; l <= k; l += i)
                F[l] = f[(l - j) / i + 1];
        }
    }

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

    return 0;
}

Knuth's Optimization

通常被用于区间合并问题,即将 \(n\) 个长度为 \(1\) 的区间合并起来,每次合并会有代价,求最优代价。

\(p_{l, r}\)\(f_{l, r}\) 的最优决策点 \(k\) ,则 Knuth's Optimization 的使用条件为: \(\forall i < j, p_{l, r - 1} \le p_{l, r} \le p_{l + 1, r}\)

若代价函数满足四边形不等式与区间包含单调性,可以证明转移方程满足 Knuth's Optimization 的使用条件。

具体实现就是枚举 \([l, r]\) 的决策时,只要枚举 \([p_{l, r - 1}, p_{l + 1, r}]\) 中的决策即可,时间复杂度 \(O(n^2)\)

P4767 [IOI2000] 邮局 加强版

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

\(n \le 3000\)\(m \le 300\)

\(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) \} \]

可以证明 \(w(l, r)\) 满足四边形不等式和区间包含单调性,于是可以决策单调性优化做到 \(O(n^2)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 3e3 + 7, M = 3e2 + 7;

int a[N], w[N][N], f[N][M], g[N][M];

int n, m;

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 l = 1; l <= n; ++l)
        for (int r = l + 1; r <= n; ++r)
            w[l][r] = w[l][r - 1] + (a[r] - a[(l + r) >> 1]);

    memset(f, inf, sizeof(f)), f[0][0] = 0;

    for (int j = 1; j <= m; ++j) {
        g[n + 1][j] = n;

        for (int i = n; i; --i)
            for (int k = g[i][j - 1]; k <= g[i + 1][j]; ++k)
                if (f[k][j - 1] + w[k + 1][i] <= f[i][j])
                    f[i][j] = f[k][j - 1] + w[k + 1][i], g[i][j] = k;
    }

    printf("%d", f[n][m]);
    return 0;
}

P5897 [IOI 2013] wombats

给定一个 \(n \times m\) 的网格图,相邻两个格子之间有边,边带边权,移动时只能向左右或下方移动。

\(q\) 次操作,操作有:

  • 修改某条边的边权,共 \(C\) 次。
  • 查询 \((1, x) \to (n, y)\) 的最短路,共 \(Q\) 次。

\(n \le 5000\)\(m \le 200\)\(C \le 500\)\(Q \le 2 \times 10^5\) ,TL = 8s,ML = 250MB

注意到 \(n, m\) 范围差别较大,考虑对 \(n\) 一维开线段树维护,线段树上每个区间 \([l, r]\) 维护矩阵 \(f_{i, j}\) 表示 \((l, i) \to (r, j)\) 的最短路,合并时做 \((\min, +)\) 矩阵乘法。预处理直接做是 \(O(n m^3 \log n)\) 的,无法通过。

考虑优化,观察矩乘的形式 \(f_{i, j} = \min \{ fl_{i, k} + fr_{k, j} \}\) ,可以发现当 \(i < j\)\(i > j\)\(k\) 均有决策单调性。记 \(p_{i, j}\) 为决策点,则 \(p_{i, j - 1} \le p_{i, j} \le p_{i + 1, j}\) ,因此可以用 Knuth's Optimization 优化到 \(O(m^2)\) ,预处理的复杂度降为 \(O(n m^2 \log n)\)

但是此时空间是 \(O(n m^2)\) 的,无法接受。考虑将连续 \(B\) 行的状态压缩到线段树的叶子上,即当 \(r - l + 1 \le B\) 的时候定义该点为叶子,每次更新叶子时暴力 \(O(B m^2)\) 处理。

时间复杂度 \(O(n m^2 \log n + C \times m^2 (B + \log \frac{n}{B}) + Q)\) ,空间复杂度 \(O(\frac{n m^2}{B})\) ,取 \(B = 15\) 即可。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 5e3 + 7, M = 2e2 + 7, B = 15;

int w1[N][M], w2[N][M];

int n, m, q;

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

    inline Matrix(bool flag = false) {
        memset(a, inf, sizeof(a));

        if (flag) {
            for (int i = 1; i <= m; ++i)
                a[i][i] = 0;
        }
    }
};

inline Matrix getmatrix(int x) {
    vector<int> s(m);

    for (int i = 1; i < m; ++i)
        s[i] = s[i - 1] + w1[x][i];

    Matrix f;

    for (int i = 1; i <= m; ++i)
        for (int j = i; j <= m; ++j)
            f.a[i][j] = f.a[j][i] = s[j - 1] - s[i - 1];

    return f;
}

inline Matrix merge(Matrix fl, int mid, Matrix fr) {
    Matrix f;
    static int g[M][M];

    for (int i = 1; i <= m; ++i)
        for (int j = 1; j <= m; ++j)
            if (fl.a[i][j] + w2[mid][j] + fr.a[j][i] < f.a[i][i])
                f.a[i][i] = fl.a[i][j] + w2[mid][j] + fr.a[j][i], g[i][i] = j;

    for (int len = 2; len <= m; ++len)
        for (int i = 1, j = len; j <= m; ++i, ++j) {
            for (int k = g[i][j - 1]; k <= g[i + 1][j]; ++k)
                if (fl.a[i][k] + w2[mid][k] + fr.a[k][j] < f.a[i][j])
                    f.a[i][j] = fl.a[i][k] + w2[mid][k] + fr.a[k][j], g[i][j] = k;

            for (int k = g[j - 1][i]; k <= g[j][i + 1]; ++k)
                if (fl.a[j][k] + w2[mid][k] + fr.a[k][i] < f.a[j][i])
                    f.a[j][i] = fl.a[j][k] + w2[mid][k] + fr.a[k][i], g[j][i] = k;
        }

    return f;
}

namespace SMT {
Matrix mt[N / B << 2];

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

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

inline void pushup(int x, int l, int r) {
    int mid = (l + r) >> 1;
    mt[x] = merge(mt[ls(x)], mid, mt[rs(x)]);
}

void build(int x, int l, int r) {
    if (r - l + 1 <= B) {
        mt[x] = getmatrix(l);

        for (int i = l; i < r; ++i)
            mt[x] = merge(mt[x], i, getmatrix(i + 1));

        return;
    }

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

void update(int x, int nl, int nr, int p) {
    if (nr - nl + 1 <= B) {
        mt[x] = getmatrix(nl);

        for (int i = nl; i < nr; ++i)
            mt[x] = merge(mt[x], i, getmatrix(i + 1));

        return;
    }

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

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

    pushup(x, nl, nr);
}
} // namespace SMT

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

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j < m; ++j)
            scanf("%d", w1[i] + j);

    for (int i = 1; i < n; ++i)
        for (int j = 1; j <= m; ++j)
            scanf("%d", w2[i] + j);

    SMT::build(1, 1, n);
    scanf("%d", &q);

    while (q--) {
        int op, x, y;
        scanf("%d%d%d", &op, &x, &y);
        ++x, ++y;

        if (op == 1) {
            int w;
            scanf("%d", &w);
            w1[x][y] = w, SMT::update(1, 1, n, x);
        } else if (op == 2) {
            int w;
            scanf("%d", &w);
            w2[x][y] = w, SMT::update(1, 1, n, x);
        } else
            printf("%d\n", SMT::mt[1].a[x][y]);
    }

    return 0;
}

斜率优化

对于形如 \(f_i = \min \{ a_i \times b_j + c_j + d_i \}\) 的 DP 方程,将其写作:

\[-a_i \times b_j + f_i - d_i = c_j \]

\((b_j, c_j)\) 看作一个点,每次就是用斜率为 \(-a_i\) 的直线去切所有的决策判断哪个更优,即最小化截距 \(f_i - d_i\) 。显然在该情形下(取 \(\min\) ),最优决策点一定在点集的下凸壳上,于是设法维护凸壳即可。

单调队列优化

使用条件:\(b\) 单增、\(a\) 单减,即加入的点的 \(x\) 坐标、查询的斜率 \(-a_i\) 均单增。

若两个决策点 \(j < k\) 满足 \(k\) 优于 \(j\) ,则:

\[\begin{align} a_i \times b_k + c_k + d_i &\le a_i \times b_j + c_j + d_i \\ -a_i &\ge \dfrac{c_k - c_j}{b_k - b_j} \end{align} \]

\(slope(j,k) = \frac{c_k - c_j}{b_k - b_j}\) ,则 \(-a_i \ge slope(j,k)\) 说明决策 \(k\) 优于 \(j\)\(k > j\) )。

考虑将决策点用单调队列存储,可以发现这个斜率很符合单调队列的性质:

  • \(slope(q_{head}, q_{head + 1}) \le -a_i\) :因为 \(q_{head}\)\(q_{head+1}\) 前加入,那么这个式子就表示 \(q_{head}\) 的决策不如 \(q_{head+1}\) ,将队首弹出。
  • \(slope(q_{tail - 1}, q_{tail}) \ge slope(q_{tail}, i)\) :假设后面存在一个 \(a_t\) 使得 \(-a_t \ge slope(q_{tail-1},q_{tail})\) ,由于 \(-a_i\) 不降,等到 \(q_{tail-1}\) 弹出后,\(q_{tail}\) 也会被弹出,可以直接弹出 \(q_{tail}\)

时间复杂度 \(O(n)\) ,按照题目具体分析维护严格/非严格凸壳(是否存在共线情况)。

P3195 [HNOI2008] 玩具装箱

\(n\) 个玩具,每个玩具有个长度 \(c_i\) ,定义一段玩具 \([l, r]\) 放入一个容器的长度为 \(x = r - l + \sum_{i = l}^r c_i\) ,费用为 \((x - L)^2\) ,其中 \(L\) 为常数,最小化将所有玩具放入容器的费用和。

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

\(s_i = \sum_{j = 1}^i (c_i + 1)\) ,记 \(f_i\) 为考虑前 \(i\) 个玩具的答案,则:

\[f_i = \min \{ f_j + (s_i - s_j - 1 - L)^2 \} \]

为方便,令 \(L \gets L + 1\) ,则:

\[f_i = \min \{ -2 s_i s_j + (f_j + (s_j + L)^2) + (s_i^2 - 2 s_i L) \} \]

可以发现这个 \(s_j\) 是单增的,\(-2s_i\) 是单减的,于是直接上单调队列维护斜率优化即可做到 \(O(n)\)

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

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

int n, L;

inline ll calc(int i, int j) {
    return f[j] + (s[i] - s[j] - L) * (s[i] - s[j] - L);
}

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

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

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

    int head = 1, tail = 0;
    q[++tail] = 0;

    for (int i = 1; i <= n; ++i) {
        while (head < tail && slope(q[head], q[head + 1]) <= 2 * s[i])
            ++head;

        f[i] = calc(i, q[head]);

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

        q[++tail] = i;
    }

    printf("%lld", f[n]);
    return 0;
}

P4072 [SDOI2016] 征途

给定 \(a_{1 \sim n}\) ,将其分为 \(m\) 段,最小化 \(m^2 s^2\) ,其中 \(s^2\) 表示方差。

\(n \le 3000\)

先推式子:

\[\begin{align} m^2 s^2 &= m \times \sum_{i = 1}^m (x_i - \overline{x})^2 \\ &= (m \sum_{i = 1}^m x_i^2) - (\sum_{i = 1}^m x_i)^2 \end{align} \]

于是问题转化为最小化 \(\sum_{i = 1}^m x_i^2\) ,发现 \(m\) 越大答案越小( \((a + b)^2 > a^2 + b^2\) ),猜测它是下凸的,直接上 wqs 二分,斜率优化部分不难,时间复杂度 \(O(n \log V)\)

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

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

int n, m;

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

inline void calc(int lambda) {
    int head = 1, tail = 0;
    q[++tail] = 0;

    for (int i = 1; i <= n; ++i) {
        while (head < tail && slope(q[head], q[head + 1]) <= 2 * s[i])
            ++head;

        auto calc = [&](int i, int j) {
            return f[j] + (s[i] - s[j]) * (s[i] - s[j]) - lambda;
        };

        f[i] = calc(i, q[head]), g[i] = g[q[head]] + 1;

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

        q[++tail] = i;
    }
}

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];

    int l = -1e9, r = 0, res = 0;

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

        if (g[n] >= m)
            res = mid, r = mid - 1;
        else
            l = mid + 1;
    }

    calc(res), f[n] += 1ll * res * m;
    printf("%lld", f[n] * m - s[n] * s[n]);
    return 0;
}

单调栈优化

与单调队列优化类似,不过此时决策点是不升的,因此需要用栈。

P10652 [ROI 2017] 前往大都会 (Day 1)

ROI 国有 \(n\) 个城市,以及 \(m\) 条铁路,每条铁路都是单向运行的,第 \(i\) 条铁路依次经过 \(v_{i,1},v_{i,2},\dots,v_{i,l_i+1}\) 号城市并停靠,其中 \(v_{i,j} \to v_{i,j+1}\) 的铁路长度是 \(t_{i,j}\)

如果多条铁路经过 \(u\) 号城市,那么可以在 \(u\) 号城市换乘其他铁路,每条铁路都可以在停靠点任意上下车。

找到一条从 \(1\)\(n\) 的路径,这条路径需要满足其总长度最小,并且在此条件上路径上相邻两个换乘点间火车上距离的平方和最大。

注:起点和终点都是换乘点,题目保证有解。

\(n, m \le 10^6\)

首先求出最短路图,那么只要在图上找到平方和最大的路径。

由于最短路图是DAG,于是可以按拓扑序DP。令 \(f_x\) 为以 \(x\) 为终点的最大权值,枚举上一个换乘点 \(y\),则:

\[f_x = \max (f_y + (d_x - d_y)^2) = d_x^2 + \max(-2d_y \times d_x + d_y^2 + f_y) \]

\(w(i, j) = (d_i - d_j)^2\) ,则:

\[w(i, j) + w(i + 1, j + 1) - w(i + 1, j) - w(i, j + 1) = -2(d_{i + 1} - d_i)(d_{j + 1} - d_j) \le 0 \]

此时四边形不等式是反过来的,因此决策单调性也是反过来的,因此决策点单调不升。

单调栈优化即可,时间复杂度 \(O(m \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e6 + 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;

vector<int> que[N << 1];
vector<pair<int, int> > belong[N];
vector<int> city[N], tim[N], id[N];

ll f[N];
int s[N], dis[N];

int n, m;

inline void Dijkstra(int S) {
    memset(dis + 1, inf, sizeof(int) * n);
    priority_queue<pair<int, int> > q;
    dis[S] = 0, q.emplace(-dis[S], S);

    while (!q.empty()) {
        auto c = q.top();
        q.pop();

        if (-c.first != dis[c.second])
            continue;

        int u = c.second;

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

            if (dis[v] > dis[u] + w)
                dis[v] = dis[u] + w, q.emplace(-dis[v], v);
        }
    }
}

inline void prework() {
    int tot = 0;

    for (int i = 1; i <= m; ++i) {
        id[i].resize(s[i] + 1), id[i][0] = ++tot;

        for (int j = 1; j <= s[i]; ++j) {
            int u = city[i][j - 1], v = city[i][j], w = tim[i][j - 1];

            if (dis[v] < dis[u] + w)
                ++tot;
            
            id[i][j] = tot;
        }
    }
}

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

    for (int i = 1; i <= m; ++i) {
        int u;
        scanf("%d%d", s + i, &u);
        city[i].emplace_back(u), belong[u].emplace_back(i, 0);

        for (int j = 1; j <= s[i]; ++j) {
            int w, v;
            scanf("%d%d", &w, &v);
            city[i].emplace_back(v), tim[i].emplace_back(w);
            belong[v].emplace_back(i, j), G.insert(u, v, w), u = v;
        }
    }

    Dijkstra(1), prework();
    vector<int> p(n - 1);
    iota(p.begin(), p.end(), 2);

    sort(p.begin(), p.end(), [](const int &x, const int &y) {
        return dis[x] < dis[y];
    });

    for (auto it : belong[1])
        que[id[it.first][it.second]].emplace_back(1);

    for (int x : p) {
        if (dis[x] == inf)
            break;

        for (auto it : belong[x]) {
            auto &q = que[id[it.first][it.second]];
            
            if (q.empty())
                continue;

            auto calc = [](int x, int d) {
                return -2ll * d * dis[x] + 1ll * dis[x] * dis[x] + f[x];
            };
            
            while (q.size() >= 2 && calc(q[q.size() - 2], dis[x]) >= calc(q[q.size() - 1], dis[x]))
                q.pop_back();
            
            f[x] = max(f[x], calc(q[q.size() - 1], dis[x]) + 1ll * dis[x] * dis[x]);
        }

        auto check = [](int a, int b, int c) {
            ll ka = -2ll * dis[a], kb = -2ll * dis[b], kc = -2ll * dis[c];
            ll ta = 1ll * dis[a] * dis[a] + f[a];
            ll tb = 1ll * dis[b] * dis[b] + f[b];
            ll tc = 1ll * dis[c] * dis[c] + f[c];
            return (__int128)(tc - ta) * (ka - kb) >= (__int128)(tb - ta) * (ka - kc);
        };
        
        for (auto it : belong[x]) {
            auto &q = que[id[it.first][it.second]];
                            
            while (q.size() >= 2 && check(q[q.size() - 2], q[q.size() - 1], x))
                q.pop_back();
                
            q.emplace_back(x);
        }
    }

    printf("%d %lld", dis[n], f[n]);
    return 0;
}

二分队列优化

使用条件:加入点的 \(x\) 坐标 \(b\) 单增。

用单调队列维护凸壳,询问时在凸壳上二分斜率 \(-a_i\) 即可。

P5785 [SDOI2012] 任务安排

\(n\) 个任务按顺序分批执行,每批任务开始需要一个固定的启动时间 \(S\) 。第 \(i\) 个任务花费的时间是 \(t_i\) ,每个任务的花费是它完成的时刻乘上它自身的费用系数 \(c_i\)。需要找到一个最佳的分批顺序使得总费用最小。

\(n \le 3 \times 10^5\)\(1 \le s \le 2^8\)\(|T_i| \le 2^8\)\(0 \le c_i \le 2^8\)

\(T_i, C_i\) 为前缀和数组,设 \(f_i\) 表示把前 \(i\) 个任务分成若干个组的最小花费,有转移方程:

\[f_i = \min \{ f_j + T_i \times (C_i - C_j) + S \times (C_n - C_j) \} \]

把这个式子整理一下:

\[f_i = \min \{ -T_i \times C_j + (f_j - S \times C_j) + (T_i \times C_i + S \times C_n) \} \]

可以发现加入的点的 \(x\) 坐标单增,于是用二分队列优化即可做到 \(O(n \log n)\) ,需要注意避免小数运算丢精度。

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

ll f[N];
int t[N], c[N], q[N];

int n, s;

inline int X(int i) {
    return c[i];
}

inline ll Y(int i) {
    return f[i] - 1ll * s * c[i];
}

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

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

    for (int i = 1; i <= n; ++i) {
        scanf("%d%d", t + i, c + i);
        t[i] += t[i - 1], c[i] += c[i - 1];
    }

    int head = 1, tail = 0;
    q[++tail] = 0;

    for (int i = 1; i <= n; ++i) {
        auto search = [&](int k) {
            int l = head, r = tail - 1, pos = tail;

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

                if (Y(q[mid + 1]) - Y(q[mid]) >= 1ll * k * (X(q[mid + 1]) - X(q[mid])))
                    pos = mid, r = mid - 1;
                else
                    l = mid + 1;
            }

            return q[pos];
        };

        auto calc = [](int i, int j) {
            return f[j] + 1ll * t[i] * (c[i] - c[j]) + 1ll * s * (c[n] - c[j]);
        };

        f[i] = calc(i, search(t[i]));

        auto cmp = [](int i, int j, int k) {
            return (Y(j) - Y(i)) * (X(k) - X(j)) >= (Y(k) - Y(j)) * (X(j) - X(i));
        };

        while (head < tail && cmp(q[tail - 1], q[tail], i))
            --tail;

        q[++tail] = i;
    }

    printf("%lld", f[n]);
    return 0;
}

cdq分治优化

设函数 cdq(l, r) 表示求解 \(f_{l \sim r}\) ,返回 \(l \sim r\) 点的凸壳。

先递归处理 \([l, mid]\)\([mid + 1, r]\) 。对于当前层,枚举 \([mid + 1, r]\) 的点,计算 \([l, mid]\) 点的凸壳所产生的贡献。计算完答案后将左右两部分凸壳归并返回。

可以做到 \(O(n \log n)\) 的复杂度。

P4027 [NOI2007] 货币兑换

\(n\) 天,初始有 \(S\) 元。每天可以:

  • 花一些现金买入股票,其中第 \(i\) 天股票中 A 股和 B 股的数量比为 \(r_i\) ,即只能按 \(r_i\) 的比值买入股票。
  • 或按相同比例卖出 A 股和 B 股,并按当天的价值获得现金,其中第 \(i\) 天 A 股价值 \(a_i\) 元、B 股价值 \(b_i\) 元。
  • 或什么也不干。

同一天内可以进行多次操作,求 \(n\) 天后能够获得的最大价值。

\(n \le 10^5\)

可以发现必然存在一种最优的买卖方案满足:每次买进操作使用完所有的人民币,每次卖出操作卖出所有的金券。

设第 \(i\) 天最大收益为 \(f_i\) ,并设 \(f_i\) 可以换为两种股票数量分别为 \(x_i = f_i \frac{r_i}{a_i r_i + b_i}, y_i = f_i \frac{1}{a_i r_i + b_i}\)

枚举上一次买入的时间 \(j\) ,可以得到转移方程:

\[f_i = \max(f_{i - 1}, \max_{j < i} \{ x_j a_i + y_j b_i \}) \]

前一项是好处理的,考虑后一项,设转移到 \(f_i\)\(j\) 优于 \(k\) ,则:

\[x_j a_i + y_j b_i > x_k a_i + y_k b_i \\ \dfrac{y_j - y_k}{x_j - x_k} > -\dfrac{a_i}{b_i} \]

很明显 \(x, y\) 都是没有单调性的,无法直接用单调栈或队列建凸壳。

考虑按 \(-\frac{a_i}{b_i}\) cdq 分治,分治时对前一块的 \(x\) 排序,就可以建出凸壳优化转移,时间复杂度 \(O(n \log n)\)

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

struct Node {
    double x, y, k;
    int id;
} nd[N], tmp[N];

double a[N], b[N], rate[N], f[N];
int sta[N];

double S;
int n;

inline double slope(int a, int b) {
    return nd[a].x == nd[b].x ? inf : (nd[b].y - nd[a].y) / (nd[b].x - nd[a].x);
}

void cdq(int l, int r) {
    if (l == r) {
        f[l] = max(f[l], f[l - 1]);
        nd[l].x = f[l] * rate[l] / (a[l] * rate[l] + b[l]);
        nd[l].y = f[l] / (a[l] * rate[l] + b[l]);
        return;
    }

    int mid = (l + r) >> 1, lp = l, rp = mid + 1;

    for (int i = l; i <= r; ++i) {
        if (nd[i].id <= mid)
            tmp[lp++] = nd[i];
        else
            tmp[rp++] = nd[i];
    }

    memcpy(nd + l, tmp + l, sizeof(Node) * (r - l + 1));
    cdq(l, mid);
    int top = 0;

    for (int i = l; i <= mid; ++i) {
        while (top > 1 && slope(sta[top], i) > slope(sta[top - 1], sta[top]))
            --top;

        sta[++top] = i;
    }

    for (int i = mid + 1, j = 1; i <= r; ++i) {
        while (j < top && slope(sta[j], sta[j + 1]) > nd[i].k)
            ++j;

        f[nd[i].id] = max(f[nd[i].id], nd[sta[j]].x * a[nd[i].id] + nd[sta[j]].y * b[nd[i].id]);
    }

    cdq(mid + 1, r);
    inplace_merge(nd + l, nd + mid + 1, nd + r + 1, [](const Node &a, const Node &b) { return a.x < b.x; });
}

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

    for (int i = 1; i <= n; ++i) {
        scanf("%lf%lf%lf", a + i, b + i, rate + i);
        nd[i].x = S * rate[i] / (a[i] * rate[i] + b[i]);
        nd[i].y = S / (a[i] * rate[i] + b[i]);
        nd[i].k = -a[i] / b[i];
        nd[i].id = i, f[i] = S;
    }

    sort(nd + 1, nd + n + 1, [](const Node &a, const Node &b) { return a.k > b.k; });
    cdq(1, n);
    printf("%.3lf", f[n]);
    return 0;
}

李超树优化

这是一个很无脑的做法,直接用李超树维护一次函数(直线),时间复杂度可以做到 \(O(n \log n)\)

P4655 [CEOI2017] Building Bridges

\(n\) 个柱子,高度为 \(h_{1 \sim n}\) 。若一座桥连接了 \(i, j\) ,付出 \((h_i - h_j)^2\) 的代价。未被桥连接的柱子将会被拆除,付出 \(w_i\) 的代价。求通过桥将 \(1, n\) 两根柱子连接的最小代价,桥不能在端点以外的任何地方相交。

\(n \le 10^5\)

首先设 \(s_i\) 表示 \(w\) 的前缀和,\(f_i\) 表示联通 \(1, i\) 的代价,则:

\[f_i = h_i^2 + s_{i - 1} + \min_{j = 1}^{i - 1} \{ -2h_ih_j + f_j + h_j^2 - s_j \} \]

用李超树维护直线 \(y = -2h_j x + (f_j + h_j^2 - s_j)\) ,每次查询 \(x = h_i\)\(y\) 的最小值,时间复杂度 \(O(n \log n)\)

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

struct Line {
    ll k, b;
    
    inline ll operator () (const int x) {
        return k * x + b;
    }
};

ll s[N], f[N];
int h[N], w[N];

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 SMT {
Line s[V << 2];

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

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

void maintain(int x, int nl, int nr, Line k) {
    int mid = (nl + nr) >> 1;
    
    if (k(mid) < s[x](mid))
        swap(k, s[x]);
    
    if (nl == nr)
        return;
    
    if (k(nl) < s[x](nl))
        maintain(ls(x), nl, mid, k);
    
    if (k(nr) < s[x](nr))
        maintain(rs(x), mid + 1, nr, k);
}

ll query(int x, int nl, int nr, int pos) {
    if (nl == nr)
        return s[x](pos);
    
    int mid = (nl + nr) >> 1;
    
    if (pos <= mid)
        return min(s[x](pos), query(ls(x), nl, mid, pos));
    else
        return min(s[x](pos), query(rs(x), mid + 1, nr, pos));
}
} // namespace SMT

signed main() {
    n = read();
    
    for (int i = 1; i <= n; ++i)
        h[i] = read();
    
    for (int i = 1; i <= n; ++i)
        s[i] = s[i - 1] + (w[i] = read());
    
    int maxh = *max_element(h + 1, h + 1 + n);
    fill(SMT::s + 1, SMT::s + 1 + 4 * maxh, (Line) {0, inf});
    SMT::maintain(1, 1, maxh, (Line) {-2ll * h[1], f[1] + 1ll * h[1] * h[1] - s[1]});
    
    for (int i = 2; i <= n; ++i) {
        f[i] = 1ll * h[i] * h[i] + s[i - 1] + SMT::query(1, 1, maxh, h[i]);
        SMT::maintain(1, 1, maxh, (Line) {-2ll * h[i], f[i] + 1ll * h[i] * h[i] - s[i]});
    }
    
    printf("%lld", f[n]);
    return 0;
}

应用

P1973 [NOI2011] NOI 嘉年华

给出 \(n\) 个区间,将它们分为两份,满足两份区间不交,可以丢弃区间。

第一问:求两份区间数量较小者的最大值。

第二问:对于所有 \(i \in [1, n]\) ,求强制选取第 \(i\) 个区间时的第一问。

\(n \le 200\)

首先将值域离散化为 \([1, m]\) 。设 \(cnt(l, r)\) 表示被 \([l, r]\) 完全包含的区间个数,不难 \(O(n^3)\) 预处理得到。

再设 \(f_{i, j}\) 表示值域以 \(i\) 结尾的前缀选 \(j\) 个区间到第一份,此时第二份最多能选区间的数量。不难得到转移方程:

\[f_{i, j} = \max_{k = 1}^{i - 1} \{ f_{k, j - cnt(k + 1, i)}, f_{k, j} + cnt(k + 1, i) \} \]

答案即为:

\[\max_{i = 0}^n \{ \min(f_{m, i}, i) \} \]

于是第一问可以 \(O(n^3)\) 解决。

对于第二问,再设 \(g_{i, j}\) 表示值域以 \(i\) 开始的后缀选择 \(j\) 个区间到第一份,此时第二份最多能选区间的数量,转移与 \(f\) 类似。

由对称性,钦定强制选取的区间 \([l, r]\) 给第一份,于是答案为:

\[\max_{i = 1}^{l - 1} \max_{j = r + 1}^m \max_{k = 0}^n \max_{t = 0}^n \min(k + t + cnt(i + 1, j - 1), f_{i, k} + g_{j, t}) \]

直接做是 \(O(n^4)\) 的,考虑优化。设:

\[h(l, r) = \max_{i = 0}^n \max_{j = 0}^n \min(i + j + cnt(l + 1, r - 1), f_{l, i} + g_{r, j}) \]

则答案即为 \(\max_{i = 1}^{l - 1} \max_{j = r + 1}^m h(l, r)\) ,这一部分可以做到 \(O(n^2)\)

接下来考虑如何计算 \(h(l, r)\) 。可以发现 \(\min\) 里面的东西是两份的区间数,由对称性,令 \(k + t + cnt(l + 1, r - 1)\) 为较大值,则需要最大化 \(f_{l, i} + g_{r, j}\) 。注意到 \(f_{l, i}\)\(i\) 的增大而减小,\(g_{r, j}\)\(j\) 的增大而减小。那么当 \(i\) 增加时再增加 \(j\) 显然不优,因此可以单纯增加左边、减少右边。这一部分时间复杂度降为 \(O(n^3)\)

总时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e2 + 7, M = 4e2 + 7;

struct Interval {
    int l, r;
} a[N];

int cnt[M][M], f[M][N], g[M][N], h[M][M];

int n, m;

signed main() {
    scanf("%d", &n);
    vector<int> vec = {-inf, inf};

    for (int i = 1; i <= n; ++i) {
        scanf("%d%d", &a[i].l, &a[i].r);
        a[i].r += a[i].l - 1;
        vec.emplace_back(a[i].l), vec.emplace_back(a[i].r);
    }

    sort(vec.begin(), vec.end());
    vec.erase(unique(vec.begin(), vec.end()), vec.end());

    for (int i = 1; i <= n; ++i) {
        a[i].l = lower_bound(vec.begin(), vec.end(), a[i].l) - vec.begin() + 1;
        a[i].r = lower_bound(vec.begin(), vec.end(), a[i].r) - vec.begin() + 1;
    }
    
    m = vec.size();

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= a[i].l; ++j)
            for (int k = a[i].r; k <= m; ++k)
                ++cnt[j][k];

    memset(f[0] + 1, -inf, sizeof(int) * n);

    for (int i = 1; i <= m; ++i)
        for (int j = 0; j <= n; ++j) {
            f[i][j] = -inf;

            for (int k = 0; k < i; ++k) {
                if (f[k][j] != -inf)
                    f[i][j] = max(f[i][j], f[k][j] + cnt[k + 1][i]);

                if (f[k][max(j - cnt[k + 1][i], 0)] != -inf)
                    f[i][j] = max(f[i][j], f[k][max(j - cnt[k + 1][i], 0)]);
            }
        }

    int ans = 0;

    for (int i = 0; i <= n; ++i)
        ans = max(ans, min(f[m][i], i));

    printf("%d\n", ans);
    memset(g[m + 1] + 1, -inf, sizeof(int) * n);

    for (int i = m; i; --i)
        for (int j = 0; j <= n; ++j) {
            g[i][j] = -inf;

            for (int k = i + 1; k <= m + 1; ++k) {
                if (g[k][j] != -inf)
                    g[i][j] = max(g[i][j], g[k][j] + cnt[i][k - 1]);

                if (g[k][max(j - cnt[i][k - 1], 0)] != -inf)
                    g[i][j] = max(g[i][j], g[k][max(j - cnt[i][k - 1], 0)]);
            }
        }

    for (int i = 1; i <= m; ++i)
        for (int j = i + 2; j <= m; ++j) {
            h[i][j] = -inf;

            for (int k = 0, t = n; k <= n && f[i][k] != -inf; ++k) {
                while (~t && g[j][t] != -inf) {
                    if (min(f[i][k] + g[j][t], k + t + cnt[i + 1][j - 1]) >= h[i][j])
                        h[i][j] = min(f[i][k] + g[j][t], k + t + cnt[i + 1][j - 1]);
                    else
                        break;

                    --t;
                }

                ++t;
            }
        }

    for (int i = 1; i <= n; ++i) {
        int ans = 0;

        for (int j = 1; j < a[i].l; ++j)
            for (int k = a[i].r + 1; k <= m; ++k)
                ans = max(ans, h[j][k]);

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

    return 0;
}
posted @ 2024-08-08 19:15  wshcl  阅读(105)  评论(0)    收藏  举报