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;
}