Skip to content

妈妈,世界是一个巨大的脑叶公司 - 题解

标签与难度

标签: 模拟, 数据结构, set, 优先队列, 懒更新, 差分思想 难度: 1800

题目大意喵~

主管大人,你好喵~!这次我们要面对一个血量为 mm 的异想体。我们手下有 nn 位勇敢的员工,每位员工 ii 都有自己的攻击力 aia_i 和最大血量 bib_i

一开始,所有 nn 位员工都在战场上与异想体战斗。接下来会发生 kk 个事件,我们需要按顺序处理。事件有四种类型:

  1. 员工入场 (1 x): 编号为 xx 的员工加入战斗。这位员工会以满血状态回归。
  2. 员工离场 (2 x): 编号为 xx 的员工离开战斗。
  3. 异想体AOE (3 y): 异想体对所有在场员工造成 yy 点伤害。
  4. 主管治疗 (4 x h): 治疗编号为 xx 的员工 hh 点血量,但不会超过他的最大血量。

在每个事件处理完毕后,所有在场且存活(血量 > 0)的员工会一起攻击异想体,造成等于他们攻击力总和的伤害。

我们的任务是判断能否在 kk 个事件内(或之内)击败异想体(使其血量 0\le 0)。如果可以,输出 "YES" 和战斗结束时存活的员工总数。如果 kk 个事件结束后异想体仍然存活,或者所有员工都阵亡了,就输出 "NO"。

特别注意:一旦异想体被击败或我方全员阵亡,就要立刻停止处理后续事件并输出结果哦!

解题思路分析

喵哈~!这道题看起来像一个复杂的模拟题,特别是那个全体攻击(AOE伤害)的操作,如果处理不好,肯定会超时的说。让我们一步一步来分析吧!

朴素的想法和它的瓶颈

最直接的想法就是,我们维护每个员工的当前血量。

  • 当员工入场/离场时,我们更新一个“在场员工”列表。
  • 当治疗时,我们直接增加对应员工的血量。
  • 当异想体发动AOE攻击时,我们就遍历整个“在场员工”列表,给每个人的血量都减去伤害值 y

这个思路看起来很简单,但是问题出在第三步,喵~。如果战场上有非常多的员工(比如 NN 个),而异想体又频繁地使用AOE攻击,那么每次AOE我们都要进行 O(N)O(N) 次操作。在最坏的情况下,总的时间复杂度会达到 O(NK)O(N \cdot K),这对于 N,KN, K 都是 10510^5 级别的数据来说,是绝对无法接受的,肯定会超时的!

懒惰的我想出懒惰的办法!

既然一个个给员工减血太慢了,那我们能不能换个思路呢?异想体的AOE伤害是对所有在场员工生效的,这是一个“全局”效果。对于这种全局的增减,我们可以使用一种叫做“懒更新”或者“差分思想”的技巧,喵!

我们不记录每个员工被扣了多少血,而是记录一个全局的累积伤害值 total_aoe_damage

  • 每当异想体造成 y 点AOE伤害时,我们不碰任何员工,只是简单地让 total_aoe_damage += y

那么,一个员工的真实血量怎么计算呢? 假设员工 i 的最大血量是 bib_i,他在 total_aoe_damageDentryD_{entry} 时进入(或重置)战场。那么在任意时刻,当累积伤害为 total_aoe_damage 时,他受到的总伤害就是 total_aoe_damage - D_{entry}。所以他的当前血量就是: HPcurrent=bi(totalaoedamageDentry)HP_{current} = b_i - (total_aoe_damage - D_{entry})

一个员工会阵亡,当他的 HPcurrent0HP_{current} \le 0 时,也就是: bi(totalaoedamageDentry)0b_i - (total_aoe_damage - D_{entry}) \le 0 整理一下这个不等式,就变成了: bi+Dentrytotalaoedamageb_i + D_{entry} \le total_aoe_damage

看!这个 b_i + D_{entry} 是一个只在员工入场或被治疗时才会改变的值。我们可以把它看作是每个员工的“死亡临界点”或者“虚拟血量”。当全局累积伤害超过了这个值,员工就倒下了。

用什么数据结构来管理呢?

现在,我们的问题转化为了:

  1. 维护一个在场员工的集合。
  2. 当全局累积伤害增加时,需要快速找出所有“虚拟血量”小于等于当前累积伤害的员工,让他们离场。

为了能“快速找出”虚拟血量最低的员工,一个能自动排序的数据结构就是我们的好朋友啦!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 (治疗 x h):

    • 首先,像离场操作一样,将员工 xset 中移除。
    • 治疗 h 血量,相当于他的虚拟血量增加了 h。新的虚拟血量是 employee_virtual_hp[x] + h
    • 但是,血量不能超过上限 bxb_x。满血状态对应的虚拟血量是 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)并不会减少这个总存活数,因为他们只是撤退了,并没有阵亡,喵~

这样一来,我们就把一个可能很慢的模拟问题,变成了一个高效的数据结构维护问题啦!

代码实现

cpp
#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;
}

复杂度分析

  • 时间复杂度: O(NlogN+KlogN)O(N \log N + K \log N)

    • 初始化时,我们将 NN 个员工插入 set,耗时 O(NlogN)O(N \log N)
    • 在主循环中,我们处理 KK 个事件。
    • 对于操作1、2、4,都涉及到对 set 的一次插入或删除,单次操作复杂度为 O(logS)O(\log S),其中 SS 是 set 的当前大小,SNS \le N,所以是 O(logN)O(\log N)
    • 对于操作3,while 循环看起来可能会执行很多次。但是,每个员工一生只会被因伤害而移除出 set 一次。所以,在所有 KK 次事件中,while 循环的总执行次数不会超过 NN。因此,所有操作3中处理阵亡员工的总时间复杂度是 O(NlogN)O(N \log N)(摊还分析)。
    • 综上,总时间复杂度为 O(NlogN+KlogN)O(N \log N + K \log N)
  • 空间复杂度: O(N)O(N)

    • 我们需要存储每个员工的攻击力、最大血量和虚拟血量,这需要 O(N)O(N) 的空间。
    • set 最多存储 NN 个员工,也需要 O(N)O(N) 的空间。
    • 所以总的空间复杂度是 O(N)O(N),非常优秀的说!

知识点总结

这道题是主管大人对我们的考验,也是一次学习的好机会呢,喵~

  1. 懒更新/差分思想: 这是本题的核心!当遇到对一个区间或集合进行统一的增减操作时,不要傻乎乎地一个个去修改。维护一个全局的“懒惰标记”(比如本题的 total_aoe_damage),将修改操作的成本从 O(N)O(N) 降为 O(1)O(1),然后在需要精确值的时候再通过标记来计算。
  2. std::set 作为动态排序集合: set 能够自动维护其中元素的有序性,并支持高效的插入、删除和查找最小/最大元素的操作(都是对数时间复杂度)。这使它成为实现“快速找到最脆弱员工”这一需求的不二之选。当然,用 std::priority_queue 也能达到类似的效果。
  3. 摊还分析: 对于操作3中的 while 循环,虽然单次看起来可能很慢,但从整个算法的生命周期来看,每个元素最多进出一次,总成本是可控的。理解摊还分析能帮助我们更准确地评估算法的真实效率。

希望这篇题解能帮助到你,主管大人!只要我们开动脑筋,再强大的异想体也能被我们镇压的,喵~!