分组得分最高的所有下标

标签: 数组

难度: Medium

给你一个下标从 0 开始的二进制数组 nums ,数组长度为 nnums 可以按下标 i0 <= i <= n )拆分成两个数组(可能为空):numsleftnumsright

  • numsleft 包含 nums 中从下标 0i - 1 的所有元素(包括 0i - 1,而 numsright 包含 nums 中从下标 in - 1 的所有元素(包括 in - 1 )。
  • 如果 i == 0numsleft ,而 numsright 将包含 nums 中的所有元素。
  • 如果 i == nnumsleft 将包含 nums 中的所有元素,而 numsright

下标 i 分组得分numsleft0 的个数和 numsright1 的个数之

返回 分组得分 最高所有不同下标 。你可以按 任意顺序 返回答案。

示例 1:

输入:nums = [0,0,1,0]
输出:[2,4]
解释:按下标分组
- 0 :numsleft 为 [] 。numsright 为 [0,0,1,0] 。得分为 0 + 1 = 1 。
- 1 :numsleft 为 [0] 。numsright 为 [0,1,0] 。得分为 1 + 1 = 2 。
- 2 :numsleft 为 [0,0] 。numsright 为 [1,0] 。得分为 2 + 1 = 3 。
- 3 :numsleft 为 [0,0,1] 。numsright 为 [0] 。得分为 2 + 0 = 2 。
- 4 :numsleft 为 [0,0,1,0] 。numsright 为 [] 。得分为 3 + 0 = 3 。
下标 2 和 4 都可以得到最高的分组得分 3 。
注意,答案 [4,2] 也被视为正确答案。

示例 2:

输入:nums = [0,0,0]
输出:[3]
解释:按下标分组
- 0 :numsleft 为 [] 。numsright 为 [0,0,0] 。得分为 0 + 0 = 0 。
- 1 :numsleft 为 [0] 。numsright 为 [0,0] 。得分为 1 + 0 = 1 。
- 2 :numsleft 为 [0,0] 。numsright 为 [0] 。得分为 2 + 0 = 2 。
- 3 :numsleft 为 [0,0,0] 。numsright 为 [] 。得分为 3 + 0 = 3 。
只有下标 3 可以得到最高的分组得分 3 。

示例 3:

输入:nums = [1,1]
输出:[0]
解释:按下标分组
- 0 :numsleft 为 [] 。numsright 为 [1,1] 。得分为 0 + 2 = 2 。
- 1 :numsleft 为 [1] 。numsright 为 [1] 。得分为 0 + 1 = 1 。
- 2 :numsleft 为 [1,1] 。numsright 为 [] 。得分为 0 + 0 = 0 。
只有下标 0 可以得到最高的分组得分 2 。

提示:

  • n == nums.length
  • 1 <= n <= 105
  • nums[i]01

Submission

运行时间: 184 ms

内存: 24.8 MB

class Solution:
    def maxScoreIndices(self, nums: List[int]) -> List[int]:
        # best 为历史最大值
        presum = best = 0
        # ans 为历史最大值的下标
        ans = [0]

        for i, num in enumerate(nums):
            if num == 0:
                presum += 1
                if presum > best:
                    best = presum
                    ans = [i + 1]
                elif presum == best:
                    ans.append(i + 1)
            else:
                presum -= 1
        
        return ans

Explain

题解的核心思想是通过一个单次遍历计算每个可能的分割点i的得分。初始化一个前缀和presum来记录当前0的数量减去1的数量。对于每一个遍历到的元素,如果是0,则presum增加1;如果是1,则presum减少1。这样,每到一个新的元素时,presum实际上代表了如果从当前元素开始分割,左边部分0的数量和右边部分1的数量之和。通过比较presum和历史最大值best,可以决定是否更新结果列表ans。如果presum大于best,更新best并重置ans;如果presum等于best,将当前下标加1后添加到ans中。

时间复杂度: O(n)

空间复杂度: O(n)

class Solution:
    def maxScoreIndices(self, nums: List[int]) -> List[int]:
        # 初始化前缀和和历史最大得分
        presum = best = 0
        # 初始化结果列表,初始时假设最大得分在第0个位置
        ans = [0]

        # 遍历数组的每个元素
        for i, num in enumerate(nums):
            # 根据当前元素更新前缀和
            if num == 0:
                presum += 1
            else:
                presum -= 1
            # 判断当前前缀和是否更新了历史最高得分
            if presum > best:
                best = presum
                ans = [i + 1]
            elif presum == best:
                ans.append(i + 1)

        return ans

Explore

使用前缀和(presum)来记录当前0的数量减去1的数量的原因是,这样的处理可以更直接地反映分割点左右两侧的得分差异。如果单独维护0和1的数量,虽然可以计算得分,但需要针对每个可能的分割点分别计算其左侧和右侧的得分,这样的计算不仅繁琐,效率也相对较低。使用presum,我们可以通过单次遍历同时得到每个分割点的得分差异,使得算法更为高效和简洁。

在题解中,presum的计算方式(遇到0增加1,遇到1减少1)实际上帮助我们计算从数组开始到当前元素为止的得分。presum的值表示如果在当前点将数组分割成两部分,左侧得分(即左侧0的数量)和右侧得分(即右侧1的数量)的差。因此,presum的值越大,表示如果在此处分割,左侧0的数量与右侧1的数量之差越大,即分割得分越高。通过跟踪presum的最大值,我们可以找到产生最高得分的所有可能分割点。

在题解中,确实是只有在presum超过当前已知的最大值best时,才会更新best并重新设置ans为当前下标加1。这意味着只有新的最大得分才被考虑,之前达到相同得分best的分割点都不会被重置,因为当presum等于best时,会将当前下标加1添加到ans中,而不是重置。这样可以确保所有达到最高得分的分割点都被记录在ans列表中。

是的,ans列表初始化为[0]确实假设了在数组开始位置(即没有进行任何分割时)就可能有最大得分。这种假设在整个数组中不存在0,只有1的情况下是成立的,因为此时数组左侧(空的)得分为0,而右侧全为1的得分为数组长度,即最大可能得分。因此,考虑数组开始位置作为一个有效的分割点是合理的,以确保能够考虑所有可能的情况。