问题描述
小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]
样例2:
输入:
n = 7, arr = [1, 2, 4, 8, 0, 256, 0]输出:[6, 6]
样例3:
输入:
n = 8, arr = [1, 2, 4, 8, 0, 256, 512, 0]输出:[6, 7]
分析
注意到,数组元素的可能值要么是0,要么是2的某个非负整数次幂。 而直接将数组中的非零相邻元素相乘,计算量较大,并且可能产生过大的整数从而导致溢出(对C++等语言来说)。 为了避免这一问题,我们可以
- 将非零元素表示为其以2为底的对数,如256表示为;
- 将比较相邻非零元素乘积转化为比较相邻非零元素的对数表示之和。
为了避免重复计算,每当首次计算某个元素的以2为底的对数时,记录其结果。 这样下次遇到相同元素时就可以直接使用已有转换结果。
题目要求找到产生最大乘积的(闭)区间,因而我们首先需要声明两个整数变量left和right。
left和right均初始化为数组中首个元素的索引,即0。设符合要求的最大部分和为max_sum;考虑到正整数的对数不可能为负,max_sum可以初始化为-1。使用curr_sum记录遍历中累计的部分和;数组中的每个“部分”是不含“0”且由“0”分割开的区间,则每当开始访问某个“部分”时,curr_sum应当重置为0(相同底数的乘法运算就是指数部分的加法运算,而)。遍历数组,访问到索引为的元素arr[i]时:
- 若
arr[i]不为0,则计算其以2为底的对数,- 如果不为0(即
arr[i]不为1),那么将部分和curr_sum加上arr[i],right更新为i,否则忽略(因为任何数乘以1都是此数本身,而题目要求选择y更小的区间);
- 如果不为0(即
- 如果
arr[i]为0,那么- 若
curr_sum>max_sum,则max_sum=curr_sum,x、y分别更新为left、right; curr_sum清零,left更新为i+1;
- 若
遍历结束时,还要再检查一次是否有max_sum<curr_sum。
最后,由于数组的起始位置为 1,结束位置为 n,我们需要将left和right分别加1后再作为结果返回。
上述方法具有时间复杂度。
题解
import math
from functools import cache
@cache
def log2(elem: int) -> int:
return int(math.log2(elem))
def solution(n: int, arr: list[int]) -> list[int]:
left, right = 0, 0
best_left, best_right = 0, 0
max_sum, curr_sum = -1, 0
for i in range(n):
if elem := arr[i]:
if log := log2(elem):
right = i
curr_sum += log
else:
if curr_sum > max_sum:
max_sum = curr_sum
best_left, best_right = left, right
curr_sum = 0
left = i + 1
if curr_sum > max_sum:
best_left, best_right = left, right
return [best_left + 1, best_right + 1]