[洛谷1485] 火枪打怪
题目描述
LXL进入到了一片丛林,结果他发现有n只怪物排成一排站在他面前。LXL有一杆火枪能对付这些怪物。他知道从左至右数第i只怪物的血量是mi。现在LXL可以将一些子弹射向某个怪物。LXL可以控制他所发射的子弹数量及子弹的威力值。当某个子弹射到第i个怪物,如果这个子弹的威力值为p,除了这个怪物会掉p点血以外,它左边的第j个怪物(j<i),也会遭到Max(0, p - (i - j) * (i - j))的溅射伤害(好神奇的子弹)。当某只怪物的血量小于0时,它就死了,但它的尸体还在,即怪物的位置永远不会改变。LXL希望只用k发子弹,请你求出一个最小的正整数p,使LXL用k发子弹且每发子弹的威力值为p就可以消灭所有怪物。
输入输出格式
输入格式:第一行,两个正整数n,k。
第二行,n个正整数,第i个正整数表示从左至右数第i只怪物的血量mi。
输出格式:一个正整数,表示子弹的最小威力值p。
输入输出样例
说明
对于30%的数据,n<=300。
对于100%的数据,n<=500000,k<=500000,1<=mi<=10^10
题目要求
LXL面前有n只怪物,他将一些子弹射向某个怪物。LXL可以控制他所发射的子弹数量及子弹的威力值。当某个子弹射到第i个怪物,如果这个子弹的威力值为p,除了这个怪物会掉p点血以外,它左边的第j个怪物(j<i),也会遭到Max(0, p - (i - j) * (i - j))的溅射伤害。当某只怪物的血量小于0(敲黑板!!等于0还没有死喔)时,它就死了,但是位置永远不会改变。求出用k发子弹消灭所有怪物的最小威力值p,且p为正整数。
思路分析
思路和楼下基本是相同的,另外扩充一些细节。
首先提醒一个细节:会爆int,所以定义要用longlong。
其次对做法进行分析。因为p是跟怪物的Hp值相关的,所以我们先观察一下Hp的范围:1<=Hp[i]<=10^10。因为范围比较大,所以我们采用二分的方法来寻找最优解。需要注意的是,威力值p有可能是大于Hp值的,所以我们在对p进行二分查找的时候要将范围在[1,10^10]的基础上再进行扩大。
那么如何判断p是否满足题目条件呢?这里我们写一个判断函数对其进行条件满足与否的判定。如何判定是整个题的关键,那么我们一步一步来分析:
<判断函数>
子弹的溅射范围是在被击中怪物的左侧,所以为了尽可能的多溅射到怪物,我们从最右边开始枚举。我们用一个num数组来记录每个位置上射出子弹的个数(受到伤害为p而不是p-(i-j)*(i-j)的子弹个数,也就是说被溅射到的不算,溅射的子弹会用别的方法储存)。接下来怎么做呢?
首先我们对式子p-(i-j)*(i-j)进行处理。将该式拆开得到:伤害值=p-i^2+2ij-j^2。由于我们在枚举时p的值在该次枚举内是固定不变的,所以我们将p当做一个定值去处理。同理j(当前枚举到的位置)也可以认为是一个定值,所以推到这一步我们就可以明白式子拆开的优点:我们可以单独计算和存储这两个定值。那么,假设该怪物在其位置上的有效子弹(包括其他位置溅射过来的)为plus,我们就可以得到这样一个式子:
- 总伤害(allhurt)=Σ(p-(i-j)^2)
我们将它展开来看,就有:
- 原式=Σp-Σi^2+Σ2ij-Σj^2
注:p是一个定值,j同理(因为在当前枚举回合内两个值是不变的,所以可以这样认为)。所以我们可以将Σp和Σj提出,且容易证得Σp=plus*p。
而因为i是一个变值,所以i前的Σ就需要通过循环操作来实现。
那么如何来实现呢?
我们可以通过当前枚举到的位置j的后一位(即j+1)得到当前位置的状态。我们通过累加j+1放置的子弹个数来求得plus。但是值得注意的是,如果子弹无法溅射到当前位置,也就是说累加到的子弹数里包含了无效子弹,那么如果不除去无效子弹的个数,就会对我们的计算结果造成影响。那么如何除去呢?
我们再来观察一下溅射伤害计算的公式:溅射伤害=p-(i-j)^2。容易得出,我们的溅射最大范围其实就是sqrt(p)+1。可以手动计算验证一下。我们将这个溅射范围定义为over,即:
- 溅射范围(over)=sqrt(p)+1
也就是说,如果某个位置放置的子弹已经无法对当前位置的怪物造成伤害,那么我们就可以把该位置上的子弹数从plus中减去。这样我们就实现了计算有效子弹个数的想法。
完成了公式内所有未知数的求值工作,接下来我们就该维护num的值了。当我们求出总伤害(allhurt)之后,就可以计算在当前位置放置的子弹个数了。这里我们进行分类讨论:
如果当前造成的总伤害已经足够让这个位置上的怪物死亡,那么我们不需要更新num值。
如果当前造成的总伤害不能让这个位置上的怪物死亡或者恰好让该怪物的Hp值为0(为0是不代表死亡的,还活着~),那么我们需要在该位置上放置子弹。并且显然,需要放置的子弹个数为:
- Num[i]=(Hp[i]-allhurt)/p+1
为什么加1呢?因为当Hp恰好等于0的时候怪物还没有死呀qwq~
同时我们定义一个计算总子弹放置数的变量cnt,每次更新完num值后将每个num值累加起来。
因为我们要求的是用k发子弹消灭所有怪物的最小威力值p,所以如果在当前回合的枚举中我们求得的cnt大于要求子弹个数k,我们便对p的范围向左二分,反之向右。
这样我们的判断函数就写好了。
主函数没有什么特别需要强调的,需要的只是输入和简单的二分操作,这里就不再详述。
最后一定要注意的是,每次判断时要对存储计算结果的变量进行清零。(转自Merak)
式子
我们设对于二分出的p,射到从左到右第s个怪物身上的子弹数为kk[s]。对于p-(i-j)*(i-j)我们将它拆开,变成-j^2+2ij-i^2+p。对于第j个怪,它受到的溅射伤害应为-(kk[j+1]+...+kk[n])*j^2+2*(kk[j+1]*(j+1)+...+kk[n]*n)*j-(kk[j+1]*(j+1)^2+...+kk[n]*n^2)+(kk[j+1]+...+kk[n])*p,相信看了这个大家就能理解了。
#include<iostream> #include<cstdio> #include<cstring> #include<cmath> using namespace std; typedef long long ll; ll a[501000],k,num[540010],n; bool check(ll mid) { ll t=(ll)sqrt(mid)+1; ll sum=0; memset(num,0,sizeof(num)); ll z=0,i=0,i1=0; for(int j=n;j>=1;j--) { if(num[j+1]) { z+=num[j+1]; i+=(num[j+1]*(j+1)); i1+=(num[j+1]*(j+1)*(j+1)); } if(j+t<=n&&(num[j+t])) { z-=num[j+t]; i-=(num[j+t]*(j+t)); i1-=(num[j+t]*(j+t)*(j+t)); } ll all=1ll*mid*z-1ll*j*j*z+2ll*i*j-i1; if(a[j]>=all) { num[j]=(a[j]-all)/mid+1; sum+=num[j]; } if(sum>k) return 0; } return 1; } int main() { cin>>n>>k; for(int i=1;i<=n;i++) scanf("%lld",&a[i]); ll l=0,r=1e16; while(l<=r) { ll mid=(l+r)/2; if(check(mid)) r=mid-1; else l=mid+1; } cout<<r+1; }