Explosions? 题解

好像又打了一种与众不同的做法?


如果想要在最后用一个炸弹炸死所有的怪物,那么还活着的怪物的血量一定会是先单调递增,后单调递减的。

把它拆开考虑,如果能够求出“让一个怪物左边的还活着的怪物的血量单调递增”的代价,记为 $pre$;和“让一个怪物右边的还活着的怪物的血量单调递减”的代价,记为 $suf$。那么答案就是:

$$ \min\limits_{1 \leqslant i \leqslant n}\left\{suf_{i} + pre_{i} + h_{i}\right\} $$

又因为单调递增和单调递减是对称的,我们只讨论一种情况就行了。下面考虑单调递增的情况。

会发现严格单调递增的情况不太方便维护,能不能转化为非严格单调递增呢?

很简单,对于严格单调递增的第 $i$ 个数将它减去 $i$ 就好了。因为每一个数至少比它前面的数大 $1$,将这个 $1$ 减掉就会变成不严格单调递增了,前面的 $1$ 减掉了会影响到后面的,随后统计出来就是 $i$ 了。

于是可以这样维护:对于第 $i$ 个怪物,我们找到前面的“血量”大于 $h_{i} - i$ 的怪物(这里的“血量”并不是真实的血量,是减去怪物编号后的结果),将它们的“血量”消减至 $h_{i} - i$,同时统计代价,这样就能算出来 $pre_{i}$ 了。

但是这样做是错的,因为某些怪物的血量会被削减至负数,显然会多统计一段代价。

因为怪物的血量是单调递增的,所以一个活着的怪物后面的怪物肯定也是活着的,可以维护一个指针,指针指向着“第一个活着的怪物”,每次削减血量之后将指针向后扫就行,如果死了就将多算的代价减掉,否则停下。

于是我自信地打了一个双 $\log$ 做法,结果过不了(在找“血量”大于 $h_{i} - i$ 的怪物的时候套了一个二分)。

从“削减血量”这个操作下手,会发现它其实是一个区间推平的操作,如果要快速找到一段数值相同的区间可以用并查集维护,好像这样就能做到均摊 $\mathcal{O}(n \log n)$ 了?(不算路径压缩的情况下,每个结点的父亲结点至多被更改两次,一次是合并集合的时候,一次是死掉了的时候。)

于是我们要做的事情就是:区间推平,区间求和,快速查找数值相同的一段区间,用线段树和并查集一起维护就好了。

代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int t, n, pos, l, r, mid, pt, v, h[300005], father[300005];
ll ans, sum, pre[300005], suf[300005];
int findset(int x) {return father[x] == x ? x : father[x] = findset(father[x]);}
struct Segment_Tree {
    struct segment {
        int l, r, tag, mn;
        ll sum;
    } t[1200005];
    #define lc (k << 1)
    #define rc (lc | 1)
    #define mid ((t[k].l + t[k].r) >> 1)
    void build(int k) {
        t[k].tag = -(1 << 30), t[k].sum = 0, t[k].mn = 1 << 30;
        if(t[k].l == t[k].r) return;
        t[lc].l = t[k].l, t[lc].r = mid, t[rc].l = mid + 1, t[rc].r = t[k].r;
        build(lc), build(rc);
    }
    void push_down(int k) {
        if(t[k].tag > -(1 << 30)) {
            t[lc].tag = t[rc].tag = t[k].tag;
            t[lc].mn = t[rc].mn = t[k].tag;
            t[lc].sum = (t[lc].r - t[lc].l + 1ll) * t[k].tag, t[rc].sum = (t[rc].r - t[rc].l + 1ll) * t[k].tag;
            t[k].tag = -(1 << 30);
        }
    }
    void push_up(int k) {
        t[k].mn = min(t[lc].mn, t[rc].mn), t[k].sum = t[lc].sum + t[rc].sum;
    }
    void change(int k, const int& L, const int& R, const int& val) {//区间推平
        if(L <= t[k].l && t[k].r <= R) {
            t[k].mn = val, t[k].sum = (t[k].r - t[k].l + 1ll) * val;
            t[k].tag = val;
            return;
        }
        push_down(k);
        if(L <= mid) change(lc, L, R, val);
        if(R > mid) change(rc, L, R, val);
        push_up(k);
    }
    ll ask_sum(int k, const int& L, const int& R) {//区间求和
        if(L > R) return 0;
        if(L <= t[k].l && t[k].r <= R) return t[k].sum;
        push_down(k);
        ll ret = 0;
        if(L <= mid) ret += ask_sum(lc, L, R);
        if(R > mid) ret += ask_sum(rc, L, R);
        push_up(k);
        return ret;
    }
    #undef mid
} tree;
void get(ll* val) {
    tree.t[1].l = 1, tree.t[1].r = n;
    tree.build(1);
    val[1] = 0, pt = 1;
    for(int i = 1; i <= n; ++i) {
        father[i] = i;
    }
    for(int i = 1; i <= n; ++i) {
        pos = i;
        while(pos > pt) {//找到第一个数值大于 h_i - i 的区间
            if(tree.ask_sum(1, findset(pos - 1), findset(pos - 1)) >= h[i] - i) {
                father[findset(pos)] = findset(pos - 1);
                pos = findset(pos);//因为这个区间肯定会被推平,提前把它的父结点改了
            }
            else break;
        }
        sum = tree.ask_sum(1, pos, i - 1);
        val[i] = val[i - 1] + sum - (ll)(i - pos) * (h[i] - i);//统计代价
        tree.change(1, pos, i, h[i] - i);//把大于 h_i - i 的“血量”削减到 h_i - i
        while(pt < i) {//指针维护最后一个活着的怪物
            v = tree.ask_sum(1, pt, pt);
            if(v + pt < 0) {
                val[i] += v + pt;//把多算的代价减回来
                tree.change(1, pt, pt, -(1 << 29));//直接设成极小值,这样后面肯定不会再统计到了
                father[pt + 1] = pt + 1;//注意设成极小值了过后不能再用这个结点“代表”一个区间了(不能作为一个并查集的根节点)
                father[findset(pt)] = pt + 1;
                ++pt;
            }
            else break;
        }
    }
}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> t;
    while(t--) {
        cin >> n;
        for(int i = 1; i <= n; ++i) {
            cin >> h[i];
        }
        ans = 1ll << 62;
        get(pre);
        reverse(h + 1, h + 1 + n);//pre 和 suf 是对称的,reverse 一下在计算就好
        get(suf);
        reverse(h + 1, h + 1 + n);
        reverse(suf + 1, suf + 1 + n);//注意要 reverse 回来
        for(int i = 1; i <= n; ++i) {
            ans = min(ans, pre[i] + suf[i] + h[i]);//统计答案
        }
        cout << ans << '\n';
    }
    return 0;
}
posted @ 2023-12-20 19:26  A_box_of_yogurt  阅读(1)  评论(0编辑  收藏  举报  来源
Document