个人题解|最大乘积区间问题

180 阅读7分钟

问题描述

小R手上有一个长度为 n 的数组 (n > 0),数组中的元素分别来自集合 [0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]。小R想从这个数组中选取一段连续的区间,得到可能的最大乘积。

你需要帮助小R找到最大乘积的区间,并输出这个区间的起始位置 x 和结束位置 y (x ≤ y)。如果存在多个区间乘积相同的情况,优先选择 x 更小的区间;如果 x 相同,选择 y 更小的区间。

注意:数组的起始位置为 1,结束位置为 n

测试样例

样例1:

输入:n = 5, arr = [1, 2, 4, 0, 8]
输出:[1, 3]

思路:

这个问题要求我们找到一个连续子数组,其乘积最大。由于数组元素仅来自特定的集合,这使得问题更具挑战性,但同时也可以通过一些技巧和优化来解决。

  1. 暴力解法:直接计算所有子数组的乘积,记录最大值及其对应的区间。但是这种方法的时间复杂度为 O(n^2),当 n 较大时不够高效。
  2. 双指针技巧:我们可以使用双指针来处理连续区间的乘积计算。具体来说,可以通过维护一个左指针和右指针,不断扩大右指针的范围,计算当前区间的乘积。当遇到乘积为 0 时,可以跳过这个位置(因为乘积为 0 的区间无法提供最大的乘积)。
  3. 动态更新最大乘积:我们在遍历过程中不断更新当前乘积,并记录最大乘积对应的区间。如果遇到相同的最大乘积,优先选择左边界较小的区间,如果左边界相同则选择右边界较小的区间。

暴力解决法

代码实现

def solution(n: int, arr: list[int]) -> list[int]:
    # 初始化
    max_product = float('-inf')  # 用负无穷初始化最大乘积
    start = 0  # 最大乘积的区间起始位置
    end = 0    # 最大乘积的区间结束位置
    temp_start = 0  # 临时起始位置,用于记录当前子数组起始
    
    # 使用两个指针遍历数组,左指针为 `i`, 右指针为 `j`
    for i in range(n):
        product = 1  # 初始化每个子区间的乘积为 1
        
        # 从 i 开始扩展右指针
        for j in range(i, n):
            product *= arr[j]  # 更新当前区间的乘积
            
            # 如果当前乘积大于最大乘积,则更新最大乘积和区间
            if product > max_product:
                max_product = product
                start = i + 1  # 起始位置 +1,符合题目要求
                end = j + 1    # 结束位置 +1,符合题目要求
    
    return [start, end]

解释:

  1. 外层循环for i in range(n) 用来固定子数组的起始位置。
  2. 内层循环for j in range(i, n) 用来从 i 开始扩展到数组的其他元素,计算当前子数组的乘积。
  3. 乘积更新:每当计算出一个新的子数组的乘积,就判断是否大于当前记录的最大乘积,如果是,就更新最大乘积以及对应的区间。
  4. 最终返回:返回最大乘积的子数组的起始和结束位置,注意返回的是 1-based 索引。

时间复杂度:

  • 外层循环遍历每个元素:O(n)
  • 内层循环会遍历从 i 到 n 的所有元素,最坏情况下也遍历 n 次。

因此,总的时间复杂度为 O(n^2),这种复杂度对于较小的输入是可以接受的。

双指针解决法

要使用 双指针 来解决这个问题,我们需要巧妙地利用双指针来维护一个滑动窗口,避免重复计算每个子数组的乘积。双指针的核心思想是通过不断扩展或收缩窗口来跟踪乘积,并记录最大乘积区间。

方案说明:

  1. 我们可以使用一个滑动窗口来表示当前的子数组。窗口由两个指针 left 和 right 组成,left 是区间的起始位置,right 是区间的结束位置。
  2. 乘积计算:每次扩展 right 指针时,我们都更新当前区间的乘积。若乘积变得非常大,我们记录该区间的乘积和起始位置。如果遇到乘积为零,跳过该位置,因为乘积会被清零。
  3. 收缩窗口:当我们发现乘积过大时,可以尝试收缩窗口(通过增大 left 指针),以便优化乘积。这里不需要显式地缩小窗口,只要遇到乘积为 0 时自动跳过即可。

双指针解法:

我们可以从头开始,扩展 right 指针,并在每次扩展时更新当前的乘积。当乘积达到最大时,记录该区间。如果遇到乘积为 0,直接跳到下一个可能的有效区间。

代码实现:

def solution(n: int, arr: list[int]) -> list[int]:
    # Edit your code here
    max_product = float('-inf')  # 最大乘积,初始化为负无穷
    start = 0  # 最大乘积的区间起始位置
    end = 0    # 最大乘积的区间结束位置
    
    # 双指针:左指针和右指针
    left = 0
    product = 1  # 当前子数组的乘积
    
    for right in range(n):
        product *= arr[right]  # 扩展右指针,更新当前区间的乘积
        
        # 如果乘积大于当前最大乘积,更新结果
        while left <= right and product > max_product:
            max_product = product
            start = left + 1  # 起始位置 + 1,符合题目要求
            end = right + 1    # 结束位置 + 1,符合题目要求
        
        # 如果当前乘积为 0,则重置,跳过乘积为零的区间
        if arr[right] == 0:
            left = right + 1  # 跳过零元素
            product = 1  # 重新开始乘积计算
        print(f"start = {start},end = {end}")
    return [start, end]
if __name__ == "__main__":
    # Add your test cases here
    print(solution(5, [1, 2, 4, 0, 8]) == [1, 3])
    print(solution(7, [1, 2, 4, 8, 0, 256, 0]) == [6, 6])

解释:

  1. 双指针维护滑动窗口

    • left 和 right 分别表示当前区间的左右边界,初始时我们只考虑长度为 1 的区间(即只有一个元素)。
    • 通过扩展 right 指针来扩大当前的窗口,并计算该区间的乘积。
  2. 乘积更新

    • 每次 right 扩展时,我们更新当前区间的乘积(即将 arr[right] 乘到当前乘积上)。
    • 如果当前乘积大于历史最大乘积,我们就更新最大乘积,并记录该区间的起始和结束位置。
  3. 处理零值

    • 如果 arr[right] 为零,乘积会被清零,因此需要重置乘积,并将 left 指针跳到 right + 1,表示从下一个位置重新开始计算子数组乘积。
  4. 返回值

    • 最终返回的是乘积最大子数组的起始和结束位置,注意题目要求是 1-based 索引,因此需要在 start 和 end 位置上加 1。

时间复杂度分析:

  • 时间复杂度:由于我们仅遍历一次数组,并且对于每个元素最多做一次乘积计算和更新,因此总的时间复杂度是 O(n)
  • 空间复杂度:空间复杂度为 O(1) ,只需要常数的空间来维护变量。

小结:

今天的题目比较简单,算是巩固复习以前的知识点了,近期在青训营也刷了一些题目,发现有些题目给的样例结果好像是错的,代码运行一直不通过,希望工作人员能解决一下。另外,今天青训营就结束了,明天也将迎来24年的最后一个月,希望往后的日子,自己能坚持刷题打卡,学习新技术,不断充实自身,如果你恰好也看到了这篇文章,请见谅,我也才刚开始学写笔记,我会继续加油的。

鲜衣怒马少年时,不负韶华行且知