leetcode_day8_筑基期_《绝境求生》

22 阅读10分钟

目录


前言

碎碎念:猴 鸡 猪 鼠 白色银色,黑色蓝色

本系列《绝境求生》记录转码算法筑基过程,以代码随想录为纲学习,leetcode_hot_100练手,在此记录思考过程,方便过后复现。内容比较粗糙仅便于笔者厘清思路,复盘总结。

此篇后,改变了文章结构


提示:以下是本篇文章正文内容

动态规划

递归+缓存。  空间换时间。  算过的记下来,下次用到的时候直接找,不用再算

一、300. 最长递增子序列LIS

1、题目描述

给你一个整数数组 nums,找到其中最长严格递增子序列的长度。关键定义

  • 子序列:由数组派生而来,删除(或不删除)数组中的元素但不改变剩余元素的相对顺序(元素可不连续);
  • 严格递增:子序列中后一个元素必须大于前一个元素(如 [2,3,7] 是递增,[2,2,7] 不是)。

示例

  • 示例 1:输入 nums = [10,9,2,5,3,7,101,18] → 输出 4

    • 解释:最长严格递增子序列是 [2,3,7,101](或 [2,5,7,101]/[2,3,7,18]),长度为 4。
  • 示例 2:输入 nums = [0,1,0,3,2,3] → 输出 4

    • 解释:最长子序列是 [0,1,3,3](错误,严格递增应为 [0,1,2,3])→ 正确最长是 [0,1,3,3] 不符合严格递增,实际是 [0,1,3] 或 [0,1,2,3],长度 4。
  • 示例 3:输入 nums = [7,7,7,7,7] → 输出 1

    • 解释:严格递增要求元素必须更大,因此只能选一个 7,长度为 1。

提示

  • 1 <= nums.length <= 2500
  • -10^4 <= nums[i] <= 10^4

2、简单理解?

以某个元素为结尾的最长递增子序列

3、暴力法

3.1、能不能用图示意?

# 初始化dp数组(长度8,每个元素初始为1)
dp = [1, 1, 1, 1, 1, 1, 1, 1]

# 逐i计算dp[i]
i=0(nums[0]=10):无j<0 → dp[0]=1
i=1(nums[1]=9):j=010>9)→ dp[1]=1
i=2(nums[2]=2):j=0(10>2)、j=1(9>2) → dp[2]=1
i=3(nums[3]=5):j=0(10>5)、j=1(9>5)、j=2(2<5) → dp[3] = dp[2]+1=2
i=4(nums[4]=3):j=0(10>3)、j=1(9>3)、j=2(2<3→dp=2)、j=3(5>3) → dp[4]=2
i=5(nums[5]=7):j=0(10>7)、j=1(9>7)、j=2(2<7→dp=2)、j=3(5<7→dp=3)、j=4(3<7→dp=3) → dp[5]=3
i=6(nums[6]=101):j=0-5均<101,最大dp[j]=3 → dp[6]=4
i=7(nums[7]=18):j=0-5最大dp[j]=3(j=57<18),j=6(101>18) → dp[7]=4

# 最终dp数组:[1,1,1,2,2,3,4,4] → 最大值为4

3.2、初始化条件?

dp[i]初试化为1,因为每一个元素自身是长度为1的子序列

3.3、边界条件?

如果长度为1  直接返回1

3.4、代码逻辑?

状态定义dp[i ]表示   以num[i]为最长严格递增子序列长度

dp数组下标代表nums里每个数的位置。 对应的值代表子序列长度

状态转移方程:对于每个i, 遍历所有j<i:

        若nums[j]<nums[i] 满足严格递增,则dp[i]=max(dp[i],dp[j]+1)在j的最长子序列后加nums[i]
若nums[j]>nums[i]不满足递增  则跳过

返回所有位置的最长递增子序列

3.5、之前见过但没注意到的?

返回所有位置的最长递增子序列

  • 最终结果是 max(dp) 而非 dp[-1]:最长子序列不一定以最后一个元素结尾(比如示例中 dp [7]=4,dp [6]=4,最大值是 4);

3.6、疑惑点/新知识 ?

初始化为0,初始化为无穷大 float('inf'),初始化为bool,初始化为1,初始化为空

3.7、python 代码

def solution(nums):
    n=len(nums)  #感觉很经典
    if n==1:
        return 1
    dp=[1]*(n)
    #遍历每个元素i
    for i in range(1,n):
        for j in range(i):
            if nums[j]<nums[i]:
                # 状态转移:取当前dp[i]和dp[j]+1的最大值
                dp[i]=max(dp[i],dp[j]+1)
    return max(dp)

 一、152乘积最大子数组

1、题目描述

给你一个整数数组 nums,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。关键定义

  • 子数组:数组中连续的一段元素(区别于子序列,子序列可不连续);
  • 乘积特性:负数 × 负数 = 正数,负数 × 正数 = 负数,0 会重置乘积为 0,这是解题的核心难点。

示例

  • 示例 1:输入 nums = [2,3,-2,4] → 输出 6

    • 解释:连续子数组 [2,3] 的乘积是 6,是最大的([2,3,-2,4] 乘积 =-48,[-2,4] 乘积 =-8)。
  • 示例 2:输入 nums = [-2,0,-1] → 输出 0

    • 解释:最大乘积是 0(子数组 [-2,0] 或 [0] 或 [0,-1][-1] 乘积 =-1 更小)。
  • 示例 3:输入 nums = [-2,3,-4] → 输出 24

    • 解释:连续子数组 [-2,3,-4] 的乘积 =(-2)×3×(-4)=24,是最大的(负负得正让最小值反转为最大值)。

提示

  • 1 <= nums.length <= 2 * 10^4
  • -10 <= nums[i] <= 10
  • nums 的任何前缀或后缀的乘积都不会超过 2^31 - 1

2、简单理解?

负数的存在会反转乘积的大小关系(当前的负数最小值乘与一个负数可能会反转成最大值)

3、暴力法

3.1、能不能用图示意?

nums = [2,3,-2,4] 为例
枚举所有子数组:
i=0(起始为2):
  j=0 → 乘积=2;j=12×3=6;j=26×(-2)=-12;j=3→-12×4=-48 → 最大值6
i=1(起始为3):
  j=13;j=23×(-2)=-6;j=3→-6×4=-24 → 最大值3
i=2(起始为-2):
  j=2→-2;j=3→-2×4=-8 → 最大值-2
i=3(起始为4):
  j=34 → 最大值4
所有子数组乘积的最大值:6

3.2、初始化条件?

初始化最大乘积为负无穷,确保能被更小的数更新。  因为最大乘积可能是负数,所以初试不为0。

3.3、边界条件?

剪枝 数组长度为1时返回第一个元素

3.4、代码逻辑?

暴力枚举所有可能的子数组

外层循环:遍历所有的起始位置

内层循环,从起始位置到末尾,计算连续子数组的乘积

current_prod 累计乘积 

max_prod=max(max_prod, current_prod ) 更新全局最大的乘积

3.5、之前见过但没注意到的?

双指针一般是要满足单调规律才用。 子内部混乱,无序,随机是不适用的

跟最长子序列的区别是,最长子序列可以跳数,最大乘积子数组必须是连续的

3.6、疑惑点/新知识 ?

3.7、python 代码

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        n = len(nums)
        # 边界条件:数组长度为1时,直接返回自身
        if n == 1:
            return nums[0]
        
        # 初始化最大乘积为负无穷(确保能被更小的数更新)
        max_prod = float('-inf')
        
        # 外层循环:枚举子数组的起始位置i
        for i in range(n):
            # 初始化当前子数组的乘积为1(每次起始位置重置)
            current_prod = 1
            # 内层循环:枚举子数组的结束位置j(从i到n-1)
            for j in range(i, n):
                # 计算当前子数组nums[i..j]的乘积
                current_prod *= nums[j]
                # 更新最大乘积
                if current_prod > max_prod:
                    max_prod = current_prod
        
        return max_prod

4、优化法

  • 状态定义

    • dp_max[i]:以 nums[i] 结尾的连续子数组的最大乘积
    • dp_min[i]:以 nums[i] 结尾的连续子数组的最小乘积(应对负数反转的情况)。
  • 状态转移方程:对于每个 i,需比较三个值:nums[i] 本身、dp_max[i-1]×nums[i]dp_min[i-1]×nums[i]

    • dp_max[i] = max(nums[i], dp_max[i-1] * nums[i], dp_min[i-1] * nums[i])
    • dp_min[i] = min(nums[i], dp_max[i-1] * nums[i], dp_min[i-1] * nums[i])

 4.1、能不能用图示意?

nums = [-2,3,-4] 为例
初始化:
cur_max = -2(dp_max[0]),cur_min = -2(dp_min[0]),result = -2

遍历i=1(nums[1]=3):
临时保存cur_max(避免更新时覆盖):temp_max = -2
cur_max = max(3, temp_max×3=-6, cur_min×3=-6) → 3
cur_min = min(3, temp_max×3=-6, cur_min×3=-6) → -6
result = max(-2, 3) → 3

遍历i=2(nums[2]=-4):
temp_max = 3
cur_max = max(-4, temp_max×(-4)=-12, cur_min×(-4)=24) → 24
cur_min = min(-4, temp_max×(-4)=-12, cur_min×(-4)=24) → -12
result = max(3, 24) → 24

最终result=24

4.2、初始化条件?

dp_max[0]=dp_min[0]=nums[0]  第一个元素的最大最小值都是自身

4.3、边界条件?

剪枝 target=0 or 1 的情况

4.4、代码逻辑?

索引i 表示第几个元素,dp_max[i]和dp_min[i]的值是以i 为结尾的最大、最小数组乘积值

其实 dp_max[i] 和 dp_min[i] 仅需前一个值,可以用变量 cur_maxcur_min 代替数组,空间复杂度从 O (n) 降到 O (1)。

  • temp_max = cur_max:必须先保存当前的 cur_max!因为更新 cur_min 时需要原始的 cur_max,若先更新 cur_max,会导致 cur_min 计算错误(比如 nums [i] 是负数时,cur_min 需要用更新前的 cur_max×nums [i]);

  • cur_max = max(nums[i], temp_max * nums[i], cur_min * nums[i])

    • 选 nums[i]:表示重新开始一个子数组(比如前一个乘积是 0 或负数,放弃前面的子数组);
    • 选 temp_max * nums[i]:前一个最大乘积 × 当前元素(默认当前元素为正);
    • 选 cur_min * nums[i]:前一个最小乘积 × 当前元素(默认当前元素为负,负负得正);
  • cur_min = min(...):逻辑同 cur_max,取三者最小值,为后续负数反转做准备;

  • result = max(result, cur_max):每次更新全局最大值(以当前元素结尾的最大乘积可能是全局最大)。

  • 无需处理 0 的特殊情况:当 nums [i]=0 时,cur_max 和 cur_min 都会被重置为 0(max (0, ...)=0,min (0, ...)=0),符合逻辑。

4.5、之前见过但没注意到的?

4.6、疑惑点/新知识 ?

第一次见到用两个动态方程的解决问题场景。 同时维护了dp_max和dp_min

和的问题无序维护最小值,但是乘积问题必须维护,运算特性!!!!!!

4.7、python 代码

from typing import List

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        n = len(nums)
        # 边界条件:数组长度为1时,直接返回自身
        if n == 1:
            return nums[0]
        
        # 步骤1:初始化
        # cur_max:以当前元素结尾的子数组的最大乘积(对应dp_max[i])
        # cur_min:以当前元素结尾的子数组的最小乘积(对应dp_min[i])
        cur_max = nums[0]
        cur_min = nums[0]
        # result:全局最大乘积(初始为第一个元素,后续逐步更新)
        result = nums[0]
        
        # 步骤2:遍历数组(从第二个元素开始)
        for i in range(1, n):
            # 关键:临时保存cur_max,因为更新cur_min时需要原始的cur_max
            temp_max = cur_max
            
            # 步骤3:更新cur_max和cur_min
            # cur_max = max(当前元素本身, 前一个最大×当前元素, 前一个最小×当前元素)
            cur_max = max(nums[i], temp_max * nums[i], cur_min * nums[i])
            # cur_min = min(当前元素本身, 前一个最大×当前元素, 前一个最小×当前元素)
            cur_min = min(nums[i], temp_max * nums[i], cur_min * nums[i])
            
            # 步骤4:更新全局最大乘积
            if cur_max > result:
                result = cur_max
        
        # 步骤5:返回全局最大乘积
        return result


偏神太多:伤官,偏财,七杀,羊刃,伤官配印,己土身旺,日坐七杀,杀印相生