练习 决策单调性
2025-07-15

『心静自然凉』大抵是因为情绪平和时副交感神经兴奋,体温略有降低导致的。吗?

其实是因为心脏停跳后血液循环终止、代谢中断,导致产热低于散热(?)


A - 征途

https://www.luogu.com.cn/problem/P4072

用完全平方公式展开得到 \(m^2\sigma^2=m\left(\sum {x_i}^2\right)-S^2\),其中 \(S\) 为求和。

所以目标是最小化 \(\sum {x_i}^2\) 这个东西。令 \(f_{i,j}\) 表示第 \(i\) 天走到 \(j\),得到:

\[ \begin{aligned} f_{i,j}&=\min\{f_{i-1,k}+(s_j-s_k)^2\}\\ &=\min\{f_{i-1,k}-2\times s_j\times s_k+{s_k}^2\}+{s_j}^2 \end{aligned} \]

最后得到的斜率式子是 \(\dfrac {f_{i - 1, a}-f_{i - 1, b}+{s_a}^2-{s_b}^2}{2(s_a-s_b)}<s_j\),由于 \(s_j\) 单增,单调队列维护递减斜率即可 更正:是递增斜率。原因是 \(<\) 是弹出条件,而非保留条件……

#include <bits/stdc++.h>
const long long inf = 1e9;
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n, m;
    std::cin >> n >> m;
    std::vector<int> a(n + 1);
    std::vector<long long> s(n + 1);
    auto sum(0ll);
    for (int i = 1; i <= n; ++i) {
        std::cin >> a[i];
        s[i] = s[i - 1] + a[i];
        sum += a[i];
    }
    std::vector<std::vector<long long> > f(m + 1, std::vector<long long> (n + 1, inf));
    f[0][0] = 0ll;
    for (int i = 1; i <= m; ++i) {
        auto f1 = [&](int a, int b) {
            return f[i - 1][a] - f[i - 1][b] + s[a] * s[a] - s[b] * s[b];
        };
        auto f2 = [&](int a, int b) {
            return 2 * (s[a] - s[b]);
        };
        std::vector<int> q(n + 1);
        int h = 0, t = -1;
        q[++t] = i - 1;
        for (int j = i; j <= n; ++j) {
            for (; h < t && f1(q[h + 1], q[h]) < s[j] * f2(q[h + 1], q[h]); ++h);
            f[i][j] = f[i - 1][q[h]] + (s[j] - s[q[h]]) * (s[j] - s[q[h]]);
            for (; h < t && f1(j, q[t]) * f2(q[t], q[t - 1]) < f1(q[t], q[t - 1]) * f2(j, q[t]); --t);
            q[++t] = j;
        }
    }
    std::cout << m * f[m][n] - sum * sum << '\n';
    return 0;
}

B - 刷野 III

https://www.luogu.com.cn/problem/P10074

发现最坏情况就是每次『试探』都不中的情况,再试探中最坏的那一个。为啥呢,相当于我们每次攻击的一定是未知元素中血最多的一个。既然已经试探出了比阈值大的所有元素,那么下一个攻击的就一定是阈值本身,如果这次跳过它,它就会成为下一次试探失败的元素。这显然不如一开始就直接用新阈值试探。

从大到小排序。令 \(f_{j, i}\) 表示只确定了前 \(i\) 大的数,已经杀了 \(j\) 个人的最坏情况最小代价。那么显然这一次的阈值是 \(a_i\)。随便选出上一次的阈值 \(a_k\),那么中间这一段待确定的元素数量为 \(i-k\)。那么有:

\[ \begin{aligned} f_{j, i}&=\min\limits_{k<i}\{f_{j-1,k}+(i-k)\times a_i\}\\ &=\min\limits_{k<i}\{f_{j-1,k}-k\times a_i\}+i\times a_i \end{aligned} \]

经过验证,虽然这个式子和题解长得不一样,但是是对的 因为我 n^3 暴力 A 了

推出斜优形式 \(\dfrac {f_{j-1,A}-f_{j-1,B}}{A-B}<a_i\),但我的朋友,\(a_i\) 是递减的。所以用单调栈维护递增斜率即可。或者你也可以学习 grisses 打一个单调队列上二分

#include <bits/stdc++.h>
const long long inf = 1e12;
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen("P10074_4.in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n, m;
    std::cin >> n >> m;
    std::vector<long long> a(n + 1);
    for (int i = 1; i <= n; ++i)
        std::cin >> a[i];
    std::sort(a.begin() + 1, a.end(), std::greater<long long> ());
    std::vector<std::vector<long long> > f(m + 1, std::vector<long long> (n + 1, inf));
    f[0][0] = 0ll;
    for (int j = 1; j <= m; ++j) {
        std::vector<int> q(n + 1);
        int t = -1;
        q[++t] = j - 1;
        auto f1 = [&](int A, int B) {
            return f[j - 1][A] - f[j - 1][B];
        };
        auto f2 =[&](int A, int B) {
            return A - B;
        };
        for (int i = j; i <= n; ++i) {
            for (; t >= 1 && f1(q[t], q[t - 1]) > a[i] * f2(q[t], q[t - 1]); --t);
            f[j][i] = f[j - 1][q[t]] + (i - q[t]) * a[i];
            for (; t >= 1 && f1(i, q[t]) * f2(q[t], q[t - 1]) < f1(q[t], q[t - 1]) * f2(i, q[t]); --t);
            q[++t] = i;
        }
    }
    auto res(inf);
    for (int i = m; i <= n; ++i)
        res = std::min(res, f[m][i]);
    std::cout << res << '\n';
    return 0;
}

C - TRAKA

https://www.luogu.com.cn/problem/P7747

稍微手玩一下就可以发现,假如第 \(j\) 个人在第 \(i\) 次的工作时间为 \([L_{j,i},R_{j,i}]\),第 \(i-1\) 次为 \([L_{j,i-1},R_{j,i-1}]\),那么要求 \(L_{j,i}\ge R_{j,i-1}\)

\(s_j\)\(t\) 的前缀和。假设第 \(i-1\) 次加工于 \(x_{i-1}\) 开始,那么我们可以把上式转写为 \(x_{i}+s_{j-1}\times f_{i}\ge x_{i-1}+s_j\times f_{i-1}\)。也即 \(x_i-x_{i-1}\ge s_j\times f_{i-1}-s_{j-1}\times f_i\)

显然需要找到一个 \(j\) 使得 RSH 取得最大值;现在就可以考虑斜率优化了。由于所有项都和 \(i\) 有关,想到两边同除 \(f_{i}\) 消掉一个 \(i\) 有关的系数,最后化出来的斜优形式是 \(\dfrac {s_A-s_B}{s_{A-1}-s_{B-1}}>\dfrac {f_{i-1}}{f_i}\)。由于 RSH 不单调,把所有 \(j\) 塞到队列里维护递减斜率,打二分即可。

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n, m;
    std::cin >> n >> m;
    std::vector<long long> a(n + 1), w(m + 1), s(n + 1);
    for (int i = 1; i <= n; ++i)
        std::cin >> a[i], s[i] = s[i - 1] + a[i];
    for (int i = 1; i <= m; ++i)
        std::cin >> w[i];
    std::vector<long long> f(m + 1);
    std::vector<int> q(n + 1);
    int h = 0, t = -1;
    q[++t] = 1;
    for (int i = 2; i <= n; ++i) {
        for (; h < t && (s[i] - s[q[t]]) * (s[q[t] - 1] - s[q[t - 1] - 1]) > (s[q[t]] - s[q[t - 1]]) * (s[i - 1] - s[q[t] - 1]); --t);
        q[++t] = i;
    }
    for (int i = 2; i <= m; ++i) {
        int to = q[h];
        for (int l = h + 1, r = t, mid; l <= r; ) {
            mid = (l + r) >> 1;
            if ((s[q[mid]] - s[q[mid - 1]]) * w[i - 1] > w[i] * (s[q[mid] - 1] - s[q[mid - 1] - 1]))
                to = q[mid], l = mid + 1;
            else
                r = mid - 1;
        }
        f[i] = f[i - 1] + s[to] * w[i - 1] - s[to - 1] * w[i];
    }
    std::cout << f[m] + w[m] * s[n] << '\n';
    return 0;
}

D - 柠檬

https://www.luogu.com.cn/problem/P5504

  • 观察零:原问题『从两端取』可以转化为分段问题,故从其中一端考虑即可。
  • 观察一:若有一段连续的 \(x\),完整的比拆开的更优。
  • 观察二:如果一段 \(x\) 中夹杂了一些其他元素,那么在哪里分段是说不准的。
  • 观察三:如果选取的区间是 \([1,r]\),那么贪心地想,\(a_r\) 一定是关键值,不然取 \(a_r\) 就浪费了。
  • 观察四:如果选取的区间是 \([l,r]\),那么由观察四,\(a_l=a_r\),且该值为关键值。

结合这几个观察,令 \(c_i\) 表示 \(a_i\)\([1,i]\) 中出现次数,\(f_i\) 表示这一段以 \(i\) 结尾的最大价值:

\[ \begin{aligned} f_i&=\max\limits_{j<i,a_{j+1}=a_i}\{f_j+a_i\times (c_i-c_{j + 1} + 1)^2\}\\ &=\max\limits_{j<i,a_{j+1}=a_i}\{f_j+a_{j+1}\times {c_{j+1}}^2-2\times c_i\times a_{j+1}\times c_{j+1}-2\times c_{j+1}\times a_{j+1}\}+a_i\times(c_i-1)^2 \end{aligned} \]

(怎么这么大一堆。)最后可以化出 \(\dfrac {f_A-f_B+a_{A+1}\cdot c_{A+1}\cdot (c_{A+1}-2)-a_{B+1}\cdot c_{B+1}\cdot (c_{B+1}-2)}{2(c_{A+1} - c_{B+1})}>c_i\times a_i\)。发现对于每一种 \(a_i\)\(c_i\times a_i\) 是单增的。单调栈维护即可。

这其实提醒我们关于代换的问题——显然,当与 \(i\) 的项、与 \(j\) 有关的项之间存在代换关系时,应该尽量往 \(j\) 的方向靠。

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen("7.in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n;
    std::cin >> n;
    std::vector<long long> a(n + 1), c(n + 1), la(10001);
    for (int i = 1; i <= n; ++i) {
        std::cin >> a[i];
        c[i] = c[la[a[i]]] + 1, la[a[i]] = i;
    }
    std::vector<long long> f(n + 1);
    std::vector<int> _t(10001, -1);
    std::vector<std::vector<int> > _q(10001);
    auto f1 = [&](int A, int B) {
        return f[A] - f[B] + a[A + 1] * c[A + 1] * (c[A + 1] - 2) - a[B + 1] * c[B + 1] * (c[B + 1] - 2);
    };
    auto f2 = [&](int A, int B) {
        return 2 * (c[A + 1] - c[B + 1]);
    };
    ++_t[a[1]], _q[a[1]].push_back(0);
    for (int i = 1; i <= n; ++i) {
        {
            auto &t = _t[a[i]];
            auto &q = _q[a[i]];
            for (; t >= 1 && f1(q[t], q[t - 1]) < c[i] * a[i] * f2(q[t], q[t - 1]); --t);
            f[i] = f[q[t]] + a[i] * (c[i] - c[q[t] + 1] + 1) * (c[i] - c[q[t] + 1] + 1);
        }
        if (i < n) {
            auto &t = _t[a[i + 1]];
            auto &q = _q[a[i + 1]];
            for (; t >= 1 && f1(i, q[t]) * f2(q[t], q[t - 1]) > f1(q[t], q[t - 1]) * f2(i, q[t]); --t);
            q.resize(++t + 1), q[t] = i;
        }
    }
    std::cout << f[n] << '\n';
    return 0;
}

E - Knapsack with Diminishing Values

https://atcoder.jp/contests/abc373/tasks/abc373_f

发现和 单调队列优化多重背包 有异曲同工之妙。

不妨令 \(v_i\) 表示体积,\(w_i\) 表示价值。对于每一个 \(i\),把所有体积按模 \(v_i\) 的余数分类,设为 \(j\cdot v_i+x\)。对于 \(k\cdot v_i+x\),有:

\[ \begin{aligned} f_{i,j\cdot v_i+x}&=\max\limits_{k<j}\{f_{i-1,k\cdot v_i+x}+(j-k)\cdot w_i-(j-k)^2\}\\ &=\max\limits_{k<j}\{f_{i-1,k\cdot v_i+x}-k\cdot w_i-k^2+2\times j\times k\}-j^2+j\cdot w_i \end{aligned} \]

则得到 \(\dfrac {f_{i-1,A\cdot v_i+x}-f_{i-1,B\cdot v_i+x}+(B-A)\cdot w_i - A^2+B^2}{2(B-A)}<j\)。注意分母为负。总之单调队列维护递增斜率即可。

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n, m;
    std::cin >> n >> m;
    std::vector<long long> v(n + 1), w(n + 1);
    std::vector<std::vector<long long> > f(n + 1, std::vector<long long> (m + 1));
    for (int i = 1; i <= n; ++i) {
        std::cin >> v[i] >> w[i];
        for (int x = 0; x < v[i]; ++x) {
            int h = 0, t = -1;
            std::vector<int> q;
            auto f1 = [&](int A, int B) {
                return f[i - 1][A * v[i] + x] - f[i - 1][B * v[i] + x] + (B - A) * w[i] - A * A + B * B;
            };
            auto f2 = [&](int A, int B) {
                return 2 * (B - A);
            };
            for (int j = 0, J = x; J <= m; ++j, J += v[i]) {
                for (; h < t && f1(q[h + 1], q[h]) > j * f2(q[h + 1], q[h]); ++h);
                f[i][J] = f[i - 1][J];
                if (h <= t)
                    f[i][J] = std::max(f[i][J], f[i - 1][q[h] * v[i] + x] + (j - q[h]) * w[i] - (j - q[h]) * (j - q[h]));
                for (; h < t && f1(j, q[t]) * f2(q[t], q[t - 1]) < f1(q[t], q[t - 1]) * f2(j, q[t]); --t);
                q.resize(++t + 1), q[t] = j;
            }
        }
    }
    std::cout << f[n][m] << '\n';
    return 0;
}

F - Managing Telephone Poles

https://codeforces.com/problemset/problem/1575/M

?观察到性质然后被自己忽略了。非常值得批评。

不难写出类似斜率优化的式子 \(S(i,j)=\min\{ {x_k}^2-2\times i\times x_k+{y_k}^2-2\times j \times y_k\}+i^2+j^2\)

会下意识尝试固定 \(i\),就可以 \(O(n^2m)\) 完成任务,似乎不太行。顺着这个想法会观察到,固定 \(i\) 之后,每一列的 poles 中只有和第 \(i\) 行最近的才会有贡献。

这个是好做的,且这样的相邻点数量是 \(O(m)\) 的;于是将 \(i\) 视为常数进行变形,若将所有 poles 按 \(y\) 从小到大排序就能得到斜率形式 \(\dfrac { {x_A}^2-{x_B}^2+{y_A}^2-{y_B}^2-2\times i\times(x_A-x_B)}{2(y_A-y_B)}<j\)。维护递增斜率就能 \(O(n^2)\) 完成问题。

那么找相邻点这一步大可以摆烂写二分。所以总共是 \(O(nm\log m)\) 的。

不要像我一样把两边最近的都加进队列,不然你会有分母为 \(0\) 的斜率

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n, m;
    std::cin >> n >> m, ++n, ++m;
    std::vector<std::vector<int> > tag(n + 1, std::vector<int> (m + 1));
    std::vector<std::vector<int> > g(m + 1);
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j) {
            char t;
            std::cin >> t;
            if (t == '1')
                g[j].push_back(i), tag[i][j] = 1;
        }
    struct node { long long x, y; };
    auto res(0ll);
    for (int i = 1; i <= n; ++i) {
        std::vector<node> p;
        for (int j = 1; j <= m; ++j) {
            int to = std::lower_bound(g[j].begin(), g[j].end(), i) - g[j].begin();
            if (to < (int)g[j].size()) {
                p.push_back({ g[j][to], j });
                if (g[j][to] != i && to != 0 && g[j][to] - i > i - g[j][to - 1])
                    p.back() = { g[j][to - 1], j };
            }
            else if (to != 0)
                p.push_back({ g[j][to - 1], j });
        }
        std::vector<int> q(m + 1);
        int h = 0, t = -1;
        auto f1 = [&](node A, node B) {
            return A.x * A.x - B.x * B.x + A.y * A.y - B.y * B.y - 2 * i * (A.x - B.x);
        };
        auto f2 = [&](node A, node B) {
            return 2 * (A.y - B.y);
        };
        for (int j = 0; j < (int)p.size(); ++j) {
            for (; h < t && f1(p[j], p[q[t]]) * f2(p[q[t]], p[q[t - 1]]) < f1(p[q[t]], p[q[t - 1]]) * f2(p[j], p[q[t]]); --t);
            q[++t] = j;
        }
        for (int j = 1; j <= m; ++j) {  
            for (; h < t && f1(p[q[h + 1]], p[q[h]]) < j * f2(p[q[h + 1]], p[q[h]]); ++h);
            res += (p[q[h]].x - i) * (p[q[h]].x - i) + (p[q[h]].y - j) * (p[q[h]].y - j);
        }
    }
    std::cout << res << '\n';
    return 0;
}

G - Partition Game

https://codeforces.com/problemset/problem/1527/E

发现不太斜优,终于给我浸泡了两天斜优内容的大脑加了勺新的底物。

\(f_{i,j}\) 表示第 \(i\) 段以 \(j\) 结尾的最小代价;对 \(w\) 套用四边形不等式变式 \(w(l-1,r+1)+w(l,r)\ge w(l-1,r)+w(l,r+1)\) 发现成立(其中大多数时候能取等;部分特殊情况取到大于号)。

那么发现可以用分治优化。发现 \(w\) 不那么能快速求;还是套用 Yet Another Minimization Problem 中的方法,用类似莫队的方式求解。

发现这个莫队套路也很熟悉了,直接用双端队列维护即可。复杂度 \(O(nk\log n)\),看着不太安全。但注意到我们在 20 个月前的提交中使用了 \(O(nk\log n)\) 的线段树,所以能过的兄弟。

鉴于 deque 的时空常数都大得吓人,所以我用静态 vector 模拟 deque 了。

跑得比我之前线段树的一半还快,兄弟。

#include <bits/stdc++.h>
const int inf = 1e18;
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n, m;
    std::cin >> n >> m;
    std::vector<std::vector<int> > pos(n + 1);
    std::vector<int> a(n + 1), _h(n + 1), _t(n + 1, -1);
    for (int i = 1; i <= n; ++i)
        std::cin >> a[i], pos[a[i]].push_back(i);
    std::vector<std::vector<long long> > f(m + 1, std::vector<long long> (n + 1, inf));
    f[0][0] = 0ll;
    auto w = [&](int ql, int qr) {
        static int l = 1, r = 0;
        static auto res(0ll);
        for (; l > ql; ) {
            --l;
            auto &h = _h[a[l]], &t = _t[a[l]];
            auto &q = pos[a[l]];
            if (h <= t)
                res -= q[t] - q[h];
            res += q[t] - q[--h];
        }
        for (; r > qr; ) {
            auto &h = _h[a[r]], &t = _t[a[r]];
            auto &q = pos[a[r]];
            res -= q[t--] - q[h];
            if (h <= t)
                res += q[t] - q[h];
            --r;
        }
        for (; r < qr; ) {
            ++r;
            auto &h = _h[a[r]], &t = _t[a[r]];
            auto &q = pos[a[r]];
            if (h <= t)
                res -= q[t] - q[h];
            res += q[++t] - q[h];
        }
        for (; l < ql; ) {
            auto &h = _h[a[l]], &t = _t[a[l]];
            auto &q = pos[a[l]];
            res -= q[t] - q[h++];
            if (h <= t)
                res += q[t] - q[h];
            ++l;
        }
        return res;
    };
    for (int t = 1; t <= m; ++t) {
        std::function<void(int, int, int, int)> calc = [&](int l, int r, int lp, int rp) {
            if (l > r)
                return;
            if (l == r) {
                for (int i = lp; i <= rp && i < l; ++i)
                    f[t][l] = std::min(f[t][l], f[t - 1][i] + w(i + 1, l));
                return;
            }
            int mid = (l + r) >> 1, p = -1;
            for (int i = lp; i <= rp && i < mid; ++i)
                if (f[t - 1][i] + w(i + 1, mid) < f[t][mid])
                    f[t][mid] = f[t - 1][i] + w(i + 1, mid), p = i;
            calc(l, mid - 1, lp, p), calc(mid + 1, r, p, rp);
            return;
        };
        calc(t, n, t - 1, n - 1);
    }
    std::cout << f[m][n] << '\n';
    return 0;
}

H - Battle Lemmings

https://codeforces.com/problemset/problem/1420/E

容易发现 \(0\) 的数目不变,答案就是 \(0\) 的对数 - 连续 \(0\) 的对数。

然后有一个我们很熟悉的 trick,随便找一个目标序列,那么花费的操作次数就是每个对应的 \(1\) 的位置差。令 \(f_{i,j,k}\) 表示用了 \(i\) 次操作、\(j\)\(1\)、最后一个 \(1\)\(k\) 的最小连续 \(0\) 对数。那么有:

\[ \begin{aligned} f_{i,j,k}&=\min\limits_{p<k}\left\{f_{i-|k-pos_j|,j-1,p}+\dfrac {(k-p-1)(k-p-2)}2\right\}\\ &=\min\left\{f_{i-|k-pos_j|,j-1,p}-k\cdot p+\dfrac {p(p + 2)}2\right\}+\dfrac {k^2-3k+2}2 \end{aligned} \]

发现这个式子是 \(O(n^5)\) 的,而且看起来很斜优,化为斜率形式 \(\dfrac{2\times f_A-2\times f_B+A(A+2)-B(B+2)}{2(A-B)}<k\)。维护递增斜率就可以 \(O(n^4)\) 做了。

Tip:当时写着写着愣住了,比如这个 \(i-|k-pos_j|\) 不是一直在动吗。解决方案?同时维护很多个队列即可。

注意还要把最后一个 \(1\) 之后连续 \(0\) 的代价算上。

#include <bits/stdc++.h>
const long long inf = 1e9;
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n;
    std::cin >> n;
    std::vector<int> a(n + 1);
    std::vector<long long> pos(1);
    for (int i = 1; i <= n; ++i) {
        std::cin >> a[i];
        if (a[i] == 1)
            pos.push_back(i);
    }
    int m = n * (n - 1) / 2;
    std::vector<std::vector<std::vector<long long> > > f(pos.size(), std::vector<std::vector<long long> > (m + 1, std::vector<long long> (n + 1, inf)));
    f[0][0][0] = 0ll;
    for (int j = 1; j < (int)pos.size(); ++j) {
        std::vector<std::vector<int> > _q(m + 1, std::vector<int> (n + 1));
        std::vector<int> _h(m + 1), _t(m + 1, -1);
        for (int k = 0; k <= n; ++k)
            for (int i = m; i >= 0; --i) {
                if (i >= std::abs(k - pos[j])) {
                    auto f1 = [&](long long A, long long B) {
                        return 2 * f[j - 1][i - std::abs(k - pos[j])][A] - 2 * f[j - 1][i - std::abs(k - pos[j])][B] + A * (A + 2) - B * (B + 2);
                    };
                    auto f2 = [&](long long A, long long B) {
                        return 2 * (A - B);
                    };
                    auto &h = _h[i - std::abs(k - pos[j])], &t = _t[i - std::abs(k - pos[j])];
                    auto &q = _q[i - std::abs(k - pos[j])];
                    for (; h < t && f1(q[h + 1], q[h]) < k * f2(q[h + 1], q[h]); ++h) {}
                    if (h <= t)
                        f[j][i][k] = std::min(inf, f[j - 1][i - std::abs(k - pos[j])][q[h]] + (k - q[h] - 1) * (k - q[h] - 2) / 2);
                }
                auto f1 = [&](long long A, long long B) {
                    return 2 * f[j - 1][i][A] - 2 * f[j - 1][i][B] + A * (A + 2) - B * (B + 2);
                };
                auto f2 = [&](long long A, long long B) {
                    return 2 * (A - B);
                };
                auto &h = _h[i], &t = _t[i];
                auto &q = _q[i];
                for (; h < t && f1(k, q[t]) * f2(q[t], q[t - 1]) < f1(q[t], q[t - 1]) * f2(k, q[t]); --t);
                q[++t] = k;
            }
    }
    auto res(-inf);
    int cnt = n - (int)pos.size() + 1;
    cnt = cnt * (cnt - 1) / 2;
    for (int i = 0; i <= m; ++i) {
        for (int k = 0; k <= n; ++k)
            res = std::max(res, cnt - f.back()[i][k] - (n - k) * (n - k - 1) / 2);
        std::cout << res << ' ';
    }
    std::cout << '\n';
    return 0;
}

A - Yakiniku Restaurants

https://atcoder.jp/contests/arc067/tasks/arc067_d

发现固定左右端点后,收益是可以贪心算的;下意识想到只固定左端点,那么右端点应该就可以用单调队列之类的搞一搞。

先提前把所有东西塞到队列里。左端点一开始在最右边;往左边动一下之后,就可以更新每种菜的队列;发现在所有元素中作决策点的不总是队头;这个地方用 单调递减的单调栈 是极好的。这里的单调栈其实就类似 四边形不等式中的单调数据结构 了。

维护单调栈中每个决策点的影响区间;显然每个右端点的答案变化量相同;用个类似于差分的东西记录一下就好了。

复杂度 \(O(n^2)\)

#include <bits/stdc++.h>
const long long inf = 1e18;
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n, m;
    std::cin >> n >> m;
    std::vector<long long> s(n + 1), f(n + 1);
    for (int i = 2; i <= n; ++i) {
        std::cin >> s[i], s[i] += s[i - 1];
        f[i] = -s[i];
    }
    std::vector<std::vector<int> > a(n + 1, std::vector<int> (m + 1));
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            std::cin >> a[i][j];
    struct node { int j, l, r; };
    std::vector<std::stack<node> > _q(m + 1);
    auto res(-inf);
    for (int l = n; l; --l) {
        std::vector<long long> d(n + 1);
        auto add = [&](int l, int r, long long v) {
            d[l] += v;
            if (r != n)
                d[r + 1] -= v;
            return;
        };
        for (int j = 1; j <= m; ++j) {
            auto &q = _q[j];
            node now = { j, l, l };
            add(l, l, a[l][j]);
            for (; !q.empty() && a[l][j] >= a[q.top().l][q.top().j]; q.pop()) {
                now.r = q.top().r;
                add(q.top().l, q.top().r, a[l][j] - a[q.top().l][q.top().j]);
            }
            q.push(now);
        }
        std::partial_sum(d.begin() + 1, d.end(), d.begin() + 1);
        for (int r = l; r <= n; ++r) {
            f[r] += d[r];
            res = std::max(res, f[r] + s[l]);
        }
    }
    std::cout << res << '\n';
    return 0;
}

B - Jellyfish and Miku

https://codeforces.com/problemset/problem/1874/D

唉数列。唉概统。在数学讲义上折磨了我一遍之后还要到这儿来折磨我。

假设已经知道了 \(a\),考虑求期望步数。设 \(E_i\) 为从 \(i\) 出发走到 \(n\) 的期望步数。那么有:

\[ E_i=\begin{cases} E_1+1&i=0\\ 0&i=n\\ E_i=(E_{i-1}+1)\cdot \dfrac {a_i}{a_{i+1}+a_i}+(E_{i+1}+1)\cdot \dfrac {a_{i+1}}{a_{i+1}+a_{i}}&\text{otherwise} \end{cases} \]

(提示:从『\(i\) 下一步会走哪个方向』考虑。)

接下来就可以利用你的高中数学知识进行一个 \(f_0\) 的求,(一堆过程),得到 \(E_0=n+2\times \sum\limits_{i=1}^n\dfrac {\sum_{j\le i}a_j}{a_i}\),然后想要最小化这个东西。

不妨令 \(f_{i,j}\) 表示到 \(i\) 时已经分配走了 \(j\) 体积,\(\sum_{k=1}^i \dfrac {\sum_{l\le k}a_l}{a_k}\) 的最小值,有 \(f_{i,j}=\min\limits_{k<j}\left\{f_{i-1,k}+\dfrac {k}{j - k}\right\}\)。发现它大抵是满足四边形不等式的,按照 2D/1D DP 优化的结论,代入 \(p_{i,j-1}<p_{i,j}<p_{i+1,j}\) 可以 \(O(nm)\) 解决问题。

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n, m;
    std::cin >> n >> m;
    std::vector<std::vector<int> > p(n + 1, std::vector<int> (m + 1));
    std::vector<std::vector<long double> > f(n + 1, std::vector<long double> (m + 1, 1e18));
    f[0][0] = 0ll;
    for (int j = 1; j <= m; ++j)
        for (int i = std::min(j, n); i; --i) {
            int to = ((i == std::min(j, n)) ? j : std::min(p[i + 1][j], j));
            for (int k = p[i][j - 1]; k <= to; ++k)
                if (f[i - 1][k] + k * 1. / (j - k) < f[i][j])
                    f[i][j] = f[i - 1][k] + k * 1. / (j - k), p[i][j] = k;
            // printf("f[%d][%d] = %Lf, p = %d\n", i, j, f[i][j], p[i][j]);
        }
    std::cout << std::fixed << std::setprecision(10) << n + 2 * f[n][m] << '\n';
    return 0;
}

Cut the Sequence

https://www.luogu.com.cn/problem/P10977

通知:区间最值 不满足 四边形不等式。

其实在猜的时候是举了反例的,但是大脑萎缩了推着推着忘记符号了 😅

看到 \(f_i=\min\limits_{g(i)\le j<i}\{f_j+\max\{a_{j+1\sim i}\}\}\) 这个 \(j\) 的范围其实是有点单调队列优化的感觉的,但这个最大值传统的单调队列不是很可做。可以注意到最大值这一项有点 单调队列 后缀最大值的感觉(实际上就是);一个很自然的想法是利用这个最大值影响的区间,维护 \(f\) 的线段树来暴力做。

另一个比较牛的做法是发现同一个下标的 \(f\)\(a\) 的关系。首先需要注意到 \(f\) 单调不降;对于同一个 \(a\),能取到的 \(f\) 就是最靠前的;维护一个 \(a\) 的单减队列,那么共用同一个 \(a\) 的就是相邻两个下标之间的部分,其最优决策在能取到的最前端取得;需要注意到队列里的贡献并不单调,需要用一个 multiset 来存储所有贡献并查找、更新。

需要注意单调队列里某个元素 \(f_{q_i}\) 结合的其实是 \(a_{q_{i+1}}\)。还需要注意队头的维护,可能需要一些小巧思。

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen("1.in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n, m;
    std::cin >> n >> m;
    std::vector<int> a(n + 1);
    std::vector<long long> s(n + 1);
    for (int i = 1; i <= n; ++i)
        std::cin >> a[i], s[i] = s[i - 1] + a[i];
    int h = 0, t = -1;
    std::multiset<long long> st;
    std::vector<long long> f(n + 1);
    std::vector<std::pair<int, int> > q(n + 1);
    q[++t] = { 0, 0 }, a[0] = 0x3f3f3f3f;
    for (int i = 1; i <= n; ++i) {
        for (; s[i] - s[q[h].second] > m; ) {
            st.erase(st.find(f[q[h].second] + a[q[h + 1].first]));
            if (++q[h].second >= q[h + 1].first)
                a[q[++h].first] = 0x3f3f3f3f;
            else
                st.insert(f[q[h].second] + a[q[h + 1].first]);
        }
        for (; h < t && a[q[t].first] <= a[i]; --t)
            st.erase(st.find(f[q[t - 1].second] + a[q[t].first]));
        st.insert(f[q[t].second] + a[i]), q[++t] = { i, i };
        f[i] = *st.begin();
    }
    std::cout << f[n] << '\n';
    return 0;
}

言论