题目链接:https://vjudge.net/problem/UVALive-5012
因为题目的答案有单调递增的特点,所以很容易想到用二分答案来做,难得地方就是check函数要如何写,也就是说,如果给你一个p,要如何在规定的时间范围内判断这个p能否满足题目的要求(摧毁所有的石头)。
因为只能向左丢球,所以丢球的顺序肯定是从右向左丢的,比如说最右边的石头要想摧毁的话,你只能站在这个石头上向左丢球,如果一次没摧毁,就只能再丢一个球,直到摧毁这颗石头,然后再左一步,继续摧毁最右边的石头,只有这种方法。
因为p值会逐渐递减,递减的量和 i 与 j 的距离有关,能想到最暴力的方法就是从右向左丢球,每次把一个区间的石头减去对应的值,但是这种方法是会超时的。因为每一个石头都会受之前丢过的球的影响,会减去一些生命值,而这个值跟 i 有关,所以我们必须要想办法在不用知道 i 的情况下,也能求出我们想要的值。
我们假设 len为 第 j 个石头与第 i 个石头的距离。
当i == j 的时候,显然len == 0。这个时候p是没有衰减的,也就是伤害仍然是 p。
当 j == i+1的时候,这个时候 p会衰减 (len+1)^2,也就是伤害是 p - (len-1)^2。
假设 len == 1,那么当前的衰减值,对于下一个石头而言有 (len+1)^2 - len^2 = 2*len+1。
通过上一个石头的衰减值,我们能通过 2*len+1 这个公式,在不用知道 i 的情况下得到球到达当前石头的衰减值,而且这个公式是能够叠加的。
我们把上面的公式写成 2*len+kk,这个公式的叠加规律是,当只有一个球的时候,kk = 1, 每次向左一个石头,衰减值就是2*(len+1) + 1,当有两个球的时候,kk = 2,每次向左一个石头,衰减值就是 2*(len+2) + 2。
用 hit记录对石头的总伤害,每次向左遍历就减去衰减值。
因为每一个球都会有衰减到无的情况,所以我们还需要考虑当球衰减到无的时候,就丢弃这个球。
我在处理丢弃球的这部分问题的时候,卡了很久,思路挺乱的。
每次加入一个球的时候,就往队列里面存 i 这个值,当 p-(i-j)*(i-j) < 0的时候,就说明p到 j这个位置就衰减完了,这个时候的hit要么加上衰减多了的值,要么就减去上一次衰减完还剩下的值,这里代码里面选择的是后者。
在弹队列的时候不用担心会超时,咋一看弹队列好像是N^2的时间复杂度,实际上只有N而已。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <set>
#include <queue>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int Maxn = 5e4+10;
ll a[Maxn], N, K;
bool ok(ll p) {
queue<int> qu;
ll kk = 0, dis = 0, k = K, tmp, hit = 0;
for(int i = N-1; i >= 0; --i) {
while(!qu.empty()) {
tmp = qu.front();
if((tmp-i)*(tmp-i) <= p) break;
hit -= (p-(tmp-i-1)*(tmp-i-1));
dis -= (tmp-i-1);
kk--;
qu.pop();
}
hit -= 2*dis+kk;
dis += kk;
while(hit <= a[i]) {
if(k <= 0) return false;
hit += p;
k--;
kk++;
qu.push(i);
}
}
return true;
}
int main(void)
{
int T;
scanf("%d", &T);
while(T--) {
scanf("%lld%lld", &N, &K);
for(int i = 0; i < N; ++i) scanf("%d", &a[i]);
ll R = 1e18, L = 1;
while(L < R) {
ll mid = (L+R)/2;
if(ok(mid)) R = mid;
else L = mid+1;
}
printf("%lld\n", R);
}
return 0;
}