Loading

[CF 2107] F1 & F2. Cycling (Easy Version & Hard Version).md

思路

需要找点性质
直接贪显然假了

考虑有没有什么固定的最优解
注意到一种方案是
对于 \(a_i, a_{i + 1}\), 我们可以通过交换这俩然后再吃一个 \(a_{i + 1}\) 来超过 \(i\)

因此最优解有这样的构造方式
首先将整个 \(a\) 分成若干段, 然后在其中找一个元素, 把它移到段尾, 然后这样把整个段处理掉

考虑这样直接做加上斜率优化可以过, 但是显然还有一点最优解的性质
不难发现对于一段, 一定要选择其中的最后一个最小值, 然后就然后了

考虑又是前缀又是 \(\mathcal{O} (n)\)
注意到直接继承没法做的, 因为 \(\rm{dp}\) 转移是倒着的但是加数往最后加, 没得继承

考虑继续找性质
首先, 不难发现相邻两段, 如果后面那段的最小值小于前面那段的最小值, 那么必定可以让后面那段最小值吃完这两段
因此说这个段的最小值单调不降

进一步还有性质
注意到相邻两段, 前一个最小值来回在一个点上 \(2\), 后一个最小值只需要一个点 \(1\), 因此显然

  • 后一个最小值恰好比前一个最小值大 \(1\) / 相等
    直接吃到前一个最小值那里, 也就是前一个最小值应当作为段尾
  • 后一个最小值比前一个最小值大 \(2\) 及以上
    前一个最小值可以一路吃到段尾

因此分段一定是这样的
pELE1j1.png

一个小修复是如果一段中出现了等于最小值的情况, 那么可以省 \(1\), 可以视作每次找第一个出现的最小值, 然后分段这样

发现 1 2 4 4 似乎推翻了这个结论
因此不难发现

后一个最小值恰好比前一个最小值大 \(1\)

的情况下, 前一段实际上是可以吞并后一段的\((\)最早算少了后一段的一个花费\()\), 但是这样前一段就必须一路吃到段尾
因此归纳一下, 不妨认为我们可以任意挑选一个分段点飞到最后去

不难发现每次挑选的花费是一个一次函数, 每次前缀加入一个数可以单调栈维护是否可以作为某一段的最小值, 然后用李超线段树维护即可
代码不写了


搞了这么久, 还是想弄明白, 但是以后还是注意一下时间上的问题, 今天开个特例吧

刻画交换操作和超越的关系

首先刻画交换操作和超越的关系
考虑一种显然的做法如图
pEL8tsO.png

进一步观察样例不难发现 vv 可以先往后走, 向后走花费「往后走的个数」代价, 向前走花费「往后走的个数」代价, 但是可以更改所有掠过的点的超越花费
本质上是把 vv 的超越花费提前干了

初步考虑 \(\textrm{Easy Version}\)
直觉上感受到应该按照超越花费分段, 那么一段的最小值作为 \(v\), 这个性质的得出来源于本题的关键性质

\(v\) 向后移动, 还需要向前移动, 一个位置花费为 \(2\), 而不用向后移动的一个位置花费为 \(1\), 其中相差为 \(1\)

简单的做分段 \(\rm{dp}\), \(\mathcal{O} (n^2)\) 是可以过的

显然还有一定性质
首先是段的最小值单调不降, 这个是显然的

其次是相邻段最小值相差等于 \(1\) 的情况下, 这两段的分段点可以选在第一段的最小值之后和第二段的最小值之前的任意位置, 不妨强制钦定在第一段的最小值之后, 也就是最小值作为段尾
相差为 \(0\) 的情况, 显然可以视作一段, 但是少一步向前移动的花费 \(1\), 不妨直接归为分段
相差大于 \(1\) 的情况, 用第一段一路吃到段尾

然后根据样例 1 2 4 4, 发现如果拿 \(1\) 吃到段尾, 那么省下来 \(1\) 的花费
也就是说虽然 \(1\)\(2\) 亏了, 但是后面的两个数直接赚回来了, 这种情况如何处理?
不妨对于每个分段点, 都尝试一下把他吃到最后看花费

不难发现对于位置 \(p\) 上的分段点 \(v = a_p\), 其花费为

\[ \begin{align*} & f_{pre_p} + v \times (n - pre_p) + 2(n - p) + p - pre_p - 1 \\ = & f_p + v \times (n - p) + 2(n - p) \end{align*} \]

其中 \(f_p\) 表示 \(p\) 之前的花费, 可以线性通过 \(pre_p\) 维护, 作为常数即可
注意这个 \(n\) 是一个变量, 不妨设为 \(x\)

\[ \begin{align*} & f_p + a_p \times (n - p) + 2(n - p) \\ = & (a_p + 2)x + \Big[f_p - (a_p + 2)p\Big] \end{align*} \]

作为一条直线, 可以用李超线段树维护最值捏


这么快就把自己创飞了

首先找点性质
不难发现交换和超越操作结合起来有如下性质:
带着 \(a_p\) 交换

  • \(a_p\) 之前, 单点花费 \(a_p + 1\)
  • \(a_p\) 之后, 单点花费 \(a_p + 2\)
  • \(a_p\), 单点花费 \(a_p\)

于是最优解一定是长这样的
pEzVhQO.png

每个分段点的值记作 \(v_i\), 不难发现 \(v_i \leq v_{i + 1} \leq v_i + 1\)
考虑其中任意一个点都可以试着去后面, 不难发现如果你要往后面走, 那么你一定要走完

计算每个点走完的花费是简单的, 最后形如一个一次函数, 维护即可


考虑更贴近我的目标的 \(\mathcal{O} (n^2)\) \(\rm{dp}\)
对于一段区间, 我们最初的想法是选择最后一个最小值
但是正确吗?

发现并不是的, 如果这个点后面有足够多的 \(m + 1\), 实际上这个点是有可能亏损的

因此我们基于上面的结论

结论

带着 apa_p 交换

  • apa_p 之前, 单点花费 ap+1a_p + 1
  • apa_p 之后, 单点花费 ap+2a_p + 2
  • apa_p, 单点花费 apa_p

pEzmMz6.png

尝试找到一种最优解的构造

首先注意到, 全局最小值 \(a_{m_1}\) 一定控制了 \([1, m_1]\)
如果 \((m_1, n]\) 中有 \(a_{m_2} \leq a_{m_1} + 1\), 我们要么让他控制 \((m_1, m_2]\), 要么直接让 \(a_{m_1}\) 控完 \([1, m_2]\), 这是因为后续的赚可能可以补上这一段的亏空
否则我们显然直接让 \(a_{m_1}\) 控完 \([1, n]\) 即可, 因为一定更优

所以我们对于一个全局最小值之后的后缀最小值, 有两种策略

  • 吃完这一段, 并且保留当前的这个值吃掉后面的权利
  • 直接让之前的那个值吃掉这一段

因此我们转移就直接设 \(f_{i, j}\) 表示到达 \(i\), 现在的最小值是 \(j\) 的最小花费, 每次要么改要么不改, 复杂度跟状态数是一样的, \(\mathcal{O} (n^2)\)

最初要先预处理出每个可能的最小值

基于此思路的代码
#include <bits/stdc++.h>
#define int long long
const int MAXN = 5020;

int n;
int a[MAXN];

int f[MAXN][MAXN];
std::vector<int> KEY;

signed main() {
    int _; scanf("%lld", &_);
    while (_--) {
        scanf("%lld", &n);
        for (int i = 1; i <= n; ++i) scanf("%lld", a + i);
        a[0] = 1e18;

        /*预处理可能的最小值取值*/
        KEY.clear(); KEY.shrink_to_fit(); KEY.push_back(0);
        int p = 0; for (int i = 1; i <= n; ++i) if (a[i] < a[p]) p = i; KEY.push_back(p);
        
        while (p <= n) {
            p++; for (int i = p; i <= n; ++i) if (a[i] < a[p]) p = i;
            if (p > n) break;
            if (a[p] == a[*KEY.rbegin()]) KEY.push_back(p);
            else if (a[p] == a[*KEY.rbegin()] + 1) KEY.push_back(p);
            else break;
        }
        if (*KEY.rbegin() != n) KEY.push_back(n);

        for (int i = 0; i <= n; i++) for (int j = 0; j <= n; j++) f[i][j] = 1e18;
        f[0][0] = 0; a[0] = 0;
        for (auto PNT = std::next(KEY.begin()); PNT != KEY.end(); ++PNT) {
            int pnt = *PNT; int lst = *std::prev(PNT);
            for (auto CTRL = KEY.begin(); CTRL != PNT; CTRL++) {
                /*鼠鼠没啥用, 只能被吃了*/ f[pnt][*CTRL] = std::min(f[pnt][*CTRL], f[lst][*CTRL] + (a[*CTRL] + 2) * (pnt - lst));
                /*鼠鼠可能有用*/ if ((a[pnt] <= a[*CTRL] + 1 && *CTRL != 0) || (*CTRL == 0 && PNT == std::next(KEY.begin()))) f[pnt][pnt] = std::min(f[pnt][pnt], f[lst][*CTRL] + (pnt - lst - 1) * (a[pnt] + 1) + a[pnt]);
            }
        }

        int ans = 1e18;
        for (auto CTRL = std::next(KEY.begin()); CTRL != KEY.end(); CTRL++) ans = std::min(ans, f[n][*CTRL]);
        printf("%lld\n", ans);
    }

    return 0;
}

基于上述做法容易想到最终答案一定是选择一个位置吃到最后
这个易于维护

哎哎哎
感觉还是要加油了

总结

交换非相邻元素的花费为交换距离, 显然应该相邻交换最优

最优解的性质说是
关键步记录还是必要的, 老是小地方搞错
需要不断地简化你的表达

往往要注意 \(\rm{dp}\) 过程中有没有能贪的性质

神奇的错解不优

  • 对每个分段点都处理一次, 因为不确定哪个点最优
  • 理论上需要删除的处理, 如果不可能使得答案更大, 那么可以不删

考虑最优解的构造

posted @ 2025-05-06 20:20  Yorg  阅读(44)  评论(0)    收藏  举报