洛谷 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。

样例输入 

3 1
1 4 5

样例输出

6

注释

对于30%的数据,n<=300。
对于100%的数据,n<=500000,k<=500000,1<=mi<=10^10



一看到这个题就想到了首先肯定打最右面的 打死一个向前走再打最后没死的。。。丧心病狂

然后伤害 就想到了二分

二分很简单 但是验证不简单

由于伤害向左溅射

一开始枚举每一个打的 然后将伤害向左溅射一次  时间复杂度约 N^2 * log N

然后只能过3个点


看题解上说的O(n)验证很厉害啊

节省时间就直接抄过来吧。。。


容易注意到随着p的增大,所需要的子弹数单调递减。于是我们便会想到二分答案,但这并不是难点,本题的难点在于:对于一个二分出的p,如何快速计算出它所需要的子弹数?
由于本题中子弹的溅射伤害只是向左,故很容易想到子弹应从右打起,打到怪物被消灭为止。但如果朴素计算溅射伤害,复杂度可能高达O(n^2),无法通过本题。所以我们需要一个更优的计算方法。
我们设对于二分出的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。
这其实是把溅射伤害的值表示为一个二次函数。而二次函数每一项的系数都是可累加的,溅射伤害可以随着循环算出。而当p-(i-j)*(i-j)<0时,我们便需要把对应的i从系数中减去。每一位至多只会被减去一次,故总复杂度仍不变,对于一个二分出的p,计算出它所需要的子弹数的时间复杂度仍为O(n)



#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
long long s[500011];
int use[500011];
int m,n,a,b,c;
long long l,r,mid,ans;
long long lim;
long long life;
long long T1,T2,T3,used;

inline bool ok()
{
    long long a;
    memset(use,0,sizeof(use));
    used=0;T1=0,T2=0,T3=0;
    for(a=m;a>=1;a--)
    {
        if(use[a+1])
        {
            T1+=use[a+1];
            T2+=use[a+1]*(a+1);
            T3+=use[a+1]*(a+1)*(a+1);
        }
        if(a+lim<=m&&use[a+lim])
        {
            T1-=use[a+lim];
            T2-=use[a+lim]*(a+lim);
            T3-=use[a+lim]*(a+lim)*(a+lim);
        }
        life=mid*T1-a*a*T1+2*a*T2-T3;

        if(s[a]>=life)
        {
            use[a]=(s[a]-life)/mid+1;
            used=used+use[a];
        }
        if(used>n)return false;
    }
    return true;
}

int main()
{
    scanf("%d%d",&m,&n);
    for(a=1;a<=m;a++)
    {
        scanf("%d",&s[a]);
        r+=s[a];
    }
    l=s[m]/n;
    r=r/n+1;
    while(l<=r)
    {
        mid=(l+r)>>1;
        lim=(long long)sqrt(mid)+1;
        if(ok())
        {
            ans=mid;
            r=mid-1;
        }
        else
        l=mid+1;
    }
    cout<<ans<<endl;
    return 0;
}




一开始写的渣N^2的验证

bool ok()
{
    int a,b;
    long long dec;
    memset(hurt,0,sizeof(hurt));
    memset(use,0,sizeof(use));
    int used=0;
    for(a=m;a>=1;a--)
    {
        if(s[a]>=hurt[a])use[a]=(s[a]-hurt[a])/mid+1;
        used=used+use[a];
        if(used>n)return false;
        for(b=a-1;b>=1;b--)
        {
            dec=mid-d(a-b);
            if(dec<=0)break;
            hurt[b]=hurt[b]+dec*use[a];
        }
    }
    return true;
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值