题目描述
你在玩一个利用魔法杀怪物的游戏。具体地,有一排 \(n\) 个格子,第 \(i\) 个格子里有编号为 \(i\)、初始生命值为 \(h_i\) 的怪物。怪物生命值小于等于 \(0\) 则被杀死,所有怪物被杀死则游戏结束。
你有两种攻击方式:一种是普通攻击,你将消耗 \(1\) 点能量对选中的怪物进行 \(1\) 点伤害(也就是怪物的生命值减 \(1\)),你可以使用普通攻击任意次数。同时,你还可以使用恰好一次“爆炸”攻击。你想要游戏以“爆炸”攻击结束,也就是说,你会先进行若干次普通攻击(可以为 \(0\) 次),然后使用一次“爆炸”攻击结束游戏。
“爆炸”攻击的机制如下:你可以选择消耗的能量数 \(x\),然后选中一个怪物 \(i\),之后:
- 若怪物 \(i\) 当前的生命值 \(h_i > x\),那么该怪物生命值减去 \(x\);
- 若 \(h_i\le x\),则该怪物被击杀,同时向两旁的怪物(即第 \(i-1\) 和 \(i+1\) 只怪物,如果有且还存活的话)造成 \(h_i - 1\) 的伤害;
- 若 \(h_i - 1\) 的伤害足以杀死第 \(i-1\) (或 \(i+1\))只怪物,即 \(h_{i-1}\le h_i - 1\)(或 \(h_{i+1}\le h_i - 1\)),那么这只怪物同样被击杀并向两旁的怪物造成 \(h_{i-1} - 1\)(或 \(h_{i+1} - 1\))的伤害。如此循环只到杀不死一直怪物为止。
你的目标就是先用普通攻击减少某些怪物的生命值或直接杀死某些怪物,然后用这样“链式”的“爆炸”攻击杀死所有怪物。注意怪物不会移动,例如第 \(i\) 只怪物和第 \(i+2\) 只怪物永远不会相邻。
你需要消耗最少的能量值来达成目标(包括普通攻击和“爆炸”攻击的),求出这个最小值。
题目分析
首先可以想到一个\(O(n^2)\)的做法
注意到我们爆炸需要最后使用,也就是说我们要选择一个点作为爆炸的点,将所有的剩余部分全部炸完,也就是说我们爆炸的时候情况可以抽象成下图:
也就是说,当最后形成一个单峰函数的时候,就可以使用炸弹了。我们可以将这个图形分为两个部分,单调上升部分,炸弹投放点以及单调下降部分。
所以有一个很明显有一个朴素的做法,我们可以枚举每个点作为炸弹投放点,然后贪心地付出代价使其变成一个单调上升->下降图形,我们最后剩余的图形的值的总和越大。具体地讲,我们直接从每个枚举的点出发,向左右拓展,每次拓展所能节省的代价即为\(min(Temp-1,Line_i)\)(\(Temp\)用以存储上一个位置保留的高度,\(Line_i\)则是当前点的高度),在这里则是:(注意:单调下降不能两个不能相同)
for(int i=1;i<=n;i++){
int Temp=Line[i];
for(int j=i-1;j>=1;j--){
Temp=min(max(Temp-1,0),Line[j]);
Need[0][i]+=Line[j]-Temp;
}
Temp=Line[i];
for(int j=i+1;j<=n;j++){
Temp=min(max(Temp-1,0),Line[j]);
Need[1][i]+=Line[j]-Temp;[]
}
Ans=min(Ans,Need[1][i]+Need[0][i]+Line[i]);
}
这个实际上就是\(O(n^2)\)的朴素做法了。
然后就根据\(O(n^2)\)的做法形成\(O(n)\)的做法了
注意到我们要求出的剩余部分是一个单调的序列,既然是单调的,那我们自然能想到单调栈或者单调队列优化。
我们考虑一个转移,我们用\(Need_{0/1,i}\)这个数组表示从前往后(0)/从后往前(1)的情况下使得前缀/后缀区间转化为单调上升/下降区间所需的最小代价,那么我们每次遇到一个新的怪物,我们尝试将其入栈,如果在栈中的前一个元素比它小,那么就一定要出栈前一个元素。那如果前一个元素比它大呢,我们可以分析下面这种情况。
注意到这时\(9,10\)都应该出栈,那么\(7\)呢,可以发现,要让\(8\)联合左边形成单调上升区间,\(9\)这个值要改成\(7\),\(8\)的值要改成\(6\),\(7\)的值应该改为\(5\),所以可以发现\(7\)这个点起码要下降至\(8-3=5\),所以只要\(Pos_i-Pos_{Q.Back()}>Line_i-Line_{Q.Back}\)时就应该使其出栈,这样正确性才能够得到保证,可以证明以这条规则出入栈最后一定可以靠栈中的元素还原出一个单调上升的序列。
那么如何实时更新这个序列的总值呢,可以发现,每个点代表的权值,都是一个梯形的面积,所以我们可以知道一个点所能产生的权值,即是\(len\times(Line_i+Line_i-Line_{Q.Back})/2\)(\(len即是它与栈中前一个元素间隔的距离\))(注意:这个公式如果\(len\)过长会形成一段负序列,我们只需将\(len\)设置为\(min(\Delta Pos,\Delta Line)\)即可)
如上图,\(7\)这个位置所能产生的值就是\((11+8)*4/2\)(必定可以形成这段红色区域,根据前面的出入栈规则,如果不能形成,那么栈中在\(4\thicksim6\)这些位置还会有别的元素)
那么知道计算规则之后,我们只需要每次出栈的时候将它产生的贡献删除,入栈时加入贡献即可。
那么每次我们的\(Need_{0,i}\)就是\(PreSum-Q.Sum\)(\(Q.Sum\)是左侧形成序列的值之和)。
对于\(Need_{1,i}\),同理做即可。
那么统计答案的时候直接\(min^{n}_{i=1}(Need_{0,i}+Need_{1,i}+Line_i)\)即可。
代码
/*
* Author:Ehundategh
* Update:2023/10/18
* Title:explosion2.cpp
* You steal,I kill
*/
#include <bits/stdc++.h>
using namespace std;
#define MAXN 300010
int T,n;
long long Need[2][MAXN],Line[MAXN],Ans=0x7fffffffffffffff,PreSum=0,SuffSum=0;
class Container{
public:
int Head=1,Tail=0,Pos[MAXN];;
long long Sum=0;
void Push_Back(int a){Pos[++Tail]=a;}
void Pop_Back(){Tail--;}
int Back(){return Pos[Tail];}
bool Empty(){return Head>=Tail;}
}Q;
long long Calc(long long Height,long long Length){//计算元素贡献
Length=min(Length,Height);
return 1ll*(Height+Height-Length+1)*Length/2;
}
void Ehundategh_Clear(){//清空
PreSum=SuffSum=0;
Q.Head=1;Q.Tail=0;
Q.Sum=0;
Q.Push_Back(0);
Ans=0x7fffffffffffffff;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>T;
while(T-->0){
cin>>n;
Ehundategh_Clear();
for(int i=1;i<=n;i++) cin>>Line[i];
for(int i=1;i<=n;i++){
PreSum+=Line[i];
while(i-Q.Back()>Line[i]-Line[Q.Back()]&&!Q.Empty()){
Q.Sum-=Calc(Line[Q.Back()],(long long)(Q.Back()-Q.Pos[Q.Tail-1]));
Q.Pop_Back();
}
Q.Push_Back(i);
Q.Sum+=Calc(Line[Q.Back()],(long long)(Q.Back()-Q.Pos[Q.Tail-1]));
Need[0][i]=PreSum-Q.Sum;
}
Q.Head=1;Q.Tail=0;
Q.Sum=0;
Line[n+1]=0;
Q.Push_Back(n+1);
for(int i=n;i>=1;i--){
SuffSum+=Line[i];
while(Q.Back()-i>Line[i]-Line[Q.Back()]&&!Q.Empty()){
Q.Sum-=Calc(Line[Q.Back()],(long long)(Q.Pos[Q.Tail-1]-Q.Back()));
Q.Pop_Back();
}
Q.Push_Back(i);
Q.Sum+=Calc(Line[Q.Back()],(long long)(Q.Pos[Q.Tail-1]-Q.Back()));
Need[1][i]=SuffSum-Q.Sum;
}
for(int i=1;i<=n;i++){
if(Need[0][i]+Need[1][i]+Line[i]<Ans) Ans=Need[0][i]+Need[1][i]+Line[i];
}
cout<<Ans<<'\n';
}
return 0;
}
如果觉得题解对你有帮助,那就点个赞吧。