[Leetcode 3524] 求出数组的X值I

一. 题目描述

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), 无论是从时间上还是空间上,都是很大的进步。

五. 一个很好的可以对代码进行可视化的平台(轻量代码)

pythontutor

六. 写在最后

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

相关推荐
边境悍匪1 小时前
蜗牛学苑 Java 智能体学习 Day46|贯穿项目 2 思维导图复盘
java·开发语言·vue.js·学习·spring
weixin199701080163 小时前
《二手ERP对接闲鱼API:聚石塔强制入塔后的架构重构实录》(附Python源码)
python
小白快快跑哦3 小时前
python-字符串全解(六):正则表达式-转义与非转义
python·正则表达式·转义与非转义
IvanCodes9 小时前
Python 数据处理(十三):JSON、CSV 与数据序列化
开发语言·python
M78佐菲9 小时前
ARM学习笔记(1)
linux·arm开发·笔记·嵌入式硬件·学习
Patrick在香港10 小时前
Claude 工具调用返回空:8 次失败里只有 1 次状态码不对,其余全带 200
爬虫·python·api·claude·香港
Web3&Basketball10 小时前
CRM Agent 后训练实战:3 倍更少错误
python·架构·大模型·agent·推理
AIFQuant11 小时前
ETF行情API接入踩坑记:从报错到跑通的七个问题
python·金融·区块链·etf·基金
无敌贵点大王11 小时前
RTThread学习记录11——RT-Thread 设备模型吃透:UART/ADC/PWM/PIN 到底有什么区别?
c语言·stm32·学习·链表