题目描述
给你一个数组 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
。
最简单的做法是遍历每个可能的 k k k,计算轮调 k k k 个位置之后的数组得分。假设数组的长度是 n n n,则有 n n n 种可能的轮调,对于每种轮调都需要 O ( n ) O(n) O(n) 的时间计算得分,总时间复杂度是 O ( n 2 ) O(n ^ 2) O(n2),对于 n ≤ 1 0 5 n \le 10^5 n≤105的数据范围会超出时间限制,因此需要优化。
对于数组 n u m s nums nums 中的元素 x x x,当 x x x所在下标大于或等于 x x x 时,元素 x x x 会记 1 1 1 分。因此元素 x x x 记 1 1 1 分的下标范围是 [ x , n − 1 ] [x,n-1] [x,n−1],有 n − x n-x n−x个下标,元素 x x x 不计分的下标范围是 [ 0 , x − 1 ] [0, x-1] [0,x−1],有 x x x 个下标。
官方题解写的也挺好的,但是我觉得中间有几个地方光看文字有些不清楚,另外也是想完全理解这道题,所以自己参考官方题解写一个题解。
得分分析
假设元素 x x x的初始下标为 i i i,则当轮调下标为 k k k 时,元素 x x x 位于下标 ( i − k + n ) mod n (i-k+n)\ \text{mod} \ n (i−k+n) mod n。如果元素 xxx 记 111 分,则有 ( i − k + n ) mod n ≥ x (i-k+n)\ \text{mod} \ n \ge x (i−k+n) mod n≥x,等价于 k ≤ ( i − x + n ) mod n k \le (i-x+n)\ \text{mod} \ n k≤(i−x+n) mod n。下面分两种情况来讨论:
-
i < x i<x i<x
- 若 k < = i k<=i k<=i,因为 k ≥ 0 k\ge 0 k≥0,而下标 k k k的元素被轮调到下标 0 0 0处,那么由轮调定义,原来在下标 i i i的 x x x被轮调到下标 i − k i-k i−k处,易得 ( i − k ) ∈ [ 0 , i ] (i-k) \in [0,i] (i−k)∈[0,i],此时元素 x x x的得分是0。所以要求元素 x x x的得分是 1 1 1的轮调 k k k必须满足 k ≥ i + 1 k \ge i + 1 k≥i+1。
- k ≤ ( i − x + n ) mod n k \le (i-x+n)\ \text{mod} \ n k≤(i−x+n) mod n 等价于 k ≤ ( i − x + n ) k \le (i-x+n) k≤(i−x+n)。
由上述得,当 i < x i<x i<x时,使得元素 x x x得分为 1 1 1的 k k k的取值范围为 i + 1 ≤ k ≤ i − x + n i+1 \le k \le i-x+n i+1≤k≤i−x+n。
-
i ≥ x i \ge x i≥x
在 i i i的元素 x x x有两种移动方式,都能使得元素 x x x的得分为 1 1 1
- 当元素 x x x向右移动,此时 x x x能达到的最远位置为 n − 1 n-1 n−1,且在 k k k轮调时,这个位置是原始位置 k − 1 k-1 k−1处的元素占据,那么容易有 k − 1 = i k-1=i k−1=i,那么 k = i + 1 k=i+1 k=i+1,当 k > i + 1 k>i+1 k>i+1时,元素 x x x仍然会在 [ i , n − 2 ] [i,n-2] [i,n−2]的某个位置,所以 k ≥ i + 1 k \ge i+1 k≥i+1都会是使得元素 x x x的得分为 1 1 1的合法位置。
- 当元素 x x x向左移动,元素 x x x的新位置必须在 [ x , i ] [x,i] [x,i]之间。 k k k轮调时,在位置 0 0 0的元素原来所在位置为 k k k,那么要满足元素 x x x的新位置必须在 [ x , i ] [x,i] [x,i]之间,对于原来的位置来说, i − k ≥ x i-k \ge x i−k≥x,即 k ≤ i − x k \le i-x k≤i−x必须被满足。
当 i ≥ x i \ge x i≥x时,使得元素 x x x得分为 1 1 1的 k k k的取值范围为 k ≥ i + 1 k \ge i+1 k≥i+1或 k ≤ i − x k \le i-x k≤i−x。
计算得分
对于数组的每个元素,都可以计算得分为 1 1 1的轮调 k k k的范围。在这个范围内的每个轮调下标 k k k加1,这样遍历完所有元素后,找到值最大的最小轮调下标。
创建长度为 n n n 的数组 p o i n t s points points,其中 p o i n t s [ k ] points[k] points[k] 表示轮调下标为 k k k 时的得分。对于数组 n u m s nums nums 中的每个元素,得到该元素记 1 1 1 分的轮调下标范围,然后将数组 p o i n t s points points的该下标范围内的所有元素加 1 1 1。当数组 p o i n t s points points 中的元素值确定后,找到最大元素的最小下标。该做法的时间复杂度仍然是 O ( n 2 ) O(n ^ 2) O(n2),为了降低时间复杂度,需要利用差分数组。
差分数组优化
在使用 p o i n t s points points数组时,当 i < x i<x i<x我们需要把 p o i n t s points points的下标范围 [ i + 1 , i − x + n ] [i+1,i-x+n] [i+1,i−x+n]内的所有元素加 1 1 1,当 i ≥ x i\ge x i≥x时应该将 p o i n t s points points的下标范围 [ 0 , i − x ] [0,i-x] [0,i−x]和 [ i + 1 , n − 1 ] [i+1, n-1] [i+1,n−1]内的所有元素都加 1 1 1。由于是将一段或两段连续下标范围内的元素加 1 1 1,因此可以使用差分数组实现。
定义长度为 n n n 的差分数组 d i f f s diffs diffs,其中 d i f f s [ k ] = p o i n t s [ k ] − p o i n t s [ k − 1 ] diffs[k]=points[k]-points[k-1] diffs[k]=points[k]−points[k−1](特别地, p o i n t s [ − 1 ] = 0 points[-1]=0 points[−1]=0),具体做法是:令 l o w = ( i + 1 ) mod n , h i g h = ( i − x + n + 1 ) mod n low=(i+1)\ \text{mod}\ n, high=(i-x+n+1)\ \text{mod}\ n low=(i+1) mod n,high=(i−x+n+1) mod n,将 d i f f s [ l o w ] diffs[low] diffs[low] 的值加 1 1 1,将 d i f f s [ h i g h ] diffs[high] diffs[high] 的值减 1 1 1,如果 l o w ≥ h i g h low \ge high low≥high 则将 d i f f s [ 0 ] diffs[0] diffs[0] 的值加 1 1 1。
遍历数组 n u m s nums nums 的所有元素并更新差分数组之后,遍历数组 d i f f s diffs diffs并计算前缀和,则每个下标处的前缀和表示当前轮调下标处的得分。在遍历过程中维护最大得分和最大得分的最小轮调下标,遍历结束之后即可得到结果。
首先证明,为什么前缀和就是轮调为 k k k时的得分。因为由 d i f f s diffs diffs的定义, d i f f s [ k ] = p o i n t s [ k ] − p o i n t s [ k − 1 ] diffs[k]=points[k]-points[k-1] diffs[k]=points[k]−points[k−1],所以下标为 k k k的前缀和 p r e s u m [ k ] presum[k] presum[k]最终可以通过式 ( 1 ) (1) (1)化简,这就证明了前缀和确实是轮调下标为 k k k时的得分。
p r e s u m [ k ] = d i f f s [ k ] + d i f f s [ k − 1 ] + ⋯ + d i f f s [ 1 ] + d i f f s [ 0 ] = p o i n t s [ k ] − p o i n t s [ k − 1 ] + p o i n t s [ k − 1 ] − p o i n t s [ k − 2 ] + ⋯ + p o i n t s [ 1 ] − p o i n t s [ 0 ] + p o i n t s [ 0 ] − p o i n t s [ − 1 ] = p o i n t s [ k ] − p o i n t s [ − 1 ] = p o i n s [ k ] \begin{equation} \begin{split} presum[k] &= diffs[k]+diffs[k-1]+ \dots +diffs[1]+diffs[0]\\ &=points[k]-points[k-1]+points[k-1]-points[k-2]+ \dots +points[1]-points[0]+points[0]-points[-1]\\ &=points[k]-points[-1]\\ &=poins[k] \end{split} \end{equation} presum[k]=diffs[k]+diffs[k−1]+⋯+diffs[1]+diffs[0]=points[k]−points[k−1]+points[k−1]−points[k−2]+⋯+points[1]−points[0]+points[0]−points[−1]=points[k]−points[−1]=poins[k]
接下来说明使用差分数组时,给 p o i n t s points points数组中 [ l o w , h i g h − 1 ] [low,high-1] [low,high−1]范围加 1 1 1可以通过 d i f f s [ l o w ] = d i f f s [ l o w ] + 1 diffs[low]=diffs[low]+1 diffs[low]=diffs[low]+1和 d i f f s [ h i g h ] = d i f f s [ h i g h ] − 1 diffs[high]=diffs[high]-1 diffs[high]=diffs[high]−1来完成。此时,当求前缀和在 l o w low low的位置时,我们的和加 1 1 1了,然后当 h i g h high high位置时,和减 1 1 1,在这之间和没有变化,这说明我们的差分数组是有用的。
证明
差分数组做法的正确性证明需要考虑 l o w low low和 h i g h high high 的不同情况。以 i i i和 x x x的关系分情况讨论。
-
i < x i<x i<x
这种情况下, i + 1 ≤ k ≤ i − x + n i+1 \le k \le i-x+n i+1≤k≤i−x+n,此时 h i g h high high的最大值在 i = x i=x i=x时取得为 n n n,这样对 n n n进行取模后, h i g h = 0 high=0 high=0,于是 d i f f s [ 0 ] = d i f f s [ 0 ] − 1 diffs[0]=diffs[0]-1 diffs[0]=diffs[0]−1,但是实际上,加一的范围在 [ l o w , n − 1 ] [low,n-1] [low,n−1],和位置 0 0 0没有关系, h i g h high high取 0 0 0是由于取模的关系,此时也满足 l o w ≥ h i g h low \ge high low≥high,所以要把位置 0 0 0多减的 1 1 1给加回来, d i f f s [ 0 ] = d i f f s [ 0 ] + 1 diffs[0]= diffs[0]+1 diffs[0]=diffs[0]+1。
-
i ≥ x i \ge x i≥x
此情况下,由两段区域组成。
-
当元素 x x x向右移动,此时 x x x能达到的最远位置为 n − 1 n-1 n−1,且在 k k k轮调时,这个位置是原始位置 k − 1 k-1 k−1处的元素占据,那么容易有 k − 1 = i k-1=i k−1=i,那么 k = i + 1 k=i+1 k=i+1,当 k > i + 1 k>i+1 k>i+1时,元素 x x x仍然会在 [ i , n − 2 ] [i,n-2] [i,n−2]的某个位置,所以 k ≥ i + 1 k \ge i+1 k≥i+1都会是使得元素 x x x的得分为 1 1 1的合法位置。
这种情况下的 h i g h high high将是定值 n n n,对 n n n取模后为 0 0 0,有 i < x i < x i<x的论述可知,需要 d i f f s [ 0 ] = d i f f s [ 0 ] + 1 diffs[0]= diffs[0]+1 diffs[0]=diffs[0]+1。
-
当元素 x x x向左移动,元素 x x x的新位置必须在 [ x , i ] [x,i] [x,i]之间。 k k k轮调时,在位置 0 0 0的元素原来所在位置为 k k k,那么要满足元素 x x x的新位置必须在 [ x , i ] [x,i] [x,i]之间,对于原来的位置来说, i − k ≥ x i-k \ge x i−k≥x,即 k ≤ i − x k \le i-x k≤i−x必须被满足。
这一段需要将 d i f f s [ 0 ] = d i f f s [ 0 ] + 1 diffs[0]= diffs[0]+1 diffs[0]=diffs[0]+1,我们可以这么看,其实是因为这一段并没有 l o w low low,但是和元素 x x x向右移动,隐含了那一段区域的 h i g h = n high=n high=n一样,这里也隐含了这一段区域的 l o w = 0 low=0 low=0,所以 d i f f s [ 0 ] = d i f f s [ 0 ] + 1 diffs[0]= diffs[0]+1 diffs[0]=diffs[0]+1需要被执行。
这两段区域的分析整体来看,那就是执行如下代码:
++diffs[low]; --diffs[high]; ++diffs[0];
-
总结上面两种情况,是否需要执行 d i f f s [ 0 ] = d i f f s [ 0 ] + 1 diffs[0]= diffs[0]+1 diffs[0]=diffs[0]+1都可以根据 l o w ≥ h i g h low \ge high low≥high这个条件来判断。
class Solution {
public:
int bestRotation(vector<int>& nums) {
int n = nums.size();
vector<int> diffs(n);
for(int i = 0; i < n; ++i) {
int x = nums[i];
if (i < x) {
int low = (i + 1) % n;
int high = (i - x + n + 1) % n;
++diffs[low];
--diffs[high];
if (low >= high) {
// 此时high其实只能为0
// if (high != 0) {
// return 0; // 做一个测试,实际代码中可以去除
// }
++diffs[0];
}
} else {
// x往左移的这一段
int low = 0, high = (i - x + 1) % n;
++diffs[low];
if (high != 0) {
--diffs[high];
}
// x往右移的这一段
low = (i + 1) % n;
high = 0;
++diffs[low];
// ++diffs[high]; // 反正加了又要减
}
}
int ans = 0;
int score = 0;
int maxScore = -1;
for(int i = 0; i < n; ++i) {
score += diffs[i];
if (score > maxScore) {
maxScore = score;
ans = i;
}
}
return ans;
}
};