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;
}
本文来自博客园,作者:A_box_of_yogurt,转载请注明原文链接:https://www.cnblogs.com/A-box-of-yogurt/p/18016402
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现