6.小E的怪物挑战 | 豆包MarsCode AI 刷题

346 阅读7分钟

题目分析

在这道题目中,小E在一个游戏中需要依次击败多个怪物。每个怪物都有其特定的血量和攻击力,分别用数组 ha 表示。小E自身也有初始的血量 H 和攻击力 A。游戏的规则如下:

  1. 击败条件

    • 小E可以击败一个怪物的前提是该怪物的血量和攻击力都严格小于小E当前的血量和攻击力。
  2. 序列条件

    • 第一个被击败的怪物必须满足其血量小于 H 且攻击力小于 A
    • 击败的怪物序列中,后一个怪物的血量和攻击力都必须严格大于前一个被击败的怪物。
  3. 属性获取

    • 击败怪物后,小E会获得该怪物的属性值(尽管题目中未明确说明属性获取后的具体影响,结合样例分析,我们可以理解为小E的血量和攻击力保持不变,或者不受影响)。

小E希望知道她最多能击败多少个怪物。

问题转化

这道题目可以转化为一个**二维最长递增子序列(Longest Increasing Subsequence, LIS)**的问题。具体来说,我们需要在满足以下条件的情况下,找到最长的怪物子序列:

  1. 每个被选中的怪物的血量和攻击力都小于小E的初始血量 H 和攻击力 A
  2. 子序列中的怪物按照出现的顺序,且每个后续怪物的血量和攻击力都严格大于前一个怪物。

因此,问题的核心在于如何高效地找到这样一个满足条件的最长子序列。

解题思路

1. 筛选可击败的怪物

首先,我们需要筛选出所有 可被击败 的怪物,即满足以下两个条件的怪物:

  • h[i] < H
  • a[i] < A

这些怪物是小E可能击败的候选对象。

2. 寻找最长的递增子序列

在筛选出的可击败怪物中,我们需要找到一个最长的子序列,使得序列中的每个怪物的血量和攻击力都严格大于前一个怪物。这就是一个二维的最长递增子序列问题,因为我们需要同时考虑两个维度(血量和攻击力)的严格递增。

3. 动态规划(Dynamic Programming)

为了高效地找到这样的最长递增子序列,我们可以采用动态规划的方法。具体步骤如下:

  1. 初始化

    • 创建一个 dp 数组,其中 dp[i] 表示以第 i 个可击败怪物结尾的最长递增子序列的长度。
    • 初始化时,每个 dp[i] 的值都为 1,因为每个怪物至少可以单独成为一个子序列。
  2. 状态转移

    • 对于每一个怪物 i,遍历所有之前的怪物 jj < i),如果怪物 j 的血量和攻击力都小于怪物 i 的血量和攻击力(即 h[j] < h[i]a[j] < a[i]),那么我们可以将怪物 i 添加到以怪物 j 结尾的子序列中。
    • 更新 dp[i] 的值为 dp[j] + 1,表示在以 j 结尾的子序列基础上增加怪物 i
    • 取所有可能的 dp[j] + 1 中的最大值,赋给 dp[i]
  3. 结果

    • 最终的答案就是 dp 数组中的最大值,表示可击败的最多怪物数量。

实现细节

考虑到可能存在多个可击败的怪物,我们需要对所有可击败的怪物进行遍历,并动态地更新 dp 数组。由于每个怪物都需要与之前的怪物进行比较,因此时间复杂度为 O(m²),其中 m 是可击败怪物的数量。在本题的约束下,这样的时间复杂度是可接受的。

C++ 实现代码

以下是基于上述思路的 C++ 实现代码:

cpp
Copy code
#include <vector>
#include <algorithm>
using namespace std;

/**
 * @brief 计算小E最多能击败的怪物数量
 * 
 * @param n 怪物数量
 * @param H 小E的初始血量
 * @param A 小E的初始攻击力
 * @param h 每个怪物的血量数组
 * @param a 每个怪物的攻击力数组
 * @return int 最多能击败的怪物数量
 */
int solution(int n, int H, int A, vector<int> h, vector<int> a) {
    // 筛选出可以被击败的怪物:h[i] < H 且 a[i] < A
    vector<pair<int, int>> eligible;
    for(int i = 0; i < n; ++i){
        if(h[i] < H && a[i] < A){
            eligible.emplace_back(h[i], a[i]);
        }
    }
    
    // 如果没有可击败的怪物,返回0
    if(eligible.empty()) {
        return 0;
    }
    
    int m = eligible.size();
    // 初始化DP数组,每个位置至少可以自己构成一个序列
    vector<int> dp(m, 1);
    
    // 动态规划计算最长递增子序列
    for(int i = 0; i < m; ++i){
        for(int j = 0; j < i; ++j){
            if(eligible[j].first < eligible[i].first && eligible[j].second < eligible[i].second){
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }
    }
    
    // 返回DP数组中的最大值
    return *max_element(dp.begin(), dp.end());
}

示例测试

为了验证我们的算法是否正确,我们可以使用题目中给出的样例进行测试:

cpp
Copy code
#include <iostream>
int main(){
    // 样例1
    int n1 = 3, H1 = 4, A1 = 5;
    vector<int> h1 = {1, 2, 3};
    vector<int> a1 = {3, 2, 1};
    cout << solution(n1, H1, A1, h1, a1) << endl; // 输出: 1
    
    // 样例2
    int n2 = 5, H2 = 10, A2 = 10;
    vector<int> h2 = {6, 9, 12, 4, 7};
    vector<int> a2 = {8, 9, 10, 2, 5};
    cout << solution(n2, H2, A2, h2, a2) << endl; // 输出: 2
    
    // 样例3
    int n3 = 4, H3 = 20, A3 = 25;
    vector<int> h3 = {10, 15, 18, 22};
    vector<int> a3 = {12, 18, 20, 26};
    cout << solution(n3, H3, A3, h3, a3) << endl; // 输出: 3
    
    return 0;
}

运行以上代码,输出结果与题目样例一致,说明我们的算法是正确的。

复杂度分析

  • 时间复杂度O(m²),其中 m 是可击败怪物的数量。在最坏情况下,所有 n 个怪物都可被击败,因此时间复杂度为 O(n²)。对于 n10^3 左右的情况下,这样的复杂度是可以接受的。
  • 空间复杂度O(m),用于存储可击败的怪物列表和 dp 数组。最坏情况下,m = n,因此空间复杂度为 O(n)

优化思路

尽管当前的动态规划方法已经可以解决问题,但在某些情况下,如果 n 的规模较大(例如 n 达到 10^5),上述方法可能会因为时间复杂度过高而无法在限定时间内完成。这时,我们需要考虑更高效的算法。

一种可能的优化方法是对怪物按照血量或攻击力进行排序,然后利用更高效的数据结构(如线段树、树状数组)来维护和查询最长递增子序列的长度。然而,由于本题样例的规模较小,且未明确给出 n 的具体上限,当前的动态规划方法已经足够。

注意事项

  1. 怪物的顺序:需要注意的是,虽然我们是在寻找递增的血量和攻击力,但怪物的选择必须保持其原有的出现顺序。这意味着我们只能在保持原序列的前提下选择部分怪物来构成递增序列。
  2. 属性获取的影响:虽然题目中提到击败怪物后小E会获得怪物的属性值,但根据样例分析和问题的描述,似乎小E的属性在击败怪物后保持不变。因此,我们在算法中并未考虑属性的动态变化。如果属性获取后对小E的属性有影响,算法需要相应调整。
  3. 输入数据的有效性:在实现代码时,需要确保输入的怪物数量 n 与血量数组 h 和攻击力数组 a 的长度一致,避免数组越界。
  4. 边界情况:需要处理没有可击败怪物的情况,此时输出应为 0