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

798. 得分最高的最小轮调

1. 题目内容

给你一个数组 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,得分最高。

提示:
1 <= nums.length <= 1e5
0 <= nums[i] < nums.length

2. 思考过程

拿到题目后,先看一眼题目给出的数据输入范围。这里给出的输入范围上限是1e5,这意味着可以放弃所有时间复杂度高于或等于O(n^2)的算法了,需要尽可能的让算法时间复杂度往O(log n)或者O(n)靠拢。

题目里所谓的轮调操作很好理解,依次让数组中的每一个数字作为表头元素,计算该状态下的数组每个元素与其下标之差,若得到的值< or = 0,则可以记作一分,最终得到一个轮换后得分最高的数组,返回其轮换次数 k。

这里最暴力的做法就是模拟,按照题目给出的意思,完全模拟每次轮调,并最终输出得分最高的k值,这个时间复杂度是O(n^2)。必须想到一种方法,使其可以在O(n)内,知道数组中每一个元素,在k次轮换过程中对最终结果的贡献。没有思路的时候,不妨画一画:
在这里插入图片描述
以2 3 1 4 0 为例子,先模拟计算一下当k = 0即暂时不做轮调时,数组的得分情况。接着,当k = 1时,开始了第一次轮调,数字2移动到了数组末端,这时3就成为了表头元素。
在这里插入图片描述
重复这个过程,我们就能得到共计5次轮调的数组得分情况。为了便于理解,这里暂时将轮调到数组末尾的元素直观表示出来。
在这里插入图片描述
接下来就将轮调的元素放回原位,并将得分部分用蓝框标识。通过观察下图可以发现,只要逐行统计出<= 0元素的总和,就能知道该次轮调的得分情况。其实一眼就能看到,当k=3时,该行共有4个位置满足得分条件,但如何才能用代码将其计算出来呢?如果我们逐行进行计算,那时间复杂度就再次回到了O(n^2)。
在这里插入图片描述
这里开始就考验知识积累了,我这里做最后一点提示,我将每一列数字的得分情况用以下形式表示,这样子是否有了思路。
在这里插入图片描述
没错,这是很经典的差分(后续可能会专门挑一道例题讲讲差分,这里就暂时略过),我们新建一个差分数组vector<int> diff(n + 1)其中n = nums.size()。当元素开始得分时,我们在差分数组k值位置记1,当元素得分结束时,若其还在范围n内,在n+1位置减1。得分区间其实有很明显的规律:所有情况可以简单归为两类:1. 仅有一个得分区间(第一列,第二列等);2. 有两个得分区间(第三列)。

而当我们计算完全部元素的得分,并记录到差分数组中后,就能通过计算差分数组的前缀和,统计每次轮调的得分,并最终比较得到得分最大的k值。计算差分时间复杂度O(n),计算差分数组前缀和时间复杂度O(n),所以最终能在O(n)内计算得到结果。
在这里插入图片描述
至此,已经可以开始写这道题的代码逻辑了:

int bestRotation(vector<int>& nums) {
    int n = nums.size();
    vector<int> diff(n + 1, 0);
    for (int i = 0; i < n; i++)
    {
        if (nums[i] - i > 0)                // 最多存在一个得分区间
        {
            diff[i + 1] += 1;               // 不管是不是可以得分,都先加着
            int check = nums[i] - (n - 1);  // 轮换过程中能达到的最小值
            if (check > 0)                  // 若最小值大于0,说明不可能存在任何得分区间
                diff[i + 1] -= 1;
            if (i + 1 + -(check) + 1 < n)   // 这里判断闭区间位置,假如>=n,即后续全部得分,就不需要再用差分去减了
                diff[i + 1 + -(check) + 1] -= 1;
        }
        else                                // 最多可以存在两个得分区间,也可以只有一个(从头到尾情况)
        {
            diff[0]++;                      // 由于是负数开始,从头就能开始计分
            if (-(nums[i] - i) + 1 >= n)    // 这个情况就是从头到尾都是得分区间,可以不做处理
                continue;
            else
            {
                diff[-(nums[i] - i) + 1] -= 1;  // 前半段得分区间结束
                diff[i + 1] += 1;               // 后半段得分区间开始,持续到最后
            }
        }
    }
    int localmax = diff[0], index = 0;
    for (int i = 1; i < n; i++)
    {
        diff[i] += diff[i - 1];
        if (localmax < diff[i])
        {
            localmax = diff[i];
            index = i;
        }
    }
    return index;
}

最后,上面的代码为了便于理解,展现了很多判断,这些判断都是可以进行优化的,最终代码放在下面,就不做更多解释了。

这道题非常考验对差分的理解,同时也要求一点点对前缀和的思考。这道题难度分2130 ( 2023年12月),是我这几周来做过最有趣的一道题,同时也很有挑战,值得记录一下思考过程。

3. 最终代码

int bestRotation(vector<int>& nums) {
    int n = nums.size();
    vector<int> diff(n + 1, 0);
    for (int i = 0; i < n; i++)
    {
        if (nums[i] - i > 0)
        {
            diff[i + 1]++;
            if (n - (nums[i] - i) + 1 < n)
                diff[(n - (nums[i] - i) + 1)]--;
        }
        else
        {
            diff[0]++;
            diff[-(nums[i] - i) + 1]--;
            diff[i + 1]++;
        }
    }
    int localmax = diff[0], index = 0;
    for (int i = 1; i < n; i++)
    {
        diff[i] += diff[i - 1];
        if (localmax < diff[i])
        {
            localmax = diff[i];
            index = i;
        }
    }
    return index;
}
  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值