LeetCode 798. 得分最高的最小轮调

798. 得分最高的最小轮调

给你一个数组 nums,我们可以将它按一个非负整数 k 进行轮调,这样可以使数组变为 [nums[k], nums[k + 1], ... nums[nums.length - 1], nums[0], nums[1], ..., nums[k-1]] 的形式。此后,任何值小于或等于其索引的项都可以记作一分。

  • 例如,数组为 nums = [2,4,1,3,0],我们按 k = 2 进行轮调后,它将变成 [1,3,0,2,4]。这将记为 3 分,因为 1 > 0 [不计分]、3 > 1 [不计分]、0 <= 2 [计 1 分]、2 <= 3 [计 1 分],4 <= 4 [计 1 分]。

在所有可能的轮调中,返回我们所能得到的最高分数对应的轮调下标 k 。如果有多个答案,返回满足条件的最小的下标 k 。

示例 1:

输入:nums = [2,3,1,4,0]
输出:3
解释:
下面列出了每个 k 的得分:
k = 0,  nums = [2,3,1,4,0],    score 2
k = 1,  nums = [3,1,4,0,2],    score 3
k = 2,  nums = [1,4,0,2,3],    score 3
k = 3,  nums = [4,0,2,3,1],    score 4
k = 4,  nums = [0,2,3,1,4],    score 3
所以我们应当选择 k = 3,得分最高。

示例 2:

输入:nums = [1,3,0,2,4]
输出:0
解释:
nums 无论怎么变化总是有 3 分。
所以我们将选择最小的 k,即 0。

提示:

  • 1 <= nums.length <= 10^5
  • 0 <= nums[i] < nums.length

解法1:差分数组

最简单的做法是遍历每个可能的 k,计算轮调 k 个位置之后的数组得分。假设数组的长度是 n,则有 n 种可能的轮调,对于每种轮调都需要 O(n) 的时间计算得分,总时间复杂度是 O(n^2),对于 n≤10^5 的数据范围会超出时间限制,因此需要优化。

对于数组 nums 中的元素 x,当 x 所在下标大于或等于 x 时,元素 x 会记 1 分。因此元素 x 记 1 分的下标范围是 [x,n−1],有 n−x 个下标,元素 x 不计分的下标范围是 [0,x−1],有 x 个下标。

假设元素 x 的初始下标为 i,则当轮调下标为 k 时,元素 x 位于下标 (i−k+n) mod n。如果元素 x 记 1 分,则有 (i−k+n) mod n ≥ x,等价于 k ≤ (i−x+n) mod n。由于元素 x 记 1 分的下标有 n−x 个,因此有 k >= (i - x + n - (n - x) + 1) mod n 即 k ≥ (i+1) mod n。

将取模运算去掉之后,可以得到 k 的实际取值范围:

  • 当 i < x 时,i+1 ≤ k ≤ i−x+n;
  • 当 i ≥ x 时,k ≥ i+1 或 k ≤ i−x。

对于数组 nums 中的每个元素,都可以根据元素值与元素所在下标计算该元素记 1 分的轮调下标范围。

遍历所有元素之后,即可得到每个轮调下标对应的计 1 分的元素个数,计 1 分的元素个数最多的轮调下标即为得分最高的轮调下标。如果存在多个得分最高的轮调下标,则取其中最小的轮调下标。

创建长度为 n 的数组 points,其中 points[k] 表示轮调下标为 k 时的得分。对于数组 nums 中的每个元素,得到该元素记 1 分的轮调下标范围,然后将数组 points 的该下标范围内的所有元素加 1。当数组 points 中的元素值确定后,找到最大元素的最小下标。该做法的时间复杂度仍然是 O(n^2),为了降低时间复杂度,需要利用差分数组。

假设元素 x 的初始下标为 i。

当 i<x 时应将 points 的下标范围 [i+1,i−x+n] 内的所有元素加 1;

当 i≥x 时应将 points 的下标范围 [0,i−x] 和 [i+1,n−1] 内的所有元素加 1。

由于是将一段或两段连续下标范围内的元素加 1,因此可以使用差分数组实现。

定义长度为 n 的差分数组 diff,其中 diff[k]=points[k]−points[k−1](特别地,points[−1]=0),具体做法是:

令 下界 low=(i+1) mod n,上界 high=(i−x+n + 1) mod n,将 diff[low] 的值加 1,将 diff[high] 的值减 1,如果 low > high 则将 diff[0] 的值加 1。

k 范围是 [上界,下届) ,即  [low, high)

遍历数组 nums 的所有元素并更新差分数组之后,遍历数组 diff 并计算前缀和,则每个下标处的前缀和表示当前轮调下标处的得分。在遍历过程中维护最大得分和最大得分的最小轮调下标,遍历结束之后即可得到结果。

实现方面,不需要显性创建数组 points,只需要创建差分数组 diff,遍历数组 diff 时即可根据前缀和得到数组 points 中的每个元素值。

证明

差分数组做法的正确性证明需要考虑 low 和 high 的不同情况。

如果 low < high < n,更新 diff 等价于将数组 points 的下标范围 [low,high - 1] 内的所有元素加 1。

如果 0 == high ≤ low,更新 diff 等价于将数组 points 的下标范围 [low,n−1] 内的所有元素加 1,diffs[0] 先减 1 后加 1 因此 diffs[0] 没有变化,同第 1 种情况。

如果 low >= high != 0,则需要将 diffs[0] 加 1,更新 diff 等价于将数组 points 的下标范围 [low,n−1] 和 [0,high - 1] 内的所有元素加 1。

上述三种情况对应的更新数组 points 的效果都符合预期,因此差分数组的做法可以得到正确的结果。

[上界,下界)  写法:

Java版:

class Solution {
    public int bestRotation(int[] nums) {
        int n = nums.length;
        int[] diff = new int[n];
        for (int i = 0; i < n; i++) {
            int low = (i + 1) % n;
            int high = (i - nums[i] + n + 1) % n;
            diff[low]++;
            diff[high]--;
            if (low >= high) {
                diff[0]++;
            }
        }

        int score = 0;
        int maxscore = 0;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            score += diff[i];
            if (score > maxscore) {
                maxscore = score;
                ans = i;
            }
        }
        return ans;
    }
}

Python3版:

class Solution:
    def bestRotation(self, nums: List[int]) -> int:
        n = len(nums)
        diff = [0] * n 
        for i in range(n):
            low = (i + 1) % n 
            high = (i - nums[i] + n + 1) % n 
            diff[low] += 1
            diff[high] -= 1
            if low >= high:
                diff[0] += 1
        
        ans = 0
        score = 0
        maxscore = 0
        for i in range(n):
            score += diff[i] 
            if score > maxscore:
                maxscore = score 
                ans = i 
        return ans

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组 nums 的长度。需要遍历数组 nums 两次。
  • 空间复杂度:O(n),其中 n 是数组 nums 的长度。需要创建长度为 n 的数组 diff。

[上界,下界] 写法:

1.注意:差分数组是对k的差分数组,不是对数组原位置的差分数组!!!

2.设nums数组长度为n,容易看出:对于nums[i],得分的下标范围在[nums[i], n - 1],闭区间长度为(n - nums[i]) 。而若进行k轮调,则 i --> (i - k + n) % n , 此时位于下标 (i - k + n) % n 的元素值仍为nums[i],若希望轮调完依然得分, 则需要满足(i - k + n) % n >= nums[i]。

(注意差分数组是针对k的差分数组,因此化简不等式要将k单独放在不等号一侧)

化简得到 k <= (i - nums[i] + n) % n,将这一值记为 k 对于 nums[i] 的上界。 由于上述我们已经求得了 nums[i] 这一元素得分区间的长度,且得到了 k 对于 nums[i] 的上界, 那么 k 的下界即可由区间长度和上界求得,下界为 k >= (i - nums[i] + n - (n - nums[i]) + 1) % n, 即 k >= (i + 1) % n 。

因此可知针对原数组 i 下标的nums[i]而言,k 取值 [(i + 1) % n, (i - nums[i] + n) % n]时得分。

3.建立长度为 n 的前缀数组 diff ,求原数组的每个元素的上下界并且令 diff[下界]++, diff[上界 + 1]--,

4.进行从头开始的for循环累加即可得到k取值[0, n - 1]的得分情况, 取下标最小的最大得分作为最终函数的返回值。

5.注意:上述我们求出的上下界并未考虑大小,很可能由于取余使得 下界 > 上界, 此时得分区间不再是[下界,上界],而是 [0,上界] 和 [下界,n - 1] 两个区间, 因此进行必须 diff[0]++ 的操作,否则会出现起点在终点后的情况,会没开始加分就先减分。 (分类讨论可以通过画图辅助理解,上下界两个位置将 [0, n - 1] 分为三段)

Java版:

class Solution {
    public int bestRotation(int[] nums) {
        int n = nums.length;
        int[] diff = new int[n];
        for (int i = 0; i < n; i++) {
            int low = (i + 1) % n;
            int high = (i - nums[i] + n) % n;
            diff[low]++;
            diff[(high + 1) % n]--;
            if (low > high) {
                diff[0]++;
            }
        }

        int ans = 0;
        int score = 0;
        int maxscore = 0;
        for (int i = 0; i < n; i++) {
            score += diff[i];
            if (score > maxscore) {
                maxscore = score;
                ans = i;
            }
        }
        return ans;
    }
}

Python3版:

class Solution:
    def bestRotation(self, nums: List[int]) -> int:
        n = len(nums)
        diff = [0] * n 
        for i in range(n):
            low = (i + 1) % n 
            high = (i - nums[i] + n) % n 
            diff[low] += 1
            diff[(high + 1) % n] -= 1
            if low > high:
                diff[0] += 1
        
        ans = 0
        score = 0
        maxscore = 0
        for i in range(n):
            score += diff[i] 
            if score > maxscore:
                maxscore = score 
                ans = i 
        return ans

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组 nums 的长度。需要遍历数组 nums 两次。
  • 空间复杂度:O(n),其中 n 是数组 nums 的长度。需要创建长度为 n 的数组 diff。
  • 21
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值