一. 题目描述
1.1 题干信息
给你一个由正 整数组成的数组 nums,以及一个正 整数 k。
你可以对 nums 执行一次 操作,该操作中可以移除任意不重叠 的前缀和后缀,使得 nums 仍然非空 。
你需要找出 nums 的x 值 ,即在执行操作后,剩余元素的乘积 除以 k 后的余数 为 x 的操作数量。
返回一个大小为 k 的数组 result,其中 resultx 表示对于 0 ≤ x ≤ k − 1 0 \le x \le k - 1 0≤x≤k−1,nums 的x 值 。
数组的前缀 指从数组起始位置开始到数组中任意位置的一段连续子数组。
数组的后缀 是指从数组中任意位置开始到数组末尾的一段连续子数组。
子数组 是数组中一段连续的元素序列。
注意,在操作中选择的前缀和后缀可以是空的 。
1.2 示例
输入: nums = 1,2,3,4,5, k = 3
输出: 9,2,4
解释:
- 对于 x = 0,可行的操作包括所有不会移除 nums2 == 3 的前后缀移除方式。
- 对于 x = 1,可行操作包括:
- 移除空前缀和后缀 2, 3, 4, 5,nums 变为 1。
- 移除前缀 1, 2, 3 和后缀 5,nums 变为 4。
- 对于 x = 2,可行操作包括:
- 移除空前缀和后缀 3, 4, 5,nums 变为 1, 2。
- 移除前缀 1 和后缀 3, 4, 5,nums 变为 2。
- 移除前缀 1, 2, 3 和空后缀,nums 变为 4, 5。
- 移除前缀 1, 2, 3, 4 和空后缀,nums 变为 5。
输入: nums = 1,2,4,8,16,32, k = 4输出: 18,1,2,0
解释:
- 对于 x = 0,唯一 不 得到 x = 0 的操作有:
- 移除空前缀和后缀 4, 8, 16, 32,nums 变为 1, 2。
- 移除空前缀和后缀 2, 4, 8, 16, 32,nums 变为 1。
- 移除前缀 1 和后缀 4, 8, 16, 32,nums 变为 2。
- 对于 x = 1,唯一的操作是:
- 移除空前缀和后缀 2, 4, 8, 16, 32,nums 变为 1。
- 对于 x = 2,可行操作包括:
- 移除空前缀和后缀 4, 8, 16, 32,nums 变为 1, 2。
- 移除前缀 1 和后缀 4, 8, 16, 32,nums 变为 2。
- 对于 x = 3,没有可行的操作。
示例 3:输入: nums = 1,1,2,1,1, k = 2
输出: 9,6
1.3 提示
- 1 ≤ n u m s i ≤ 10 9 1 \le numsi \le 10^9 1≤numsi≤109
- 1 ≤ n u m s . l e n g t h ≤ 10 5 1 \le nums.length \le 10^5 1≤nums.length≤105
- 1 ≤ k ≤ 5 1 \le k \le 5 1≤k≤5
二. 储备知识------动态规划(Dynamic Programming, DP)
2.1 参考视频
可以参考bilibili教学视频,链接如下:
10分钟彻底搞懂"动态规划"算法------Ross-Ning
2.2 例题引入
给定一个无序数组,要求找出最长的递增的子序列,简化一下,就是,给定一个无序数组,要求找出最长的递增的子序列的长度,例: nums = 1, 5, 2, 4, 3
2.3 暴力搜索方式求解
python
def L(nums, i):
"""
description:
Returns the length of longest increasing subsquence starting from i
"""
# 4.如果查到最后一个数字的话,直接返回长度 1。后序无其他数字可以与他构成递增子序列
if i == len(nums) - 1: # Last number
return 1
max_len = 1 # 初始化最长子序列长度
for j in range(i+1, len(nums)):
# 1.检查 nums[i] 后面的所有数字, 索引记为 j
if nums[j] > nums[i]:
# 3. 将最长的子序列长度选出来
max_len = max(max_len, L(nums, j) + 1) # 2.计算从j开始的最长子序列长度 -> +1 得到目前的序列总长度
return max_len
def length_of_LIS(nums):
return max(
L(nums, i) for i in range(len(nums)) # 对每个元素依次进行搜索 -> 并返回最大的那个长度
)
虽然说,暴力解法很速度,很直接,但是他的时间复杂度太大了,对于本题的话,时间复杂度约为 O ( 2 n ) O(2^n) O(2n),所以我们按照视频的教程,考虑借助Hash表去优化该算法
2.4 利用Hash表求解
python
memo = {} # 定义一个字典(Hash表)
def L(nums, i):
"""
description:
Returns the length of longest increasing subsquence starting from i
"""
# 5.检查是否在哈希表中
if i in memo:
return memo[i]
# 4.如果查到最后一个数字的话,直接返回长度 1。后序无其他数字可以与他构成递增子序列
if i == len(nums) - 1: # Last number
return 1
max_len = 1 # 初始化最长子序列长度
for j in range(i+1, len(nums)):
# 1.检查 nums[i] 后面的所有数字, 索引记为 j
if nums[j] > nums[i]:
# 3. 将最长的子序列长度选出来
max_len = max(max_len, L(nums, j) + 1) # 2.计算从j开始的最长子序列长度 -> +1 得到目前的序列总长度
memo[i] = max_len # 更新Hash表
return max_len
def length_of_LIS(nums):
return max(
L(nums, i) for i in range(len(nums)) # 对每个元素依次进行搜索 -> 并返回最大的那个长度
)
在这里我们主要是借助Hash表做什么呢,实际上就是减少重复运算,减少的部分即是索引 i 处之后的最长子序列长度,把这个数据保存至Hash表中,这样在每次递归调用的时候,如果Hash表中存在相关数据,直接调用,就不用重复计算了,这样,利用空间换时间的方式,时间复杂度约为 O ( n 2 ) O(n^2) O(n2),然后我们可以继续按照视频的教程,利用迭代方式继续微小优化一下。
2.5 利用迭代算法求解
python
def length_of_LIS(nums):
n = len(nums)
L = [1] * n # 初始化长度,这个是后续最长递增子序列的长度
for i in reversed(range(n)): # 用i倒序遍历
for j in range(i+1, n): # 遍历i后面的数据
if nums[j] > nums[i]:
L[i] = max(L[i], L[j] + 1)
return max(L)
这里考虑使用迭代的算法,实际上他是一个倒序的思想方法,从后往前算,这样,虽然时间复杂度还是 O ( n 2 ) O(n^2) O(n2), 但是他没有利用递归,所以实际上就是利用两种方式实现。
2.6 其他时间复杂度的算法
根据deepseek提示,本题可以使用贪心+二分的算法求解,时间复杂度约为 O ( n l o g n ) O(n logn) O(nlogn),但是已经不是动态规划的思想了,所以在此不再赘述。
三. 牛刀小试
3.1 题目描述
找出给定序列的连续子序列的最大和。
3.2 解决方法
python
def max_sum(nums, i):
if i == len(nums) - 1:
return nums[i]
add_sum = nums[i]
temp_num = nums[i]
for j in range(i+1, len(nums)):
temp_num += nums[j]
if add_sum < temp_num:
add_sum = temp_num
return add_sum
def answer_num(nums):
return max(
max_sum(nums, i) for i in range(len(nums))
)
该方法时间复杂度为 O ( n 2 ) O(n^2) O(n2),根据deepseek的建议,可以利用如下的Kadane算法进行优化。
3.3 利用Kadane算法进行优化
python
def max_sum(nums: list[int]):
cur = best = nums[0]
for i in range(1, len(nums)):
cur = max(cur+nums[i], nums[i])
best = max(best, cur)
return best
这里要提的一点就是,这个cur是干嘛的,首先就是这个算法把时间复杂度优化成了 O ( n ) O(n) O(n), 是一个很大的进步,然后这个cur的问题是这样的。
因为初始化的时候cur就是nums0, 所以对于num1, 前面的cur一定到nuns0结束的子数组是最大的,然后判断cur+num1和num1谁大谁小,如果 num1大,说明前面的是累赘,要求的是最大的和,所以前面的不能用,如果num1小,那说明前面的是buff,我要用,这样就和前面的串起来了,继续往后走,每一次更新cur的时候,都会更新一下best, 这样最终输出的best就是答案了。
四. 实战演练
4.1 先利用暴力算法求解
python
class Solution:
def resultArray(self, nums: List[int], k: int) -> List[int]:
result = [0] * k
for i in range(len(nums)):
for j in range(i, len(nums)):
num_ij = nums[i:j+1]
answer = 1
for x in num_ij:
answer *= x
answer %= k
result[answer] += 1
return result
方法是没问题的,可以解决部分测试用例,但毕竟也是暴力算法,而且在Leetcode平台提交的时候,也会出现超出时间限制导致提交失败,所以先分析一下,她这个时间复杂度已经达到了 O ( n 3 ) O(n^3) O(n3)数量级,所以,肯定是要优化的
4.2 利用DP的思想去尝试求解
python
class Solution:
def resultArray(self, nums: List[int], k: int) -> List[int]:
result = [0] * k
dp = [[0 for i in range(len(nums))] for j in range(len(nums))]
for i in range(len(nums)-1, -1, -1): # i = 4 3 2 1 0
for j in range(len(nums)-1, i-1, -1): # j = 4 ... i
if i == j:
dp[i][j] = nums[i]
elif i < j:
dp[i][j] = dp[i+1][j] * nums[i]
else:
continue
result[dp[i][j] % k] += 1
return result
这里,利用空间换时间的主要改动就是,加入了一个dp列表,而且是个二维的列表,像下面这个矩阵一样。
( a 00 a 01 a 02 a 03 a 04 a 10 a 11 a 12 a 13 a 14 a 20 a 21 a 22 a 23 a 24 a 30 a 31 a 32 a 33 a 34 a 40 a 41 a 42 a 43 a 44 ) \begin{pmatrix} a_{00} & a_{01} & a_{02} & a_{03} & a_{04} \\ a_{10} & a_{11} & a_{12} & a_{13} & a_{14} \\ a_{20} & a_{21} & a_{22} & a_{23} & a_{24} \\ a_{30} & a_{31} & a_{32} & a_{33} & a_{34} \\ a_{40} & a_{41} & a_{42} & a_{43} & a_{44} \end{pmatrix} a00a10a20a30a40a01a11a21a31a41a02a12a22a32a42a03a13a23a33a43a04a14a24a34a44
并且,我们要求这个矩阵是这样的,
a i j = { n u m s i i = j n u m s i + 1 ∗ n u m s j i < j 0 i > j a_{ij} = \begin{cases}numsi & i = j \\ numsi+1 * numsj & i < j\\ 0 & i > j\end{cases} aij=⎩ ⎨ ⎧numsinumsi+1∗numsj0i=ji<ji>j
这个矩阵元素 a i j a_{ij} aij 实际意义就是从第i个元素到第j个元素(边界均包含)的各个元素的乘积,并且是从下往上,从右往左不断更新的。
但是,这个算法虽然把时间复杂度改为了 O ( n 2 ) O(n^2) O(n2), 空间复杂度变成了 O ( n 2 ) O(n^2) O(n2), 他是会造成超出内存限制的,所以我们考虑如下数学方法,将矩阵进行压缩。
4.3 利用矩阵压缩方法继续求解
python
class Solution:
def resultArray(self, nums: List[int], k: int) -> List[int]:
result = [0] * k
length = int(len(nums) * (len(nums) + 1) / 2)
dp = [0 for i in range(length)]
for i in range(len(nums)-1, -1, -1): # i = 4 3 2 1 0
for j in range(len(nums)-1, i-1, -1): # j = 4 ... i
index_ij = int((2*(len(nums))+1-i) * i * 0.5 + j - i)
index_i1j = int(((2*(len(nums))+1)-(i+1)) * (i+1) * 0.5 + j - i - 1)
if i == j:
dp[index_ij] = nums[i]
elif i < j:
dp[index_ij] = dp[index_i1j] * nums[i]
else:
continue
result[dp[index_ij] % k] += 1
return result
这里的矩阵压缩,主要利用了如下坐标转换公式,对于矩阵元素 a i j a_{ij} aij, 我们得到的压缩后的数组(列表)中索引为
i n d e x = 1 2 × i × ( 2 × l + 1 − i ) + j − i = i l + 1 2 i − 1 2 i 2 + j − i = i × l − 1 2 i 2 − i 2 + j \begin{aligned} index &= \frac{1}{2}\times i\times (2\times l+1-i) + j - i \\ &= il + \frac12i-\frac12 i^2+j-i \\ &= i\times l -\frac12i^2-\frac i2+j \end{aligned} index=21×i×(2×l+1−i)+j−i=il+21i−21i2+j−i=i×l−21i2−2i+j
其中, l = l e n ( n u m s ) l =len(nums) l=len(nums)
实际上,根本没有对矩阵进行复杂度上的压缩,他只是看起来把二维压缩成了一维,但还是空间复杂度为 O ( n 2 ) O(n^2) O(n2), 所以这个方法行不通,只好考虑官方题解了
4.4 官方题解
python
class Solution:
def resultArray(self, nums: List[int], k: int) -> List[int]:
n = len(nums)
result = [0] * k
dp = [0] * k
for x in nums:
ndp = [0] * k
ndp[x % k] += 1
for r in range(k):
ndp[(r*x) % k] += dp[r]
dp = ndp
for r in range(k):
result[r] += dp[r]
return result
官方题解最大的好处就是他枚举的是余数,而不是我那个数组下标,而且官方题解不太好理解的是下面这句代码:
python
for r in range(k):
ndp[(r * x) % k] += dp[r]
他这句代码的含义就是把上一层所有以 i-1 结尾的子数组,接上当前元素x,得到以 i 结尾的新子数组。举个例子,比如现在 k = 3, 上一层 dp = 1, 2, 0, 也就是说,余数为0的子数组有1个,余数为1的子数组有2个,余数为2的子数组有0个。然后,现在来了个数 x = 2, 分别把这几个余数循环,ndp0*2 % 3 = ndp0 += dp0 = 1; ndp1* 2%3 = ndp2 += dp1 = 2; ndp2\*2 % 3 = ndp1 = 0。翻译一下就是,本来有1个余数是0的子数组,接上(乘上)2之后变成余数0了,数没变还是1个;本来有2个余数是1的子数组,接上(乘上)2之后变成余数2了,数量还是2个;本来就没有余数是2的子数组,接上(乘上)2没有余数1的,数量还是0。
官方题解的时间复杂度为 O ( n k ) O(nk) O(nk), 空间复杂度为 O ( k ) O(k) O(k), 无论是从时间上还是空间上,都是很大的进步。
五. 一个很好的可以对代码进行可视化的平台(轻量代码)
六. 写在最后

所有的为时已晚,其实都是恰逢其时。
------2026.09.21