目录
采样
470. 用 Rand7() 实现 Rand10()
给定方法 rand7 可生成 [1,7] 范围内的均匀随机整数,试写一个方法 rand10 生成 [1,10] 范围内的均匀随机整数。
你只能调用 rand7() 且不能调用其他方法。请不要使用系统的 Math.random() 方法。
每个测试用例将有一个内部参数 n,即你实现的函数 rand10() 在测试时将被调用的次数。请注意,这不是传递给 rand10() 的参数。
示例 1:
输入: 1
输出: [2]
示例 2:
输入: 2
输出: [2,8]
示例 3:
输入: 3
输出: [3,8,10]
提示:
1 <= n <= 105
我的方法:拒绝采样
使用两次 rand7
API,生成两个坐标,选取十个点即可(其他点可递归调用rand10函数
)
class Solution:
def rand10(self):
x = rand7()
y = rand7()
if (x, y) == (1, 1):
return 1
elif (x, y) == (1, 2):
return 2
elif (x, y) == (1, 3):
return 3
elif (x, y) == (1, 4):
return 4
elif (x, y) == (1, 5):
return 5
elif (x, y) == (1, 6):
return 6
elif (x, y) == (1, 7):
return 7
elif (x, y) == (2, 1):
return 8
elif (x, y) == (2, 2):
return 9
elif (x, y) == (2, 3):
return 10
else:
return self.rand10()
生成 10 次,也可以
class Solution:
def rand10(self) -> int:
lst = []
for i in range(10):
lst.append(rand7())
return sum(lst)%10+1
拒绝采样优化
由于上述方法,生成了49个点,只利用了10个点,其余的点都会不断递归,非常浪费时间空间
改进:
- 比如我们可以利用两个
Rand7()
\textit{Rand7()}
Rand7() 相乘,分别可以得到结果如下:
- 我们可以得到每个数生成的概率为:
- 从中挑选 10 个等概率的数即可。
class Solution:
def rand10(self) -> int:
while True:
row = rand7()
col = rand7()
idx = (row - 1) * 7 + col
if idx <= 40:
return 1 + (idx - 1) % 10
拒绝采样——花式玩法
- 构造 2 次采样,分别有 2 和 5 种结果,组合起来便有 10 种概率相同的结果。
- 把这 10 种结果映射到 [ 1 , 10 ] [1,10] [1,10] 即可。
class Solution:
def rand10(self) -> int:
x = rand7()
while x>6: # 1/2 的概率
x = rand7()
y = rand7()
while y>5: # 1/5 的概率
y = rand7()
return y if x<=3 else 5+y
r
a
n
d
7
(
)
rand7()
rand7() 构造任意范围的随机数发生器
上述方法理论上可以构造任何范围的随机数发生器,比如
r
a
n
d
11
(
)
rand11()
rand11() :
构造
2
2
2 次采样,分别有
2
2
2 和
6
6
6 种结果,组合起来便有
12
12
12 种概率相同的结果。
把这
12
12
12 种结果映射到
[
1
,
12
]
[1,12]
[1,12] ,然后再拒绝
12
12
12 即可。
r
a
n
d
100
(
)
rand100()
rand100():
构造
3
3
3 次采样,分别有
4
,
5
,
5
4,5,5
4,5,5 种结果,组合起来便有
100
100
100 种概率相同的结果。
把这
100
100
100 种结果映射到
[
1
,
100
]
[1,100]
[1,100] 即可。
质数
判断一个数是否为质数
质数定义:x是质数,则它的因数只包含 [1,x]
方法1:暴力
方法二:根号优化
注意,因数一定是成对出现的,当遍历到
x
\sqrt x
x时还未找到一个因数,那么之后的数也不是 x 的因数了
举例 x = 24:
- 当找到 因数 3时,那么一定有个因数为 24/3=8。请注意,这一对因数 (3, 8) 一定满足:一个在 24 \sqrt {24} 24之前,一个在 24 \sqrt {24} 24 之后。
- 因此,当你在
x
\sqrt x
x 之前还未找到因数,那之后也找不到了
方法三:继续优化,除2以外的偶数一定不是质数,只判断奇数
def isPrime(n: int):
if n==2:
return True
elif n%2==0:
return False
for i in range(3, int(n**0.5)+1, 2):
if n%i==0:
return False
return True
204. 计数质数
给定整数 n ,返回 所有小于非负整数 n 的质数的数量 。
示例 1:
输入:n = 10
输出:4
解释:小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。
示例 2:
输入:n = 0
输出:0
示例 3:
输入:n = 1
输出:0
提示:
0 <= n <= 5 * 106
方法一:暴力解(每个数都单独判断一次)
超时
- 时间复杂度: O ( n n ) O(n\sqrt{n}) O(nn)
方法二:埃氏筛
考虑 每个数之间的关联性,基于以下事实:
- 如果 x x x 是质数,那么大于 x x x 的 x x x 的倍数 (即 2 x , 3 x , … 2x,3x,\ldots 2x,3x,…) 一定不是质数
- 继续优化,对于一个质数 x x x,如果按上文说的我们从 2 x 2x 2x 开始标记其实是冗余的,应该直接从 x ⋅ x x\cdot x x⋅x 开始标记。【因为 2 x , 3 x , … 2x,3x,\ldots 2x,3x,… 这些数一定在 x x x 之前就被其他数的倍数标记过了,例如 2 2 2 的所有倍数, 3 3 3 的所有倍数等。】
因此,构造一个 vector<int> isPrimes(n, true)
数组,表示第 i
位 是否为质数
def countPrimes(self, n: int) -> int:
isPrimes = [1]*n
cnt = 0
for i in range(2, n):
if isPrimes[i]:
cnt+=1
for j in range(i*i, n, i): # 👀 从 i*i开始,消除冗余
isPrimes[j] = 0
return cnt
时间复杂度: O ( n log log n ) O(n\log \log n) O(nloglogn)
因数
质因数分解
给定一个正整数,求它的质因数
思路:
从 idx=2 开始,满足条件则不断更新 x=x//idx
否则 ++idx
def primeFators(self, x: int)->List[int]:
idx = 2
ans = []
while x>1:
if x%idx==0:
ans.append(idx)
x //= idx
else:
idx += 1
return ans
- 时间复杂度:O(N)
优化,只判断前根号个数
定理:
- 大于 n \sqrt n n 的质因数最多只有一个
退出 while 循环后,如果 x!=1
,则剩下的x一定是一个质数
def primeFators(self, x: int)->List[int]:
idx = 2
ans = []
end = int(x**0.5)+1
while x>1 and idx<end: # 👀 end 用于剪枝,复杂度根号n
if x%idx==0:
ans.append(idx)
x //= idx
else:
idx += 1
if x!=1:
ans.append(x)
return ans
- 时间复杂度: O ( N ) O(\sqrt N) O(N)
最大公因数
给定两个正整数 x,y,求其 gcd
方法:辗转相除法
gcd
(
x
,
y
)
=
gcd
(
y
,
x
m
o
d
y
)
\gcd({\color{red}x},{\color{blue}y})=\gcd({\color{blue}y}, {\color{red}x}\mod {\color{blue}y})
gcd(x,y)=gcd(y,xmody).
def _gcd(x: int, y: int)->int:
if y==0:
return x
return _gcd(y, x%y)
- 时间复杂度 O ( log M ) O(\log M) O(logM), M = max ( x , y ) M = \max(x, y) M=max(x,y)
最小公倍数
def _lcd(x: int, y: int)->int:
return x*y//_gcd(x, y)
位运算
面试题 05.02. 二进制数转字符串
二进制数转字符串。给定一个介于0和1之间的实数(如0.72),类型为double,打印它的二进制表达式。如果该数字无法精确地用32位以内的二进制表示,则打印“ERROR”。
示例1:
输入:0.625
输出:"0.101"
示例2:
输入:0.1
输出:"ERROR"
提示:0.1无法被二进制准确表示
提示:
32位包括输出中的 "0." 这两位。
题目保证输入用例的小数位数最多只有 6 位
思路
介于 0 0 0 和 1 1 1 之间的实数的整数部分是 0 0 0,小数部分大于 0 0 0,因此其二进制表示的整数部分是 0 0 0,需要将小数部分转换成二进制表示。
-
以示例 1 1 1 为例,十进制数 0.625 0.625 0.625 可以写成 1 2 1 + 1 2 3 \dfrac{1}{2^1} + \dfrac{1}{2^3} 211+231,因此对应的二进制数是 0.10 1 ( 2 ) 0.101_{(2)} 0.101(2) ,二进制数中的左边的 1 1 1 为小数点后第一位,表示 1 2 1 \dfrac{1}{2^1} 211 ,右边的 1 1 1 为小数点后第三位,表示 1 2 3 \dfrac{1}{2^3} 231 。
-
如果将十进制数 0.625 0.625 0.625 乘以 2 2 2,则得到 1.25 1.25 1.25,可以写成 1 + 1 2 2 1 + \dfrac{1}{2^2} 1+221 ,因此对应的二进制数是 1.0 1 ( 2 ) 1.01_{(2)} 1.01(2) 。二进制数 0.10 1 ( 2 ) 0.101_{(2)} 0.101(2) 的两倍是 1.0 1 ( 2 ) 1.01_{(2)} 1.01(2),因此在二进制表示中,将一个数乘以 2 2 2 的效果是将小数点向右移动一位。
根据上述结论, 将实数的十进制表示转换成二进制表示的方法 \color{red}将实数的十进制表示转换成二进制表示的方法 将实数的十进制表示转换成二进制表示的方法是:
- 每次将实数乘以 2 2 2,将此时的整数部分添加到二进制表示的末尾,然后将整数部分置为 0 0 0,
- 重复上述操作,直到小数部分变成
0
0
0 或者小数部分出现循环时结束操作。
- 当小数部分变成 0 0 0 时,得到二进制表示下的有限小数;
- 当小数部分出现循环时,得到二进制表示下的无限循环小数。
由于这道题要求二进制表示的长度最多为 32 32 32 位,否则返回 “ERROR" \text{``ERROR"} “ERROR",因此不需要判断给定实数的二进制表示的结果是有限小数还是无限循环小数,而是在小数部分变成 0 0 0 或者二进制表示的长度超过 32 32 32 位时结束操作。当操作结束时,如果二进制表示的长度不超过 32 32 32 位则返回二进制表示,否则返回 “ERROR" \text{``ERROR"} “ERROR"。
def printBin(self, num: float) -> str:
ans = "0."
while len(ans) <= 32 and num!=0:
num *= 2
digit = int(num)
ans += str(digit)
num -= digit
return ans if len(ans)<=32 else "ERROR"
982. 按位与为零的三元组
给你一个整数数组 nums ,返回其中 按位与三元组 的数目。
按位与三元组 是由下标 (i, j, k) 组成的三元组,并满足下述全部条件:
0 <= i < nums.length
0 <= j < nums.length
0 <= k < nums.length
nums[i] & nums[j] & nums[k] == 0
,其中 & 表示按位与运算符。
示例 1:
输入:nums = [2,1,3]
输出:12
解释:可以选出如下 i, j, k 三元组:
(i=0, j=0, k=1) : 2 & 2 & 1
(i=0, j=1, k=0) : 2 & 1 & 2
(i=0, j=1, k=1) : 2 & 1 & 1
(i=0, j=1, k=2) : 2 & 1 & 3
(i=0, j=2, k=1) : 2 & 3 & 1
(i=1, j=0, k=0) : 1 & 2 & 2
(i=1, j=0, k=1) : 1 & 2 & 1
(i=1, j=0, k=2) : 1 & 2 & 3
(i=1, j=1, k=0) : 1 & 1 & 2
(i=1, j=2, k=0) : 1 & 3 & 2
(i=2, j=0, k=1) : 3 & 2 & 1
(i=2, j=1, k=0) : 3 & 1 & 2
示例 2:
输入:nums = [0,0,0]
输出:27
提示:
1 <= nums.length <= 1000
0 <= nums[i] < 216
思路:哈希表降时间复杂度
最容易想到的做法是使用三重循环枚举三元组 ( i , j , k ) (i,j,k) (i,j,k),再判断 nums [ i ] & nums [ j ] & nums [ k ] \textit{nums}[i] ~\&~ \textit{nums}[j] ~\&~ \textit{nums}[k] nums[i] & nums[j] & nums[k] 的值是否为 0 0 0。但这样做的时间复杂度是 O ( n 3 ) O(n^3) O(n3),其中 n n n 是数组 nums \textit{nums} nums 的长度,会超出时间限制。
注意到题目中给定了一个限制:数组 nums \textit{nums} nums 的元素不会超过 2 16 2^{16} 216。这说明, nums [ i ] & nums [ j ] \textit{nums}[i] ~\&~ \textit{nums}[j] nums[i] & nums[j] 的值也不会超过 2 16 2^{16} 216。因此,我们可以首先使用二重循环枚举 i i i 和 j j j,并使用一个长度为 2 16 2^{16} 216 的数组(或哈希表)存储每一种 nums [ i ] & nums [ j ] \textit{nums}[i] ~\&~ \textit{nums}[j] nums[i] & nums[j] 以及它出现的次数。随后,我们再使用二重循环,其中的一重枚举记录频数的数组,另一重枚举 k k k,这样就可以将时间复杂度从 O ( n 3 ) O(n^3) O(n3) 降低至 O ( n 2 + 2 16 ⋅ n ) O(n^2 + 2^{16} \cdot n) O(n2+216⋅n)。
def countTriplets(self, nums: List[int]) -> int:
from collections import Counter
cnt = Counter((x&y) for x in nums for y in nums)
ans = 0
for x in nums:
for mask, freq in cnt.items():
if (x&mask)==0:
ans += freq
return ans
其他
754. 到达终点数字
在一根无限长的数轴上,你站在0的位置。终点在target的位置。
你可以做一些数量的移动 numMoves :
每次你可以选择向左或向右移动。
第 i 次移动(从 i == 1 开始,到 i == numMoves ),在选择的方向上走 i 步。
给定整数 target ,返回 到达目标所需的 最小 移动次数(即最小 numMoves ) 。
示例 1:
输入: target = 2
输出: 3
解释:
第一次移动,从 0 到 1 。
第二次移动,从 1 到 -1 。
第三次移动,从 -1 到 2 。
示例 2:
输入: target = 3
输出: 2
解释:
第一次移动,从 0 到 1 。
第二次移动,从 1 到 3 。
提示:
-109 <= target <= 109
target != 0
方法一:分析 + 数学
假设移动了 k k k 次,每次任意地向左或向右移动,那么最终达到的位置实际上就是将 1 , 2 , 3 , … , k 1,2,3,\ldots,k 1,2,3,…,k 这 k k k 个整数添加正号或负号后求和的值。如果 target < 0 \textit{target} < 0 target<0,可以将这 k k k 个数的符号全部取反,这样求和的值为 − target > 0 -\textit{target} > 0 −target>0。因此我们可以只考虑 target > 0 \textit{target} > 0 target>0 的情况。
设 k k k 为最小的满足 s = ∑ i = 1 k ≥ target s s = \sum_{i=1}^{k} \geq \textit{target}s s=∑i=1k≥targets 的正整数。
- 如果 s = target s = \textit{target} s=target,那么答案即为 k k k。
- 如果 s > target s > \textit{target} s>target,需要在部分整数前添加负号来将和凑到 target \textit{target} target。
如果 delta = s − target \textit{delta} = s - \textit{target} delta=s−target 为偶数,则目标为从 1 1 1 到 k k k 中找出若干个整数使得他们的和为 delta 2 \dfrac{\textit{delta}}{2} 2delta,下面证明一定能到找这样的若干个整数。
- 如果 k ≥ delta 2 k \geq \dfrac{\textit{delta}}{2} k≥2delta,那么只需要选出一个 delta 2 \dfrac{\textit{delta}}{2} 2delta.
- 否则,可以先选出 k k k,再从剩余的 1 1 1 到 k − 1 k-1 k−1 中选出和为 delta 2 − k \dfrac{\textit{delta}}{2} - k 2delta−k 的若干个数即可,这样就把原问题变成了一个规模更小的问题。因为 delta 2 < s \dfrac{\textit{delta}}{2} < s 2delta<s,因此一定可以找出若干个数使得和为 delta 2 \dfrac{\textit{delta}}{2} 2delta。
如果
delta
\textit{delta}
delta 为奇数,那么就无法凑出这样的若干个数字。考虑
k
+
1
k+1
k+1 和
k
+
2
k+2
k+2,
∑
i
=
1
k
+
1
\sum_{i=1}^{k+1}
∑i=1k+1 和
∑
i
=
1
k
+
2
\sum_{i=1}^{k+2}
∑i=1k+2 中必有一个和
s
s
s 的奇偶性相同,使得此时的
delta
\textit{delta}
delta 为偶数。此时也满足
⌊
delta
2
⌋
<
∑
\Big\lfloor \dfrac{\textit{delta}}{2} \Big\rfloor < \sum
⌊2delta⌋<∑,因此也可以找到若干个数的和为
⌊
delta
2
⌋
\Big\lfloor \dfrac{\textit{delta}}{2} \Big\rfloor
⌊2delta⌋。
class Solution:
def reachNumber(self, target: int) -> int:
target = abs(target)
k = 0
while target > 0:
k += 1
target -= k
return k if target % 2 == 0 else k + 1 + k % 2
891. 子序列宽度之和
一个序列的 宽度 定义为该序列中最大元素和最小元素的差值。
给你一个整数数组 nums ,返回 nums 的所有非空 子序列 的 宽度之和 。由于答案可能非常大,请返回对 109 + 7 取余 后的结果。
子序列 定义为从一个数组里删除一些(或者不删除)元素,但不改变剩下元素的顺序得到的数组。例如,[3,6,2,7] 就是数组 [0,3,1,6,2,2,7] 的一个子序列。
示例 1:
输入:nums = [2,1,3]
输出:6
解释:子序列为 [1], [2], [3], [2,1], [2,3], [1,3], [2,1,3] 。
相应的宽度是 0, 0, 0, 1, 1, 2, 2 。
宽度之和是 6 。
示例 2:
输入:nums = [2]
输出:0
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 105
思路:转换一下,将两两之间的计算,转换为每个元素的对最终答案的共享
- 数组的顺序不影响结果,因此我们可以首先对其排序
- 例如得到,[1,2,3,4,5,6]
以 4 最大的子序列有 8个,以 4 最小的子序列有2个,4 的贡献为:8*4 + 2*(-4)
class Solution:
def sumSubseqWidths(self, nums: List[int]) -> int:
nums.sort()
n = len(nums)
ans, MOD = 0, 10**9+7
ans, pow2 = 0, 1
for x, y in zip(nums, reversed(nums)):
ans += (x - y) * pow2
pow2 = pow2 * 2 % MOD
return ans % MOD
增函数->二分
878. 第 N 个神奇数字
一个正整数如果能被 a 或 b 整除,那么它是神奇的。
给定三个整数 n , a , b ,返回第 n 个神奇的数字。因为答案可能很大,所以返回答案 对 109 + 7 取模 后的值。
示例 1:
输入:n = 1, a = 2, b = 3
输出:2
示例 2:
输入:n = 4, a = 2, b = 3
输出:6
提示:
1 <= n <= 109
2 <= a, b <= 4 * 104
找到增函数关系,二分法
题目给出三个数字
n
n
n,
a
a
a,
b
b
b,满足
1
≤
n
≤
1
0
9
1 \le n \le 10^9
1≤n≤109 ,
2
≤
a
,
b
≤
4
×
1
0
4
2 \le a, b \le 4 \times 10^4
2≤a,b≤4×104 ,并给出「神奇数字」的定义:若一个正整数能被
a
a
a 和
b
b
b 整除,那么它就是「神奇」的。现在需要求出对于给定
a
a
a 和
b
b
b 的第
n
n
n 个「神奇数字」。设
f
(
x
)
f(x)
f(x) 表示为小于等于
x
x
x 的「神奇数字」个数,因为小于等于
x
x
x 中能被
a
a
a 整除的数的个数为
⌊
x
a
⌋
\lfloor \frac{x}{a} \rfloor
⌊ax⌋,小于等于
x
x
x 中能被
b
b
b 整除的个数为
⌊
x
b
⌋
\lfloor \frac{x}{b} \rfloor
⌊bx⌋,小于等于
x
x
x 中同时能被
a
a
a 和
b
b
b 整除的个数为
⌊
x
c
⌋
\lfloor \frac{x}{c} \rfloor
⌊cx⌋,其中
c
c
c 为
a
a
a 和
b
b
b 的最小公倍数,所以
f
(
x
)
f(x)
f(x)的表达式为:
f
(
x
)
=
⌊
x
a
⌋
+
⌊
x
b
⌋
−
⌊
x
c
⌋
f(x) = \lfloor \frac{x}{a} \rfloor + \lfloor \frac{x}{b} \rfloor - \lfloor \frac{x}{c} \rfloor
f(x)=⌊ax⌋+⌊bx⌋−⌊cx⌋
即
f
(
x
)
f(x)
f(x) 是一个随着
x
x
x 递增单调不减函数。那么我们可以通过「二分查找」来进行查找第
n
n
n 个「神奇数字」。
def nthMagicalNumber(self, n: int, a: int, b: int) -> int:
from math import lcm
MOD = 10 ** 9 + 7
c = lcm(a, b)
func = lambda x: x // a + x // b - x // c
l = min(a, b)
r = n * min(a, b) + 1
while l < r:
m = l + (r - l) // 2
cnt = func(m)
if cnt == n: # 注意此处 如果 cnt==n 这个数不一定是 结果
r = m # 因为如果 x是答案 x~x+(min(a,b)-1)的数的CC是一样的
elif cnt > n:
r = m
else:
l = m + 1
return r % MOD
快速计算均值——前缀和
813. 最大平均值和的分组
给定数组 nums 和一个整数 k 。我们将给定的数组 nums 分成 最多 k 个相邻的非空子数组 。 分数 由每个子数组内的平均值的总和构成。
注意我们必须使用 nums 数组中的每一个数进行分组,并且分数不一定需要是整数。
返回我们所能得到的最大 分数 是多少。答案误差在 10-6 内被视为是正确的。
示例 1:
输入: nums = [9,1,2,3,9], k = 3
输出: 20.00000
解释:
nums 的最优分组是[9], [1, 2, 3], [9]. 得到的分数是 9 + (1 + 2 + 3) / 3 + 9 = 20.
我们也可以把 nums 分成[9, 1], [2], [3, 9].
这样的分组得到的分数为 5 + 2 + 6 = 13, 但不是最大值.
示例 2:
输入: nums = [1,2,3,4,5,6,7], k = 4
输出: 20.50000
提示:
1 <= nums.length <= 100
1 <= nums[i] <= 104
1 <= k <= nums.length
思路:DP
命题:平均值和最大的分组的子数组数目必定是
k
k
k。
证明略,见:
https://leetcode.cn/problems/largest-sum-of-averages/solutions/1993132/zui-da-ping-jun-zhi-he-de-fen-zu-by-leet-09xt/
- 子问题:如何计算子数组的平均值?
- 前缀和,使用一个数组 prefix \textit{prefix} prefix来保存数组 nums \textit{nums} nums的前缀和
- 子问题:如何确定最优划分?
- 动态规划,
dp
[
i
]
[
j
]
\color{red}\textit{dp}[i][j]
dp[i][j] 表示
nums
\textit{nums}
nums 在区间
[
0
,
i
−
1
]
[0, i-1]
[0,i−1] 被切分成
j
j
j 个子数组的最大平均值和,显然
i
≥
j
i \ge j
i≥j,计算分两种情况讨论:
- 当 j = 1 j = 1 j=1 时, dp [ i ] [ j ] \textit{dp}[i][j] dp[i][j] 是对应区间 [ 0 , i − 1 ] [0, i - 1] [0,i−1] 的平均值;
- 当 j > 1 j > 1 j>1 时,我们将可以将区间 [ 0 , i − 1 ] [0, i - 1] [0,i−1] 分成 [ 0 , x − 1 ] [0, x - 1] [0,x−1] 和 [ x , i − 1 ] [x, i - 1] [x,i−1] 两个部分,其中 x ≥ j − 1 x \ge j-1 x≥j−1,那么 dp [ i ] [ j ] \textit{dp}[i][j] dp[i][j] 等于所有这些合法的切分方式的平均值和的最大值。
- 动态规划,
dp
[
i
]
[
j
]
\color{red}\textit{dp}[i][j]
dp[i][j] 表示
nums
\textit{nums}
nums 在区间
[
0
,
i
−
1
]
[0, i-1]
[0,i−1] 被切分成
j
j
j 个子数组的最大平均值和,显然
i
≥
j
i \ge j
i≥j,计算分两种情况讨论:
因此转移方程为:
dp
[
i
]
[
j
]
=
{
∑
r
=
0
i
−
1
nums
[
r
]
i
,
j
=
1
max
x
≥
j
−
1
{
d
p
[
x
]
[
j
−
1
]
+
∑
r
=
x
i
−
1
nums
[
r
]
i
−
x
}
,
j
>
1
\textit{dp}[i][j] = \begin{cases} \dfrac{\sum_{r = 0}^{i - 1}\textit{nums}[r]}{i}, & j = 1 \\ \max\limits_{x \ge j - 1} \{dp[x][j - 1] + \dfrac{\sum_{r = x}^{i - 1}\textit{nums}[r]}{i - x}\}, & j > 1 \end{cases}
dp[i][j]=⎩
⎨
⎧i∑r=0i−1nums[r],x≥j−1max{dp[x][j−1]+i−x∑r=xi−1nums[r]},j=1j>1
假设数组 nums \textit{nums} nums 的长度为 n n n,那么 dp [ n ] [ k ] \textit{dp}[n][k] dp[n][k] 表示数组 nums \textit{nums} nums 分成 k k k 个子数组后的最大平均值和,即最大分数。
Mod 运算
1590. 使数组和能被 P 整除
给你一个正整数数组 nums,请你移除 最短 子数组(可以为 空),使得剩余元素的 和 能被 p 整除。 不允许 将整个数组都移除。
请你返回你需要移除的最短子数组的长度,如果无法满足题目要求,返回 -1 。
子数组 定义为原数组中连续的一组元素。
示例 1:
输入:nums = [3,1,4,2], p = 6
输出:1
解释:nums 中元素和为 10,不能被 p 整除。我们可以移除子数组 [4] ,剩余元素的和为 6 。
示例 2:
输入:nums = [6,3,5,2], p = 9
输出:2
解释:我们无法移除任何一个元素使得和被 9 整除,最优方案是移除子数组 [5,2] ,剩余元素为 [6,3],和为 9 。
示例 3:
输入:nums = [1,2,3], p = 3
输出:0
解释:和恰好为 6 ,已经能被 3 整除了。所以我们不需要移除任何元素。
示例 4:
输入:nums = [1,2,3], p = 7
输出:-1
解释:没有任何方案使得移除子数组后剩余元素的和被 7 整除。
示例 5:
输入:nums = [1000000000,1000000000,1000000000], p = 3
输出:0
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 109
1 <= p <= 109
方法一:前缀和 + 两层循环遍历 (超时)
前缀和 用于记录 连续子数组的和,无脑两层循环遍历找到最终答案
def minSubarray(self, nums: List[int], p: int) -> int:
preSum = [0 for _ in range(len(nums)+1)]
for i in range(len(nums)):
preSum[i+1] = preSum[i] + nums[i]
if preSum[-1]%p==0:
return 0
ans = len(nums)
for i in range(len(preSum)):
for j in range(i+1, len(preSum)):
if preSum[-1]-preSum[j]+preSum[i]%p==0:
ans = min(ans, j-i)
return ans if ans<len(nums) else -1
时间复杂度: O ( n 2 ) O(n^2) O(n2)
方法二:数学化简 + 前缀和 + 哈希表
- 定理一:给定正整数 x x x、 y y y、 z z z、 p p p,如果 y m o d p = x y \bmod p = x ymodp=x,那么 ( y − z ) m o d p = 0 (y - z) \bmod p = 0 (y−z)modp=0 等价于 z m o d p = x z \bmod p = x zmodp=x。
证明: y m o d p = x y \bmod p = x ymodp=x 等价于 y = k 1 × p + x y = k_1 \times p + x y=k1×p+x, ( y − z ) m o d p = 0 (y-z) \bmod p = 0 (y−z)modp=0 等价于 y − z = k 2 × p y - z = k_2 \times p y−z=k2×p, z m o d p = x z \bmod p = x zmodp=x 等价于 z = k 3 × p + x z = k_3 \times p + x z=k3×p+x 都是整数,那么给定 y = k 1 × p + x y = k_1 \times p + x y=k1×p+x,有 y − z = k 2 × p ↔ z = ( k 1 − k 2 ) × p + x ↔ z = k 3 × p + x y - z = k_2 \times p \leftrightarrow z = (k_1 - k_2) \times p + x \leftrightarrow z = k_3 \times p + x y−z=k2×p↔z=(k1−k2)×p+x↔z=k3×p+x。
- 定理二:给定正整数 x x x, y y y, z z z, p p p,那么 ( y − z ) m o d p = x (y - z) \bmod p = x (y−z)modp=x 等价于 z m o d p = ( y − x ) m o d p z \bmod p = (y - x) \bmod p zmodp=(y−x)modp。
证明: ( y − z ) m o d p = x (y - z) \bmod p = x (y−z)modp=x 等价于 y − z = k 1 × p + x y - z = k_1 \times p + x y−z=k1×p+x,其中 k 1 k_1 k1是整数,经过变换有 z = y − k 1 × p − x = k 2 × p + ( y − x ) m o d p − k 1 × p = ( k 2 − k 1 ) × p + ( y − x ) m o d p z = y - k_1 \times p - x = k_2 \times p + (y - x) \bmod p - k_1 \times p = (k_2 - k_1) \times p + (y - x) \bmod p z=y−k1×p−x=k2×p+(y−x)modp−k1×p=(k2−k1)×p+(y−x)modp,等价于 z m o d p = ( y − x ) m o d p z \bmod p = (y - x) \bmod p zmodp=(y−x)modp。
因此,这里我们利用定理1,改进方法1中的语句
preSum[-1]-preSum[j]+preSum[i]%p == 0
=
>
=>
=>
preSum[-1]+preSum[i]%p == preSum[j]%p
此时,等式两边只存在一个变量,因此我们可以用哈希表对其进行优化。
def minSubarray(self, nums: List[int], p: int) -> int:
preSum = [0 for _ in range(len(nums)+1)]
for i in range(len(nums)):
preSum[i+1] = preSum[i] + nums[i]
if preSum[-1]%p==0:
return 0
ans = len(nums)
dic = {}
for i in range(len(preSum)):
cur = preSum[i]%p
if cur in dic:
ans = min(ans, i-dic[cur])
dic[(preSum[-1] + preSum[i])%p] = i
return ans if ans<len(nums) else -1
时间复杂度: O ( n ) O(n) O(n)