最大乘积区间问题 | 豆包MarsCode AI刷题

140 阅读3分钟

问题描述

小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表示为log2256=8\log_2256=8
  • 将比较相邻非零元素乘积转化为比较相邻非零元素的对数表示之和。

为了避免重复计算,每当首次计算某个元素的以2为底的对数时,记录其结果。 这样下次遇到相同元素时就可以直接使用已有转换结果。

题目要求找到产生最大乘积的(闭)区间,因而我们首先需要声明两个整数变量leftrightleftright均初始化为数组中首个元素的索引,即0。设符合要求的最大部分和为max_sum;考虑到正整数的对数不可能为负,max_sum可以初始化为-1。使用curr_sum记录遍历中累计的部分和;数组中的每个“部分”是不含“0”且由“0”分割开的区间,则每当开始访问某个“部分”时,curr_sum应当重置为0(相同底数的乘法运算就是指数部分的加法运算,而log21=0\log_21=0)。遍历数组,访问到索引为ii的元素arr[i]时:

  • arr[i]不为0,则计算其以2为底的对数ll
    • 如果ll不为0(即arr[i]不为1),那么将部分和curr_sum加上arr[i]right更新为i,否则忽略(因为任何数乘以1都是此数本身,而题目要求选择y更小的区间);
  • 如果arr[i]为0,那么
    • curr_sum>max_sum,则max_sum=curr_sumxy分别更新为leftright
    • curr_sum清零,left更新为i+1

遍历结束时,还要再检查一次是否有max_sum<curr_sum

最后,由于数组的起始位置为 1,结束位置为 n,我们需要将leftright分别加1后再作为结果返回。

上述方法具有时间复杂度O(n)O(n)

题解

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]