E. Monsters (hard version)
E. Monsters (hard version)
This is the hard version of the problem. In this version, you need to find the answer for every prefix of the monster array.
In a computer game, you are fighting against monsters. Monster number has health points, all are integers. A monster is alive while it has at least health point.
You can cast spells of two types:
- Deal damage to any single alive monster of your choice.
- Deal damage to all alive monsters. If at least one monster dies (ends up with health points) as a result of this action, then repeat it (and keep repeating while at least one monster dies every time).
Dealing damage to a monster reduces its health by .
Spells of type 1 can be cast any number of times, while a spell of type 2 can be cast at most once during the game.
For every , answer the following question. Suppose that only the first monsters, with numbers , are present in the game. What is the smallest number of times you need to cast spells of type 1 to kill all monsters?
Input
Each test contains multiple test cases. The first line contains the number of test cases (). The description of the test cases follows.
Each test case consists of two lines. The first line contains a single integer () — the number of monsters.
The second line contains integers () — monsters' health points.
It is guaranteed that the sum of over all test cases does not exceed .
Output
For each test case, print integers. The -th of these integers must be equal to the smallest number of times you need to cast spells of type 1 to kill all monsters, if only monsters with numbers are present in the game.
Example
input
2 3 3 1 2 6 4 1 5 4 1 1
output
2 1 0 3 2 4 4 4 4
Note
In the first test case, for , the initial health points of the monsters are . It is enough to cast a spell of type 2:
- Monsters' health points change to . Since monster number dies, the spell is repeated.
- Monsters' health points change to . Since monster number dies, the spell is repeated.
- Monsters' health points change to . Since monster number dies, the spell is repeated.
- Monsters' health points change to .
Since it is possible to use no spells of type 1 at all, the answer is .
In the second test case, for , the initial health points of the monsters are . Here is one of the optimal action sequences:
- Using a spell of type 1, deal damage to monster number . Monsters' health points change to .
- Using a spell of type 1, deal damage to monster number . Monsters' health points change to .
- Using a spell of type 1, deal damage to monster number again. Monsters' health points change to .
- Use a spell of type 2:
- Monsters' health points change to . Since monsters number , , and die, the spell is repeated.
- Monsters' health points change to . Since monster number dies, the spell is repeated.
- Monsters' health points change to . Since monster number dies, the spell is repeated.
- Monsters' health points change to .
- Using a spell of type 1, deal damage to monster number . Monsters' health points change to .
Spells of type 1 are cast times in total. It can be shown that this is the smallest possible number.
解题思路
先说简单版C. Monsters (easy version)的做法。
在所有的选择方案中,对于在第二种攻击之后存在若干次第一种攻击的方案,我们总是可以把剩下的第一种攻击提前到第二种攻击之前。对于第二次攻击结束后还剩下血量的怪物,只能使用第一种攻击将其变成,而如果我们在第二种攻击之前提前对这些还剩血量的怪物使用剩下的第一种攻击,由于第二种攻击对所有怪兽造成的伤害不变,所以在第二种攻击后这些怪物的血量就会变成,并且第一种攻击的次数不会变多。
所以现在我们要如何安排第一次攻击,使得最后的第二种攻击能够一次性消灭所有的怪物?由于攻击与怪物的排列顺序无关,因此假设现在数组(怪物血量)是升序排序的,通过若干次的第一种攻击后变成数组(依然升序)。很明显如果想让第二种攻击一次性将数组清零,那么中任意相邻的两个元素的差值不能超过,因为如果存在某个相邻的元素(假设是满足这个条件的最小的),那么当被消灭,前面的怪兽血量都已经清零,并且剩下的怪兽(之后)都已经损失了的血量,此时是所有剩下的怪物中血量最小的,即还剩下的血量,但这个值严格大于,因此第二种攻击被中断,从而无法一次性消灭所有的怪物。
因此中任意两个元素之间的差值不能超过,可以等于也可以等于。这就启示我们可以遍历数组(数组已升序排序),对于中的第个元素,考虑中的第个元素,如果(假设),那么就要对至少进行次第一种攻击,根据贪心思想我们就取,并且有。
C. Monsters (easy version)的AC代码如下:
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 typedef long long LL; 5 6 const int N = 2e5 + 10; 7 8 int a[N]; 9 10 void solve() { 11 int n; 12 scanf("%d", &n); 13 for (int i = 1; i <= n; i++) { 14 scanf("%d", a + i); 15 } 16 sort(a + 1, a + n + 1); 17 LL ret = 0; 18 for (int i = 1; i <= n; i++) { 19 if (a[i] != a[i - 1]) { 20 ret += a[i] - a[i - 1] - 1; 21 a[i] = a[i - 1] + 1; 22 } 23 } 24 printf("%lld\n", ret); 25 } 26 27 int main() { 28 int t; 29 scanf("%d", &t); 30 while (t--) { 31 solve(); 32 } 33 34 return 0; 35 }
接下来是hard版本的,其实还是有用到easy版的思想的。hard版本就是求数组的每一个前缀使用第一种攻击的最少次数是多少(easy版就是求整个数组使用的第一种攻击最少是多少)。
在easy版本中数组可能会存在重复元素,比如有,根据上面的算法得到的,就会有重复的数字和,那么最小代价就是。即最小代价为。
而如果中没有重复的元素出现,那么必然是的形式(即),那么最小代价就是。因此如果知道从得到没有重复元素,那么代价就可以通过公式直接求出来而无需遍历来求。
之所以要说这个是因为中从第个开始的重复元素是不会产生代价的,这意味着对应的。这是因为中相邻两个重复元素的差值为,而根据构造的贪心原则(只要,那么),两个相邻元素的差值最大为,而只有相邻元素相同即,才会有。因此在中,把中重复的元素所对应的数删除,并不会影响最小代价的结果。
例如,,和都是重复元素,那么在中把对应的下标删除得到,有,代价为。
因此为了求每个前缀的最小代价,每次加入一个新的元素,我们需要维护一个没有重复元素的以及对应的,这就是解决这题的主要思想。可是关键的地方并不是求,而是判断每次加入的元素是否会使得出现重复的元素,以及需要在中删除哪个元素来保证没有重复的元素出现。前面之所以会讲得这么啰嗦是为了方便读者看得懂下面要说明的解决这个问题的方法,因为我第一次看别人的题解看不懂所以希望记录的详细些。
已知和的代价是一样的,那么呢?,实际上还是一样的,画个值域图:
其实可以发现,只要中数值为的个数超过个,不管加入再多的(即红色部分)都不会改变代价,也就是说这些多出的是应该被删除的。因为中相邻的两个数相差最多为,因此在值域不超过的所有数里(即绿色框内的数),只要数的个数超过那么必然至少存在相邻两个数的差值为,即存在值相同的数。更一般的,如果数值小于等于的数的个数超过个,那么总是可以删除多余的数并使得代价不变。
再举例子,红色的数表示可以删除的数(任何不超过的数都不多于个):
假设数值为的数有个,对求前缀和那么就得到,表示所有数值不超过的数的个数。因此我们要保证在的每个前缀中,对于所有的(这里的是数值不是下标)都有,即保证任何不超过的数都不多于个,这样才能保证中不会有重复的数出现,才能用公式直接求出前缀的最小代价。
为此,每次加入一个数,我们都要对加上(题目中值域的最大值为),然后检查是否存在某个值(因为每次只增加一个数,因此如果不满足,那么必然就是),如果存在则找到满足这个情况的最小的值,然后删除,最后代入公式求答案即可。
解释一下为什么要找到最小的,因为删除后,在后面比大的数中如果存在的都会变成(因为减少了),参考下图:
因此对于的每个前缀,都需要给一个值域区间加上,同时还需要判断值域是否都满足的情况。然后其实可以等价成为,因此我们可以用线段树来维护每一个。值域区间加上就相当于给这个区间的都加上,然后判断是否存在并找到满足这个条件最小的值就相当于判断区间的最大值是否大于,并通过二分找到最左边满足大于的那个数。这些都可以用线段树来实现,线段树只需要维护区间的最大值即可。
AC代码如下:
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 typedef long long LL; 5 6 const int N = 2e5 + 10; 7 8 int a[N]; 9 struct Node { 10 int l, r, maxv, add; 11 }tr[N * 4]; 12 13 void build(int u, int l, int r) { 14 if (l == r) { 15 tr[u] = {l, r, -l, 0}; 16 } 17 else { 18 int mid = l + r >> 1; 19 build(u << 1, l, mid); 20 build(u << 1 | 1, mid + 1, r); 21 tr[u] = {l, r, max(tr[u << 1].maxv, tr[u << 1 | 1].maxv), 0}; 22 } 23 } 24 25 void pushdown(int u) { 26 if (tr[u].add) { 27 tr[u << 1].add += tr[u].add; 28 tr[u << 1].maxv += tr[u].add; 29 tr[u << 1 | 1].add += tr[u].add; 30 tr[u << 1 | 1].maxv += tr[u].add; 31 tr[u].add = 0; 32 } 33 } 34 35 void modify(int u, int l, int r, int c) { 36 if (tr[u].l >= l && tr[u].r <= r) { 37 tr[u].maxv += c; 38 tr[u].add += c; 39 } 40 else { 41 pushdown(u); 42 int mid = tr[u].l + tr[u].r >> 1; 43 if (l <= mid) modify(u << 1, l, r, c); 44 if (r >= mid + 1) modify(u << 1 | 1, l, r, c); 45 tr[u].maxv = max(tr[u << 1].maxv, tr[u << 1 | 1].maxv); 46 } 47 } 48 49 int query(int u) { 50 if (tr[u].l == tr[u].r) return tr[u].l; 51 pushdown(u); 52 if (tr[u << 1].maxv > 0) return query(u << 1); // 左边存在大于0的值,那么取左边找 53 return query(u << 1 | 1); // 大于0的值在右边 54 } 55 56 void solve() { 57 int n; 58 scanf("%d", &n); 59 for (int i = 1; i <= n; i++) { 60 scanf("%d", a + i); 61 } 62 build(1, 1, n); 63 int cnt = 0; // 当前维护的数的个数 64 LL s = 0; // 当前维护的数的总和 65 for (int i = 1; i <= n; i++) { 66 cnt++, s += a[i]; // 先记录 67 modify(1, a[i], n, 1); // a[i]的个数加1,si ~ sn都加1,si-i ~ sn-n都加1 68 if (tr[1].maxv > 0) { // 存在si-i大于0的数,需要删除最小的那个数 69 int x = query(1); // 找到最小的那个数 70 modify(1, x, n, -1); // 删除这个数,区间减1 71 cnt--, s -= x; // 删除最小的那个数 72 } 73 printf("%lld ", s - cnt * (cnt + 1ll) / 2); // 公式计算答案 74 } 75 printf("\n"); 76 } 77 78 int main() { 79 int t; 80 scanf("%d", &t); 81 while (t--) { 82 solve(); 83 } 84 85 return 0; 86 }
参考资料
Codeforces Round #850 (Div. 2) E(二分线段树):https://zhuanlan.zhihu.com/p/603794263
By MilimNava, contest: Codeforces Round #850 (Div. 1, based on VK Cup 2022 - Final Round), problem: (C) Monsters (hard version):https://codeforces.com/contest/1785/submission/192299527
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/17099855.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效