问题描述
小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]
思路:
这个问题要求我们找到一个连续子数组,其乘积最大。由于数组元素仅来自特定的集合,这使得问题更具挑战性,但同时也可以通过一些技巧和优化来解决。
- 暴力解法:直接计算所有子数组的乘积,记录最大值及其对应的区间。但是这种方法的时间复杂度为 O(n^2),当 n 较大时不够高效。
- 双指针技巧:我们可以使用双指针来处理连续区间的乘积计算。具体来说,可以通过维护一个左指针和右指针,不断扩大右指针的范围,计算当前区间的乘积。当遇到乘积为 0 时,可以跳过这个位置(因为乘积为 0 的区间无法提供最大的乘积)。
- 动态更新最大乘积:我们在遍历过程中不断更新当前乘积,并记录最大乘积对应的区间。如果遇到相同的最大乘积,优先选择左边界较小的区间,如果左边界相同则选择右边界较小的区间。
暴力解决法
代码实现
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]
解释:
- 外层循环:
for i in range(n)用来固定子数组的起始位置。 - 内层循环:
for j in range(i, n)用来从i开始扩展到数组的其他元素,计算当前子数组的乘积。 - 乘积更新:每当计算出一个新的子数组的乘积,就判断是否大于当前记录的最大乘积,如果是,就更新最大乘积以及对应的区间。
- 最终返回:返回最大乘积的子数组的起始和结束位置,注意返回的是 1-based 索引。
时间复杂度:
- 外层循环遍历每个元素:O(n)
- 内层循环会遍历从
i到n的所有元素,最坏情况下也遍历n次。
因此,总的时间复杂度为 O(n^2),这种复杂度对于较小的输入是可以接受的。
双指针解决法
要使用 双指针 来解决这个问题,我们需要巧妙地利用双指针来维护一个滑动窗口,避免重复计算每个子数组的乘积。双指针的核心思想是通过不断扩展或收缩窗口来跟踪乘积,并记录最大乘积区间。
方案说明:
- 我们可以使用一个滑动窗口来表示当前的子数组。窗口由两个指针
left和right组成,left是区间的起始位置,right是区间的结束位置。 - 乘积计算:每次扩展
right指针时,我们都更新当前区间的乘积。若乘积变得非常大,我们记录该区间的乘积和起始位置。如果遇到乘积为零,跳过该位置,因为乘积会被清零。 - 收缩窗口:当我们发现乘积过大时,可以尝试收缩窗口(通过增大
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])
解释:
-
双指针维护滑动窗口:
left和right分别表示当前区间的左右边界,初始时我们只考虑长度为 1 的区间(即只有一个元素)。- 通过扩展
right指针来扩大当前的窗口,并计算该区间的乘积。
-
乘积更新:
- 每次
right扩展时,我们更新当前区间的乘积(即将arr[right]乘到当前乘积上)。 - 如果当前乘积大于历史最大乘积,我们就更新最大乘积,并记录该区间的起始和结束位置。
- 每次
-
处理零值:
- 如果
arr[right]为零,乘积会被清零,因此需要重置乘积,并将left指针跳到right + 1,表示从下一个位置重新开始计算子数组乘积。
- 如果
-
返回值:
- 最终返回的是乘积最大子数组的起始和结束位置,注意题目要求是 1-based 索引,因此需要在
start和end位置上加 1。
- 最终返回的是乘积最大子数组的起始和结束位置,注意题目要求是 1-based 索引,因此需要在
时间复杂度分析:
- 时间复杂度:由于我们仅遍历一次数组,并且对于每个元素最多做一次乘积计算和更新,因此总的时间复杂度是 O(n) 。
- 空间复杂度:空间复杂度为 O(1) ,只需要常数的空间来维护变量。
小结:
今天的题目比较简单,算是巩固复习以前的知识点了,近期在青训营也刷了一些题目,发现有些题目给的样例结果好像是错的,代码运行一直不通过,希望工作人员能解决一下。另外,今天青训营就结束了,明天也将迎来24年的最后一个月,希望往后的日子,自己能坚持刷题打卡,学习新技术,不断充实自身,如果你恰好也看到了这篇文章,请见谅,我也才刚开始学写笔记,我会继续加油的。
鲜衣怒马少年时,不负韶华行且知