Medium

题目描述

给定一个正整数 hp 和两个正整数数组 damagerequirement(均从 1 开始索引)。

有一个包含 n 个陷阱房间的地牢,房间编号从 1 到 n。进入房间 i 会使你的生命值减少 damage[i]。减少后,如果你的剩余生命值至少为 requirement[i],你就能获得该房间的 1 分。

score(j) 为你以 hp 生命值开始,按顺序进入房间 j, j + 1, …, n 所获得的分数。

返回整数 score(1) + score(2) + ... + score(n),即所有起始房间的分数总和。

注意:你不能跳过房间。即使生命值变为非正数,你也可以完成旅程。

示例 1:

输入:hp = 11, damage = [3,6,7], requirement = [4,2,5]
输出:3
解释:
score(1) = 2, score(2) = 1, score(3) = 0。总分数是 2 + 1 + 0 = 3。

示例 2:

输入:hp = 2, damage = [10000,1], requirement = [1,1]
输出:1
解释:
score(1) = 0, score(2) = 1。总分数是 0 + 1 = 1。

约束条件:

  • 1 <= hp <= 10^9
  • 1 <= n == damage.length == requirement.length <= 10^5
  • 1 <= damage[i], requirement[i] <= 10^4

解题思路

解题思路

这道题要求计算所有可能起始位置的分数总和。直接模拟每个起始位置会导致 O(n²) 的时间复杂度,需要优化。

核心观察

  1. 总的可能得分点数为所有子数组的数量:n * (n + 1) / 2
  2. 我们需要减去那些无法获得分数的情况

关键转换: 对于从位置 i 开始到位置 j 的路径,玩家在房间 j 能获得分数的条件是: hp - (从i到j的总伤害) >= requirement[j]

使用前缀和 pref[i] = damage[0] + damage[1] + ... + damage[i-1],上述条件可以转换为: hp - (pref[j+1] - pref[i]) >= requirement[j] 即:pref[i] >= requirement[j] - hp + pref[j+1]

算法步骤

  1. 计算伤害的前缀和数组
  2. 对每个结束位置 j,统计有多少起始位置 i 无法在房间 j 获得分数
  3. 使用二分查找或有序数据结构高效统计

推荐解法:使用二分查找,时间复杂度 O(n log n)。

代码实现

class Solution {
public:
    long long totalScore(int hp, vector<int>& damage, vector<int>& requirement) {
        int n = damage.size();
        vector<long long> pref(n + 1, 0);
        
        // 计算前缀和
        for (int i = 0; i < n; i++) {
            pref[i + 1] = pref[i] + damage[i];
        }
        
        // 总的可能得分数
        long long total = (long long)n * (n + 1) / 2;
        
        // 对于每个结束位置j,找出无法获得分数的起始位置数量
        for (int j = 0; j < n; j++) {
            long long threshold = requirement[j] - (long long)hp + pref[j + 1];
            // 找到第一个 >= threshold 的前缀和位置
            int count = lower_bound(pref.begin(), pref.begin() + j + 1, threshold) - pref.begin();
            total -= count;
        }
        
        return total;
    }
};
class Solution:
    def totalScore(self, hp: int, damage: List[int], requirement: List[int]) -> int:
        n = len(damage)
        
        # 计算前缀和
        pref = [0]
        for d in damage:
            pref.append(pref[-1] + d)
        
        # 总的可能得分数
        total = n * (n + 1) // 2
        
        # 对于每个结束位置j,找出无法获得分数的起始位置数量
        for j in range(n):
            threshold = requirement[j] - hp + pref[j + 1]
            # 使用二分查找
            import bisect
            count = bisect.bisect_left(pref[:j + 1], threshold)
            total -= count
            
        return total
public class Solution {
    public long TotalScore(int hp, int[] damage, int[] requirement) {
        int n = damage.Length;
        long[] pref = new long[n + 1];
        
        // 计算前缀和
        for (int i = 0; i < n; i++) {
            pref[i + 1] = pref[i] + damage[i];
        }
        
        // 总的可能得分数
        long total = (long)n * (n + 1) / 2;
        
        // 对于每个结束位置j,找出无法获得分数的起始位置数量
        for (int j = 0; j < n; j++) {
            long threshold = requirement[j] - (long)hp + pref[j + 1];
            // 二分查找第一个 >= threshold 的位置
            int count = BinarySearch(pref, 0, j + 1, threshold);
            total -= count;
        }
        
        return total;
    }
    
    private int BinarySearch(long[] arr, int start, int end, long target) {
        int left = start, right = end;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (arr[mid] < target) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return left;
    }
}
var totalScore = function(hp, damage, requirement) {
    const n = damage.length;
    const pref = [0];
    
    // 计算前缀和
    for (let i = 0; i < n; i++) {
        pref.push(pref[pref.length - 1] + damage[i]);
    }
    
    // 总的可能得分数
    let total = n * (n + 1) / 2;
    
    // 对于每个结束位置j,找出无法获得分数的起始位置数量
    for (let j = 0; j < n; j++) {
        const threshold = requirement[j] - hp + pref[j + 1];
        // 二分查找第一个 >= threshold 的位置
        let left = 0, right = j + 1;
        while (left < right) {
            const mid = Math.floor((left + right) / 2);
            if (pref[mid] < threshold) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        total -= left;
    }
    
    return total;
};

复杂度分析

复杂度类型大小
时间复杂度O(n log n)
空间复杂度O(n)

其中 n 是房间数量。时间复杂度主要来自于对每个位置进行二分查找,空间复杂度用于存储前缀和数组。