动态规划 算法(二)

LeetCode 53. 最大子数组和

问题描述

给定一个整数数组 nums,找到一个具有最大和的连续子数组(子数组至少包含一个元素),返回其最大和。

示例

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6。

算法思路

动态规划(DP)

  1. 状态定义
    • dp[i] 表示以 nums[i] 结尾的连续子数组的最大和。
  2. 状态转移
    • 对于当前元素 nums[i],有两种选择:
      • nums[i] 加入前一个子数组(dp[i-1] + nums[i])。
      • nums[i] 作为新子数组的起点(nums[i])。
    • 状态转移方程:
      dp[i] = max(nums[i], dp[i-1] + nums[i])
  3. 结果更新
    • 遍历过程中记录全局最大值 maxSum

空间优化

  • 由于 dp[i] 仅依赖于 dp[i-1],可用单个变量 currentSum 替代 DP 数组。

代码实现

方法一:动态规划(DP 数组)

class Solution {
    public int maxSubArray(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        
        int n = nums.length;
        // dp[i] 表示以 nums[i] 结尾的连续子数组的最大和
        int[] dp = new int[n];
        dp[0] = nums[0];  // 初始化第一个元素
        int maxSum = dp[0];  // 全局最大和
        
        for (int i = 1; i < n; i++) {
            // 状态转移:选择当前元素单独成组 或 加入前一个子数组
            dp[i] = Math.max(nums[i], dp[i-1] + nums[i]);
            // 更新全局最大值
            maxSum = Math.max(maxSum, dp[i]);
        }
        return maxSum;
    }
}

方法二:动态规划(空间优化)

class Solution {
    public int maxSubArray(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        
        int currentSum = nums[0];  // 当前子数组和(空间优化版 dp[i])
        int maxSum = nums[0];      // 全局最大和
        
        for (int i = 1; i < nums.length; i++) {
            // 状态转移:选择当前元素单独成组 或 加入前一个子数组
            currentSum = Math.max(nums[i], currentSum + nums[i]);
            // 更新全局最大值
            maxSum = Math.max(maxSum, currentSum);
        }
        return maxSum;
    }
}

算法分析

  • 时间复杂度:O(n)
    只需遍历数组一次。
  • 空间复杂度
    • 方法一:O(n),使用 DP 数组存储中间状态。
    • 方法二:O(1),仅需两个变量。

算法过程

输入:nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]

  1. 初始化
    • currentSum = -2, maxSum = -2
  2. i=1(元素 1):
    • currentSum = max(1, -2+1= -1) = 1
    • maxSum = max(-2, 1) = 1
  3. i=2(元素 -3):
    • currentSum = max(-3, 1-3= -2) = -2
    • maxSum = max(1, -2) = 1
  4. i=3(元素 4):
    • currentSum = max(4, -2+4= 2) = 4
    • maxSum = max(1, 4) = 4
  5. i=4(元素 -1):
    • currentSum = max(-1, 4-1= 3) = 3
    • maxSum = max(4, 3) = 4
  6. i=5(元素 2):
    • currentSum = max(2, 3+2= 5) = 5
    • maxSum = max(4, 5) = 5
  7. i=6(元素 1):
    • currentSum = max(1, 5+1= 6) = 6
    • maxSum = max(5, 6) = 6
  8. i=7(元素 -5):
    • currentSum = max(-5, 6-5= 1) = 1
    • maxSum = max(6, 1) = 6
  9. i=8(元素 4):
    • currentSum = max(4, 1+4= 5) = 5
    • maxSum = max(6, 5) = 6

最终结果:6

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1:标准示例
    int[] nums1 = {-2,1,-3,4,-1,2,1,-5,4};
    System.out.println("Test 1: " + solution.maxSubArray(nums1)); // 6
    
    // 测试用例2:全正数数组
    int[] nums2 = {1,2,3,4};
    System.out.println("Test 2: " + solution.maxSubArray(nums2)); // 10
    
    // 测试用例3:全负数数组
    int[] nums3 = {-3,-1,-2,-5};
    System.out.println("Test 3: " + solution.maxSubArray(nums3)); // -1
    
    // 测试用例4:单元素数组
    int[] nums4 = {5};
    System.out.println("Test 4: " + solution.maxSubArray(nums4)); // 5
    
    // 测试用例5:正负交替
    int[] nums5 = {3,-2,5,-1};
    System.out.println("Test 5: " + solution.maxSubArray(nums5)); // 6 (3-2+5=6)
    
    // 测试用例6:空数组
    int[] nums6 = {};
    System.out.println("Test 6: " + solution.maxSubArray(nums6)); // 0
}

关键点

  1. 状态转移逻辑
    • 若前序子数组和为负数,则放弃前序部分(currentSum = nums[i])。
    • 若前序子数组和为正数,则加入前序部分(currentSum += nums[i])。
  2. 负数处理
    • 当全为负数时,算法会自动选择最大的单个负数(如测试用例3)。
  3. 空间优化
    • 仅需保存前一个状态,无需完整 DP 数组。

常见问题

  1. 为什么空间优化时不需要数组?
    当前状态仅依赖前一个状态,用变量 currentSum 代替数组即可。
  2. 如何处理全负数数组?
    状态转移中的 Math.max() 会自动选择最大单个值(如 [-3,-1,-2]currentSum 会依次更新为 -3 → -1 → -2,最终 maxSum=-1)。
  3. 为何初始值设为 nums[0]
    子数组至少包含一个元素,因此初始状态就是第一个元素本身。

LeetCode 152. 乘积最大子数组

问题描述

给定一个整数数组 nums,找出乘积最大的非空连续子数组,并返回该子数组的乘积。

示例

输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6

输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。

算法思路

动态规划(DP)

  1. 问题:数组中存在负数时,负负得正可能导致最小值变最大值。
  2. 关键思路:同时维护两个 DP 数组:
    • maxDp[i]:以 nums[i] 结尾的子数组的最大乘积。
    • minDp[i]:以 nums[i] 结尾的子数组的最小乘积。
  3. 状态转移
    • 当前值可能是正数或负数,需考虑三种情况:
      • 当前值本身(nums[i])。
      • 当前值 × 前一个位置的最大乘积(maxDp[i-1] * nums[i])。
      • 当前值 × 前一个位置的最小乘积(minDp[i-1] * nums[i])。
    • 转移方程:
      maxDp[i] = max(nums[i], maxDp[i-1] * nums[i], minDp[i-1] * nums[i])
      minDp[i] = min(nums[i], maxDp[i-1] * nums[i], minDp[i-1] * nums[i])
      
  4. 结果更新:遍历过程中记录 maxDp 中的最大值。

代码实现

方法一:动态规划(DP 数组)

class Solution {
    public int maxProduct(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        int n = nums.length;
        // 创建两个 DP 数组:
        // maxDp[i] 表示以 nums[i] 结尾的子数组的最大乘积
        // minDp[i] 表示以 nums[i] 结尾的子数组的最小乘积
        int[] maxDp = new int[n];
        int[] minDp = new int[n];
        
        // 初始化:第一个元素的最大和最小乘积都是它本身
        maxDp[0] = nums[0];
        minDp[0] = nums[0];
        int maxProduct = nums[0]; // 全局最大乘积
        
        for (int i = 1; i < n; i++) {
            // 计算三种候选值:
            // 1. 当前数字本身
            // 2. 当前数字乘以前一个位置的最大乘积
            // 3. 当前数字乘以前一个位置的最小乘积
            int candidate1 = nums[i];                   // 当前元素自身
            int candidate2 = maxDp[i - 1] * nums[i];    // 乘以前一个最大乘积
            int candidate3 = minDp[i - 1] * nums[i];    // 乘以前一个最小乘积
            
            // 更新当前最大乘积:取三种候选值的最大值
            maxDp[i] = Math.max(candidate1, Math.max(candidate2, candidate3));
            // 更新当前最小乘积:取三种候选值的最小值
            minDp[i] = Math.min(candidate1, Math.min(candidate2, candidate3));
            
            // 更新全局最大乘积
            if (maxDp[i] > maxProduct) {
                maxProduct = maxDp[i];
            }
        }
        return maxProduct;
    }
}

方法二:动态规划(空间优化)

class Solution {
    public int maxProduct(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        // 使用变量代替 DP 数组:
        int maxSoFar = nums[0]; // 当前最大乘积
        int minSoFar = nums[0]; // 当前最小乘积
        int maxProduct = nums[0]; // 全局最大乘积
        
        for (int i = 1; i < nums.length; i++) {
            // 保存旧值(避免覆盖)
            int prevMax = maxSoFar;
            int prevMin = minSoFar;
            
            // 更新当前最大乘积(三种候选值)
            maxSoFar = Math.max(nums[i], Math.max(prevMax * nums[i], prevMin * nums[i]));
            // 更新当前最小乘积(三种候选值)
            minSoFar = Math.min(nums[i], Math.min(prevMax * nums[i], prevMin * nums[i]));
            
            // 更新全局最大乘积
            if (maxSoFar > maxProduct) {
                maxProduct = maxSoFar;
            }
        }
        return maxProduct;
    }
}

算法分析

  • 时间复杂度:O(n),遍历数组一次。
  • 空间复杂度
    • 方法一:O(n),使用两个 DP 数组。
    • 方法二:O(1),仅使用常数空间。

算法过程

输入:nums = [2, 3, -2, 4]

  1. 初始化
    • maxDp[0] = minDp[0] = 2maxProduct = 2
  2. i=1(元素 3):
    • candidate1 = 3
    • candidate2 = 2*3 = 6
    • candidate3 = 2*3 = 6
    • maxDp[1] = max(3,6,6)=6
    • minDp[1] = min(3,6,6)=3
    • maxProduct = max(2,6)=6
  3. i=2(元素 -2):
    • candidate1 = -2
    • candidate2 = 6*(-2) = -12
    • candidate3 = 3*(-2) = -6
    • maxDp[2] = max(-2, -12, -6) = -2
    • minDp[2] = min(-2, -12, -6) = -12
    • maxProduct = max(6, -2) = 6
  4. i=3(元素 4):
    • candidate1 = 4
    • candidate2 = -2*4 = -8
    • candidate3 = -12*4 = -48
    • maxDp[3] = max(4, -8, -48) = 4
    • minDp[3] = min(4, -8, -48) = -48
    • maxProduct = max(6, 4) = 6

最终结果:6

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例 1: 标准示例
    int[] nums1 = {2, 3, -2, 4};
    System.out.println("Test 1: " + solution.maxProduct(nums1)); // 6
    
    // 测试用例 2: 包含负数
    int[] nums2 = {-2, 0, -1};
    System.out.println("Test 2: " + solution.maxProduct(nums2)); // 0
    
    // 测试用例 3: 全负数数组
    int[] nums3 = {-2, -3, -1, -5};
    System.out.println("Test 3: " + solution.maxProduct(nums3)); // 30
    
    // 测试用例 4: 单个元素
    int[] nums4 = {5};
    System.out.println("Test 4: " + solution.maxProduct(nums4)); // 5
    
    // 测试用例 5: 多个负数和正数
    int[] nums5 = {2, -5, 3, 1, -2, 4};
    System.out.println("Test 5: " + solution.maxProduct(nums5)); // 240(整个数组乘积)
    
    // 测试用例 6: 空数组
    int[] nums6 = {};
    System.out.println("Test 6: " + solution.maxProduct(nums6)); // 0
    
    // 测试用例 7: 两个负数
    int[] nums7 = {-2, -3};
    System.out.println("Test 7: " + solution.maxProduct(nums7)); // 6
}

关键点

  1. 负数处理:负负得正,需同时维护最大/最小值。
  2. 状态转移
    • 最大乘积 = max(当前值, 前一个最大值×当前值, 前一个最小值×当前值)
    • 最小乘积 = min(当前值, 前一个最大值×当前值, 前一个最小值×当前值)
  3. 空间优化:只需前一个状态,用变量代替数组。
  4. 边界条件:空数组返回 0,单个元素返回自身。

常见问题

  1. 为什么需要同时维护最大值和最小值?
    负数可能将最小值变为最大值(如 [-2, -3]min=-2,遇到 -3-2*-3=6 成为最大值)。
  2. 空间优化时为何要保存旧值?
    maxSoFarminSoFar 的更新依赖前一个状态,需用临时变量保存避免覆盖。
  3. 遇到 0 时如何处理?
    nums[i]=0 时:
    • maxSoFar = max(0, 0, 0) = 0
    • minSoFar = min(0, 0, 0) = 0
      后续计算会从 0 重新开始。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值