青训营X豆包MarsCode 技术训练营 | 豆包MarsCode AI 刷题 | Java区间动态规划问题解析——最大乘积子区间问题

73 阅读4分钟

在算法学习中,区间问题常常是动态规划的重点之一。今天我们来解决一道关于区间最大乘积的问题,并深入探讨如何通过优化算法设计来高效解决它。


题目描述

问题背景

给定一个长度为 nn 的数组,数组元素仅由集合 [0,1,2,4,8,16,32,64,128,256,512,1024][0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024] 中的值组成。要求在数组中找到一段连续的子区间,使其乘积最大。

输出要求

如果有多个区间满足最大乘积的条件:

  1. 优先选择左端点 xx 较小的区间。
  2. 如果左端点相同,则选择右端点 yy 较小的区间。

样例解析

  1. 输入样例 1

    输入:
    5
    1 2 4 0 8
    输出:
    1 3
    
    • 区间 [1,3][1, 3] 和 [5,5][5, 5] 的乘积均为 8。
    • 选择左端点较小的 [1,3][1, 3]。
  2. 输入样例 2

    输入:
    7
    1 2 4 8 0 256 0
    输出:
    6 6
    
    • 区间 [6,6][6, 6] 的乘积为 256,为最大乘积。

解题思路

为了解决这个问题,我们需要处理以下几个关键点:

  1. 最大乘积的计算

    • 当数组中没有 00 时,所有区间都有效,使用滑动窗口即可快速找到最大乘积。
    • 当数组中存在 00 时,区间的乘积会被分割,需要在每个非零子数组中分别计算。
  2. 如何避免乘积溢出

    • 由于输入数字都是 2k2^k 的形式,乘积可以用对数来处理。将乘法转化为加法,可以避免溢出。
  3. 最优区间的选择

    • 在找到多个相同乘积的区间时,优先选择左端点最小的区间。
  4. 算法优化

    • 使用动态规划或前缀积思想优化计算,避免暴力枚举。

代码实现

下面是基于上述思路的具体代码实现:

import java.util.Arrays;

public class Main {
    public static int[] solution(int n, int[] arr) {
        int maxProduct = 0;  // 最大乘积
        int start = 0, end = 0;  // 记录最大乘积区间的起始和结束位置
        int currentProduct = 1;  // 当前区间的乘积
        int tempStart = 0;  // 临时记录当前区间起点

        for (int i = 0; i < n; i++) {
            if (arr[i] == 0) {  // 碰到0,重新开始计算
                currentProduct = 1;
                tempStart = i + 1;
                continue;
            }

            currentProduct *= arr[i];

            if (currentProduct > maxProduct) {
                maxProduct = currentProduct;
                start = tempStart;
                end = i;
            } else if (currentProduct == maxProduct) {
                // 如果乘积相同,优先选择左端点小的
                if (tempStart < start || (tempStart == start && i < end)) {
                    start = tempStart;
                    end = i;
                }
            }
        }

        return new int[]{start + 1, end + 1};  // 转为从1开始的索引
    }

    public static void main(String[] args) {
        System.out.println(Arrays.equals(solution(5, new int[]{1, 2, 4, 0, 8}), new int[]{1, 3}));
        System.out.println(Arrays.equals(solution(7, new int[]{1, 2, 4, 8, 0, 256, 0}), new int[]{6, 6}));
    }
}

代码解析

  1. 初始化变量

    • maxProduct:记录当前最大乘积。
    • currentProduct:记录当前区间的乘积,初始化为 1。
    • startend:记录最大乘积区间的起止索引。
    • tempStart:用于暂时记录当前区间的起点。
  2. 遍历数组

    • 遇到 0 时,将乘积重置为 1,同时更新临时起点为下一位置。
    • 否则,将当前元素乘入 currentProduct,更新最大乘积以及对应的区间索引。
  3. 区间选择

    • currentProduct == maxProduct 时,比较起点 tempStart 和终点 end,确保选择最优区间。
  4. 返回结果

    • 最后输出的是从 1 开始的索引,因此需要将索引加 1。

边界条件

  • 只有一个元素:如 n = 1 时,直接输出 [1, 1]
  • 全为零的数组:乘积为零,无需处理区间选择。
  • 多个区间乘积相同:根据题目要求,选择最左侧起点最小的区间。

测试用例分析

  1. 测试用例 1: 输入:5, [1, 2, 4, 0, 8]
    输出:[1, 3]
    解释:区间 [1,3][1, 3] 的乘积为 8,最优解。

  2. 测试用例 2: 输入:7, [1, 2, 4, 8, 0, 256, 0]
    输出:[6, 6]
    解释:区间 [6,6][6, 6] 的乘积为 256,最大乘积且最左。

  3. 边界测试

    • 输入:1, [0],输出:[1, 1]
    • 输入:3, [0, 0, 0],输出:[1, 1]

时间复杂度分析

  1. 遍历数组

    • 主循环遍历一次数组,时间复杂度为 O(n)O(n)。
  2. 更新乘积与区间

    • 每次更新操作为常数时间,复杂度为 O(1)O(1)。

总时间复杂度为 O(n)O(n),空间复杂度为 O(1)O(1),非常高效。


总结与反思

这道题目虽然看似简单,但它涉及到多个关键点的处理:

  1. 通过乘积转化对区间的理解。
  2. 优化边界条件的判断。
  3. 在同乘积条件下的区间最优选择。

在实际开发中,这种思维方式可以应用于很多类似的问题,比如最大子数组和、连续子数组乘积等。希望这篇博客能对你有所启发! 😊