题目
解题思路
暴力解
遍历每个可能的 k
,计算轮调 k
个位置之后的数组得分。假设数组的长度是 n
,则有 n
种可能的轮调,对于每种轮调都需要
O
(
n
)
O(n)
O(n) 的时间计算得分,总时间复杂度是
O
(
n
2
)
O(n^2)
O(n2),考虑到数据范围为
1
≤
n
≤
1
0
5
1 \le n \le 10^5
1≤n≤105,暴力解会超出时间限制
差分
假定当前下标为 i
,轮调次数为 k
,那么轮调后下标为 i−k
,当新下标为负数时,相当于 nums[i]
出现在比原数组更“靠后”的位置,此时下标等价于
(
i
−
k
+
n
)
m
o
d
n
(i - k + n) \mod n
(i−k+n)modn。
考虑什么情况下 n u m s [ i ] nums[i] nums[i] 能够得分?
首先新下标的取值范围为 [ 0 , n − 1 ] [0, n - 1] [0,n−1],即有 0 ⩽ i − k ⩽ n − 1 0 \leqslant i - k \leqslant n - 1 0⩽i−k⩽n−1。由此可分析出 k k k 的取值范围为:
0 ⩽ i − k ⇔ k ⩽ i 0 \leqslant i - k \Leftrightarrow k \leqslant i 0⩽i−k⇔k⩽i
i − k ⩽ n − 1 ⇔ i − ( n − 1 ) ⩽ k i - k \leqslant n - 1 \Leftrightarrow i - (n - 1) \leqslant k i−k⩽n−1⇔i−(n−1)⩽k
即由新下标取值范围可知 k k k 的上下界分别为 i i i 和 i − ( n − 1 ) i - (n - 1) i−(n−1)。
同时为了满足得分定义,还有 n u m s [ i ] ⩽ i − k nums[i] \leqslant i - k nums[i]⩽i−k,进行变形可得:
n
u
m
s
[
i
]
⩽
i
−
k
⇔
k
⩽
i
−
n
u
m
s
[
i
]
nums[i] \leqslant i - k \Leftrightarrow k \leqslant i - nums[i]
nums[i]⩽i−k⇔k⩽i−nums[i]
此时有两个关于
k
k
k 的上界
k
⩽
i
k \leqslant i
k⩽i
以及
k
⩽
i
−
n
u
m
s
[
i
]
k \leqslant i - nums[i]
k⩽i−nums[i]
由于
n
u
m
s
[
i
]
nums[i]
nums[i]取值范围为
[
0
,
n
)
[0, n)
[0,n),则有
i
−
n
u
m
s
[
i
]
⩽
i
i - nums[i] \leqslant i
i−nums[i]⩽i
由于必须同时满足「合法移动(有效下标)」和「能够得分」,仅考虑范围更小(更严格)由
n
u
m
s
[
i
]
⩽
i
−
k
nums[i] \leqslant i - k
nums[i]⩽i−k 推导而来的上界
k
⩽
i
−
n
u
m
s
[
i
]
k \leqslant i - nums[i]
k⩽i−nums[i] 即可。
综上,
n
u
m
s
[
i
]
nums[i]
nums[i] 能够得分的
k
k
k 的取值范围为:
[
i
−
(
n
−
1
)
,
i
−
n
u
m
s
[
i
]
]
[i - (n - 1), i - nums[i]]
[i−(n−1),i−nums[i]]
最后考虑
[
i
−
(
n
−
1
)
,
i
−
n
u
m
s
[
i
]
]
[i - (n - 1), i - nums[i]]
[i−(n−1),i−nums[i]](编程时均进行加
n
n
n 模
n
n
n 转为正数)什么情况下为合法的连续段:
- 当 i − ( n − 1 ) ⩽ i − n u m s [ i ] i - (n - 1) \leqslant i - nums[i] i−(n−1)⩽i−nums[i] 时, [ i − ( n − 1 ) , i − n u m s [ i ] ] [i - (n - 1), i - nums[i]] [i−(n−1),i−nums[i]] 为合法连续段;
- 当 i − ( n − 1 ) > i − n u m s [ i ] i - (n - 1) > i - nums[i] i−(n−1)>i−nums[i] 时,根据负数下标等价于 ( i − k + n ) m o d n (i - k + n) \mod n (i−k+n)modn,此时 [ i − ( n − 1 ) , i − n u m s [ i ] ] [i - (n - 1), i - nums[i]] [i−(n−1),i−nums[i]] 等价于 [ 0 , i − n u m s [ i ] ] [0, i - nums[i]] [0,i−nums[i]] 和 [ i − ( n − 1 ) , n − 1 ] [i - (n - 1), n - 1] [i−(n−1),n−1] 两段。
连续一段有可能出现中间,也有可能是两头两段
分析出原数组的每个 n u m s [ i ] nums[i] nums[i] 能够得分的 k k k 的取值范围后,就可以将原问题转换为【区间修改】&【单点查询】问题,进而可利用差分模板解决(对照LeetCode 1109.航班预订统计),举例如下:
输入:
nums = [2,3,1,4,0]
转换为区间修改问题,定义数组
K[n]
,K[i]
代表轮调下标为i
时的得分
K
下标0 1 2 3 4 nums[0]=2
可以得分的轮调下标范围1 1 1 nums[1]=3
可以得分的轮调下标范围1 1 nums[2]=1
可以得分的轮调下标范围1 1 1 1 nums[3]=4
可以得分的轮调下标范围1 nums[4]=0
可以得分的轮调下标范围1 1 1 1 1 每次轮调所能获得的分数 2 3 3 4 3 综上,轮调下标
k=3
时,得分最高。
算法流程如下:
- 分析出原数组的每个 n u m s [ i ] nums[i] nums[i] 能够得分的 k k k 的取值范围表达式
- 遍历原数组并更新差分数组
- 计算出每个 n u m s [ i ] nums[i] nums[i] 能够得分的 k k k 的取值范围,假定取值范围为 [ l , r ] [l, r] [l,r],可以对 [ l , r ] [l, r] [l,r] 进行 + 1 +1 +1 标记(即【区间修改】,修改差分数组两端点即可),代表 k k k 的取值范围为 [ l . r ] [l.r] [l.r]时 n u m s [ i ] nums[i] nums[i]能够得 1 分
- 当处理完所有的
n
u
m
s
[
i
]
nums[i]
nums[i] 并更新差分数组之后,找到标记次数最多的位置
k
k
k 即是答案
- 遍历差分数组并计算前缀和,则每个下标处的前缀和表示当前轮调下标处的得分
- 在遍历过程中维护最大得分和最大得分的最小轮调下标,遍历结束之后即可得到结果
class Solution {
public int bestRotation(int[] nums) {
int n = nums.length;
int[] diffs = new int[n + 1];
for (int i = 0; i < n; i++) {
// nums[i] 能够得分的 k 的取值范围[l, r]
int l = (i - (n - 1) + n) % n;
int r = (i - nums[i] + n) % n;
if (l <= r) {
// 得分段出现在中间
add(diffs, l, r);
} else {
// 得分段出现在两头
add(diffs, 0, r);
add(diffs, l, n - 1);
}
}
// 前缀和 每个下标处的前缀和表示当前轮调下标处的得分
// 不需要显性创建得分数组 points
// 遍历差分数组时即可根据前缀和得到数组 points 中的每个元素值,节约空间
for (int i = 1; i <= n; i++) {
diffs[i] += diffs[i - 1];
}
int ans = 0;
for (int i = 1; i <= n; i++) {
// 维护最大得分下标
if (diffs[i] > diffs[ans]) {
ans = i;
}
}
return ans;
}
// 区间修改
private void add(int[] c, int l, int r) {
c[l] += 1;
c[r + 1] -= 1;
}
}
-
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。需要遍历数组 nums \textit{nums} nums两次。
-
空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums的长度。需要创建长度为 n n n 的数组 diffs \textit{diffs} diffs。