妈妈,世界是一个巨大的脑叶公司 - 题解
标签与难度
标签: 模拟, 数据结构, set, 优先队列, 懒更新, 差分思想 难度: 1800
题目大意喵~
主管大人,你好喵~!这次我们要面对一个血量为 的异想体。我们手下有 位勇敢的员工,每位员工 都有自己的攻击力 和最大血量 。
一开始,所有 位员工都在战场上与异想体战斗。接下来会发生 个事件,我们需要按顺序处理。事件有四种类型:
- 员工入场 (1 x): 编号为 的员工加入战斗。这位员工会以满血状态回归。
- 员工离场 (2 x): 编号为 的员工离开战斗。
- 异想体AOE (3 y): 异想体对所有在场员工造成 点伤害。
- 主管治疗 (4 x h): 治疗编号为 的员工 点血量,但不会超过他的最大血量。
在每个事件处理完毕后,所有在场且存活(血量 > 0)的员工会一起攻击异想体,造成等于他们攻击力总和的伤害。
我们的任务是判断能否在 个事件内(或之内)击败异想体(使其血量 )。如果可以,输出 "YES" 和战斗结束时存活的员工总数。如果 个事件结束后异想体仍然存活,或者所有员工都阵亡了,就输出 "NO"。
特别注意:一旦异想体被击败或我方全员阵亡,就要立刻停止处理后续事件并输出结果哦!
解题思路分析
喵哈~!这道题看起来像一个复杂的模拟题,特别是那个全体攻击(AOE伤害)的操作,如果处理不好,肯定会超时的说。让我们一步一步来分析吧!
朴素的想法和它的瓶颈
最直接的想法就是,我们维护每个员工的当前血量。
- 当员工入场/离场时,我们更新一个“在场员工”列表。
- 当治疗时,我们直接增加对应员工的血量。
- 当异想体发动AOE攻击时,我们就遍历整个“在场员工”列表,给每个人的血量都减去伤害值
y。
这个思路看起来很简单,但是问题出在第三步,喵~。如果战场上有非常多的员工(比如 个),而异想体又频繁地使用AOE攻击,那么每次AOE我们都要进行 次操作。在最坏的情况下,总的时间复杂度会达到 ,这对于 都是 级别的数据来说,是绝对无法接受的,肯定会超时的!
懒惰的我想出懒惰的办法!
既然一个个给员工减血太慢了,那我们能不能换个思路呢?异想体的AOE伤害是对所有在场员工生效的,这是一个“全局”效果。对于这种全局的增减,我们可以使用一种叫做“懒更新”或者“差分思想”的技巧,喵!
我们不记录每个员工被扣了多少血,而是记录一个全局的累积伤害值 total_aoe_damage。
- 每当异想体造成
y点AOE伤害时,我们不碰任何员工,只是简单地让total_aoe_damage += y。
那么,一个员工的真实血量怎么计算呢? 假设员工 i 的最大血量是 ,他在 total_aoe_damage 为 时进入(或重置)战场。那么在任意时刻,当累积伤害为 total_aoe_damage 时,他受到的总伤害就是 total_aoe_damage - D_{entry}。所以他的当前血量就是:
一个员工会阵亡,当他的 时,也就是: 整理一下这个不等式,就变成了:
看!这个 b_i + D_{entry} 是一个只在员工入场或被治疗时才会改变的值。我们可以把它看作是每个员工的“死亡临界点”或者“虚拟血量”。当全局累积伤害超过了这个值,员工就倒下了。
用什么数据结构来管理呢?
现在,我们的问题转化为了:
- 维护一个在场员工的集合。
- 当全局累积伤害增加时,需要快速找出所有“虚拟血量”小于等于当前累积伤害的员工,让他们离场。
为了能“快速找出”虚拟血量最低的员工,一个能自动排序的数据结构就是我们的好朋友啦!std::set 或者 std::priority_queue (小顶堆) 都非常适合。这里我们用 std::set<std::pair<long long, int>>,其中 pair 存储 {虚拟血量, 员工编号}。set 会自动按照虚拟血量从小到大排序。
这样,每次AOE之后,我们只需要不断检查 set 的第一个元素(set.begin()),也就是虚拟血量最低的员工。如果他的虚拟血量小于等于 total_aoe_damage,就说明他阵亡了,我们将他从 set 中移除,更新总攻击力,并减少存活员工计数。我们重复这个过程,直到 set 顶端的员工也能存活为止。
具体操作实现
初始化:
- 所有员工都在场,
total_aoe_damage为 0。 - 对于员工
i,他的初始虚拟血量就是b_i + 0 = b_i。 - 将所有员工
{b_i, i}加入set。 - 维护一个
survivor_count初始为n,一个total_attack_power为所有员工攻击力之和。
- 所有员工都在场,
操作 1 (入场
x):- 员工
x以满血状态回归战场。此时的“入场时累积伤害”就是当前的total_aoe_damage。 - 他的新虚拟血量是
b_x + total_aoe_damage。 - 我们将
{新虚拟血量, x}插入set,并把他的攻击力a_x加回到total_attack_power。
- 员工
操作 2 (离场
x):- 我们需要从
set中移除员工x。为了找到他,我们需要知道他当前的虚拟血量。所以我们需要一个数组employee_virtual_hp来记录每个员工的虚拟血量。 - 从
set中移除{employee_virtual_hp[x], x},并从total_attack_power中减去a_x。
- 我们需要从
操作 3 (AOE
y):total_aoe_damage += y。- 循环检查
set.begin(),处理所有阵亡的员工。
操作 4 (治疗
xh):- 首先,像离场操作一样,将员工
x从set中移除。 - 治疗
h血量,相当于他的虚拟血量增加了h。新的虚拟血量是employee_virtual_hp[x] + h。 - 但是,血量不能超过上限 。满血状态对应的虚拟血量是
b_x + total_aoe_damage。 - 所以,更新后的虚拟血量是
min(employee_virtual_hp[x] + h, b_x + total_aoe_damage)。 - 更新
employee_virtual_hp[x]并将新{虚拟血量, x}插回set。
- 首先,像离场操作一样,将员工
存活员工计数: 最简单的方法是维护一个变量
survivor_count,初始为n。只有当员工因AOE伤害阵亡时,才将这个计数减一。员工主动离场(操作2)并不会减少这个总存活数,因为他们只是撤退了,并没有阵亡,喵~
这样一来,我们就把一个可能很慢的模拟问题,变成了一个高效的数据结构维护问题啦!
代码实现
#include <iostream>
#include <vector>
#include <numeric>
#include <set>
#include <algorithm>
// 为了方便,我们定义一个结构体来表示员工在 set 中的状态
struct Employee {
long long virtual_hp; // 员工的虚拟血量 (b_i + damage_at_entry)
int id; // 员工编号
// 自定义比较函数,让 set 按照 virtual_hp 排序
// 如果 virtual_hp 相同,则按 id 排序,以区分不同员工
bool operator<(const Employee& other) const {
if (virtual_hp != other.virtual_hp) {
return virtual_hp < other.virtual_hp;
}
return id < other.id;
}
};
int main() {
// 使用 std::ios::sync_with_stdio(false) 和 cin.tie(nullptr) 加速输入输出,喵~
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n; // 员工数量
long long m; // 异想体血量
int k; // 事件数量
std::cin >> n >> m >> k;
std::vector<int> attack(n + 1);
std::vector<int> max_hp(n + 1);
std::vector<long long> employee_virtual_hp(n + 1); // 记录每个员工当前的虚拟血量
long long total_attack_power = 0;
for (int i = 1; i <= n; ++i) {
std::cin >> attack[i];
total_attack_power += attack[i];
}
for (int i = 1; i <= n; ++i) {
std::cin >> max_hp[i];
}
std::set<Employee> active_employees;
long long total_aoe_damage = 0;
int survivor_count = n;
// 初始化:所有员工都在战场上
for (int i = 1; i <= n; ++i) {
employee_virtual_hp[i] = max_hp[i]; // 初始累积伤害为0
active_employees.insert({employee_virtual_hp[i], i});
}
bool battle_over = false;
for (int event_i = 0; event_i < k; ++event_i) {
int op_type;
std::cin >> op_type;
if (op_type == 1) { // 员工入场
int x;
std::cin >> x;
total_attack_power += attack[x];
employee_virtual_hp[x] = max_hp[x] + total_aoe_damage;
active_employees.insert({employee_virtual_hp[x], x});
} else if (op_type == 2) { // 员工离场
int x;
std::cin >> x;
total_attack_power -= attack[x];
active_employees.erase({employee_virtual_hp[x], x});
} else if (op_type == 3) { // 异想体AOE
int y;
std::cin >> y;
total_aoe_damage += y;
while (!active_employees.empty() && active_employees.begin()->virtual_hp <= total_aoe_damage) {
Employee dead_employee = *active_employees.begin();
active_employees.erase(active_employees.begin());
total_attack_power -= attack[dead_employee.id];
survivor_count--;
}
} else { // 主管治疗
int x, h;
std::cin >> x >> h;
// 只有在场的员工才能被治疗
if (active_employees.count({employee_virtual_hp[x], x})) {
active_employees.erase({employee_virtual_hp[x], x});
long long healed_virtual_hp = employee_virtual_hp[x] + h;
long long max_virtual_hp = (long long)max_hp[x] + total_aoe_damage;
employee_virtual_hp[x] = std::min(healed_virtual_hp, max_virtual_hp);
active_employees.insert({employee_virtual_hp[x], x});
}
}
// 事件结束后,在场员工发动攻击
m -= total_attack_power;
// 检查战斗是否结束
if (m <= 0) {
std::cout << "YES\n";
std::cout << survivor_count << "\n";
battle_over = true;
break;
}
if (survivor_count == 0) { // 在这里检查是因为AOE可能导致全员阵亡
std::cout << "NO\n";
battle_over = true;
break;
}
}
// k轮事件结束后,如果战斗还未结束
if (!battle_over) {
if (m <= 0) { // 可能最后一击恰好击败
std::cout << "YES\n";
std::cout << survivor_count << "\n";
} else {
std::cout << "NO\n";
}
}
return 0;
}复杂度分析
时间复杂度:
- 初始化时,我们将 个员工插入
set,耗时 。 - 在主循环中,我们处理 个事件。
- 对于操作1、2、4,都涉及到对 set 的一次插入或删除,单次操作复杂度为 ,其中 是 set 的当前大小,,所以是 。
- 对于操作3,
while循环看起来可能会执行很多次。但是,每个员工一生只会被因伤害而移除出set一次。所以,在所有 次事件中,while循环的总执行次数不会超过 。因此,所有操作3中处理阵亡员工的总时间复杂度是 (摊还分析)。 - 综上,总时间复杂度为 。
- 初始化时,我们将 个员工插入
空间复杂度:
- 我们需要存储每个员工的攻击力、最大血量和虚拟血量,这需要 的空间。
set最多存储 个员工,也需要 的空间。- 所以总的空间复杂度是 ,非常优秀的说!
知识点总结
这道题是主管大人对我们的考验,也是一次学习的好机会呢,喵~
- 懒更新/差分思想: 这是本题的核心!当遇到对一个区间或集合进行统一的增减操作时,不要傻乎乎地一个个去修改。维护一个全局的“懒惰标记”(比如本题的
total_aoe_damage),将修改操作的成本从 降为 ,然后在需要精确值的时候再通过标记来计算。 std::set作为动态排序集合:set能够自动维护其中元素的有序性,并支持高效的插入、删除和查找最小/最大元素的操作(都是对数时间复杂度)。这使它成为实现“快速找到最脆弱员工”这一需求的不二之选。当然,用std::priority_queue也能达到类似的效果。- 摊还分析: 对于操作3中的
while循环,虽然单次看起来可能很慢,但从整个算法的生命周期来看,每个元素最多进出一次,总成本是可控的。理解摊还分析能帮助我们更准确地评估算法的真实效率。
希望这篇题解能帮助到你,主管大人!只要我们开动脑筋,再强大的异想体也能被我们镇压的,喵~!