【LeetCode】300. 最长递增子序列

leetcode

 

解题思路

寻找最长严格递增子序列(LIS)是动态规划和贪心算法的经典问题。核心思路如下:

  1. ​​动态规划解法(O(n²))​​:

    • 定义 dp[i] 表示以 nums[i] 结尾的 LIS 长度。
    • 对每个 i,遍历 j∈[0, i-1],若 nums[j] < nums[i],则更新 dp[i] = max(dp[i], dp[j]+1)
    • 最终结果为 dp 数组的最大值。
  2. ​​贪心+二分优化(O(n log n))​​:

    • 维护数组 d[],其中 d[k] 表示长度为 k 的 LIS 的最小末尾值。
    • 遍历数组,对每个数用二分查找在 d 中定位:
      • 若大于所有 d[k],则扩展序列长度。
      • 否则替换 d 中第一个大于等于它的值,保持序列增长潜力。

关键步骤

动态规划

  1. ​​初始化​​:dp 数组全设为 1(每个元素自身是长度为 1 的子序列)。
  2. ​​双层循环​​:
    • 外层遍历 i∈[1, n-1]
    • 内层遍历 j∈[0, i-1],若 nums[j] < nums[i],则更新 dp[i] = max(dp[i], dp[j]+1)
  3. ​​取最大值​​:遍历 dp 数组,返回最大值。

贪心+二分

  1. ​​初始化​​:空数组 d,长度 len=0
  2. ​​遍历数组​​:
    • 若 nums[i] > d[len],则 d = append(d, nums[i])len++
    • 否则二分查找 d 中第一个 ≥ nums[i] 的位置 pos,更新 d[pos] = nums[i]
  3. ​​返回结果​​:len 即 LIS 长度。

代码实现

方法 1:动态规划(O(n²))

func lengthOfLIS_DP(nums []int) int {
    n := len(nums)
    if n == 0 {
        return 0
    }
    dp := make([]int, n) // dp[i]: 以 nums[i] 结尾的 LTS 长度
    maxLen := 1

    for i := 0; i < n; i++ {
        dp[i] = 1 // 初始化: 每个元素自身是长度为1的子序列
        for j := 0; j < i; j++ {
            if nums[j] < nums[i] {
                if dp[j]+1 > dp[i] {
                    dp[i] = dp[j] + 1 // 状态转移
                }
            }
        }
        if dp[i] > maxLen {
            maxLen = dp[i] // 更新全局最大值
        }
    }
    return maxLen
}

方法 2:贪心+二分(O(n log n))

func lengthOfLIS_Greedy(nums []int) int {
    d := make([]int, 0) // d[k]: 长度为 k 的 LIS 的最小末尾值
    for _, num := range nums {
        // 若 num 大于所有 d 中的值,则扩展序列
        if len(d) == 0 || num > d[len(d)-1] {
            d = append(d, num)
        } else {
            // 二分查找第一个 ≥ num 的位置
            pos := sort.SearchInts(d, num)
            d[pos] = num // 替换 d[pos],保持最小末尾值
        }
    }
    return len(d) // d 的长度即 LIS 长度
}

示例测试

func main() {
    tests := []struct {
        nums []int
        want int
    }{
        {[]int{10, 9, 2, 5, 3, 7, 101, 18}, 4}, // 示例1: [2,3,7,101]
        {[]int{0, 1, 0, 3, 2, 3}, 4},           // 示例2: [0,1,2,3]
        {[]int{7, 7, 7, 7, 7, 7, 7}, 1},        // 示例3: [7]
    }
    for _, test := range tests {
        res1 := lengthOfLIS_DP(test.nums)
        res2 := lengthOfLIS_Greedy(test.nums)
        fmt.Printf("输入: %v\n动态规划: %d\n贪心+二分: %d (期望: %d)\n\n",
            test.nums, res1, res2, test.want)
    }
}

复杂度分析

​​方法​​时间复杂度空间复杂度适用场景
动态规划(DP) O(n²) O(n) 代码简单,n ≤ 10³
贪心+二分(优化) ​​O(n log n)​​ O(n) 高效,n ≤ 10⁵

关键点

  1. ​​动态规划本质​​:

    • dp[i] 依赖子问题 dp[j]j < i),需遍历所有子问题。
    • 状态转移方程:dp[i] = max(dp[i], dp[j] + 1)(当 nums[j] < nums[i])。
  2. ​​贪心策略​​:

    • 维护最小末尾值数组 d[],使序列增长尽可能慢。
    • 二分查找(sort.SearchInts)确保定位效率(O(log n))。
  3. ​​二分查找细节​​:

    • 替换 d[pos] 保证相同长度下末尾值最小,提升后续扩展可能性。
    • 不改变 d 的严格递增性(数学可证)。
  4. ​​适用性​​:

    • DP 适合小规模数据(n ≤ 2000)。
    • 贪心+二分适用于大规模数据(n ≤ 10⁵),如 LeetCode 最大测试用例(n = 2500)。
posted @ 2025-06-21 12:26  云隙之间  阅读(63)  评论(0)    收藏  举报