LeetCode 446 - 等差数列划分 II - 子序列


文章目录

摘要

这一题看上去名字挺长,但核心其实就是:在一个数组里找出所有「长度至少为 3」的等差子序列数量。不要小看它,难度可是 Hard,而且非常考察你对动态规划的理解。

为什么?因为它不是简单找连续子数组,而是「子序列」,可以跳着选元素,中间跳几个都行。再加上数组长度上限可达 1000,如果暴力三层循环穷举组合,只能得到超时的命运。

这篇文章会带你完整跑一遍思路,从为什么要用 DP,到数据结构如何设计,再到 Swift 可运行 Demo 代码,让你不仅懂得答案,还能真的写出来。

描述

题目让我们从一个整数数组 nums 中找出所有 等差子序列 的数量,并且要求子序列的长度要 至少为 3

什么是等差子序列?

很简单:

  • 每两个相邻元素的差是相同的。

典型例子:

  • [2,4,6]
  • [2,4,6,8]
  • [2,6,10]
  • [7,7,7] (可以等差,差为 0)

但是这里最难的点不是判断一个序列是不是等差,而是:

我们要统计 所有可能的等差子序列数量

而且是「子序列」,意味着:

  • 可以跳着选元素
  • 不要求连续
  • 只要保持顺序就行

题目最后给的答案也会很大,但它保证在 32-bit 整数范围内。

题解答案(核心思路)

这道题最关键的问题是:如何在 O(n²) 的范围内统计所有等差子序列?

核心 DP 思路如下:

  1. 对于每个下标 i,我们维护一个字典 dp[i],里面记录所有可能差值对应的等差子序列数量。

  2. 对于每一对 (j, i)j < i

    • 计算 diff = nums[i] - nums[j]

    • dp[i][diff] += dp[j][diff] + 1

      • +1 是因为 (nums[j], nums[i]) 本身就是一个长度为 2 的"潜在"等差子序列
  3. 所有 dp[j][diff] 都代表"已形成的等差子序列(长度至少 2)",加在一起,最终形成的等差子序列长度均 ≥ 3,可以加入结果。

一个非常容易忽略的重要点:

  • +1 形成的是长度 为 2 的序列,不算进最终结果
  • 只有 dp[j][diff] 才是长度 ≥ 2 的,所以这些才会继续累加到答案中

题解代码分析

下面给出完整可运行的 Swift Demo 代码。

我专门把关键逻辑写得清晰一点,方便你理解每一步怎么做。

可运行 Demo(Swift)

swift 复制代码
import Foundation

class Solution {
    func numberOfArithmeticSlices(_ nums: [Int]) -> Int {
        let n = nums.count
        if n < 3 { return 0 }
        
        // dp[i]:一个字典,key 为差值 diff,value 为以 nums[i] 结尾、差为 diff 的等差子序列数量(长度至少为 2)
        var dp = Array(repeating: [Int: Int](), count: n)
        var result = 0
        
        for i in 0..<n {
            for j in 0..<i {
                // diff 可能很大,使用 Int64 避免溢出
                let diff = Int64(nums[i]) - Int64(nums[j])
                
                // 从 dp[j][diff] 拿到以前的数量(长度至少为 2 的子序列)
                let count = dp[j][Int(diff)] ?? 0
                
                // 更新 dp[i][diff]
                // +1 表示 (nums[j], nums[i]) 这一对作为长度=2 的新等差序列
                dp[i][Int(diff), default: 0] += count + 1
                
                // count 是长度至少为 2 的子序列,它们现在被延长,形成长度 >= 3 ,加入结果
                result += count
            }
        }
        
        return result
    }
}

// Demo 入口
func demo() {
    let solution = Solution()
    
    let nums1 = [2,4,6,8,10]
    print("输入: \(nums1) -> 输出: \(solution.numberOfArithmeticSlices(nums1))")
    
    let nums2 = [7,7,7,7,7]
    print("输入: \(nums2) -> 输出: \(solution.numberOfArithmeticSlices(nums2))")
    
    let nums3 = [1,1,2,5,7]
    print("输入: \(nums3) -> 输出: \(solution.numberOfArithmeticSlices(nums3))")
}

demo()

题解代码分析(详细分解)

我们逐行解释整个算法。

1. dp 数组的含义

swift 复制代码
var dp = Array(repeating: [Int: Int](), count: n)

这里的 dp[i] 是一个字典,保存了:

  • key = 等差的差值 diff
  • value = 以 nums[i] 结尾、差为 diff 的等差子序列数量(长度至少 2)

比如:

如果 dp[5][2] = 3

说明以 nums[5] 结尾,公差为 2 的等差子序列(长度 ≥ 2)有 3 个。

2. 遍历所有 j < i

swift 复制代码
for i in 0..<n {
    for j in 0..<i {
        ...
    }
}

每次都是用前面的数字 nums[j] 来尝试拼到 nums[i] 的后面。

3. 计算差值 diff

swift 复制代码
let diff = Int64(nums[i]) - Int64(nums[j])

这里用 Int64 是因为题目里的数字可能会超出 Int32 的范围,而 Swift 的 Int 默认是 64-bit,但我们为了安全还是显式处理一下。

4. 从 dp[j] 查看能够延续的子序列数量

swift 复制代码
let count = dp[j][Int(diff)] ?? 0

如果 count > 0,说明以前已经存在:

... , nums[j] 且公差为 diff 的等差序列

那么现在 nums[i] 出现了,这些序列可以继续延伸形成长度 ≥ 3 的序列。

5. 更新 dp[i][diff]

swift 复制代码
dp[i][Int(diff), default: 0] += count + 1

这里的逻辑是:

  • count 是来自 dp[j] 的旧等差子序列(长度 ≥ 2)
  • +1 是 (nums[j], nums[i]) 本身作为新生成的长度为 2 的等差序列

所以更新后的 dp[i] 会包含所有可能情况。

6. 把 count 累加到最终结果

swift 复制代码
result += count

为什么不是 count + 1?

因为题目要求序列长度 ≥ 3

长度为 2 的等差子序列不计入结果

count 是长度 ≥ 2 的序列,可以被延长成长度 ≥ 3,所以它们都有效。

示例测试及结果

运行 Demo 得到如下输出:

txt 复制代码
输入: [2,4,6,8,10] -> 输出: 7
输入: [7,7,7,7,7] -> 输出: 16
输入: [1,1,2,5,7] -> 输出: 0

解释:

示例 1

数组 [2,4,6,8,10] 有很多等差子序列,比如:

  • 2,4,6

  • 4,6,8

  • 6,8,10

  • 2,4,6,8

  • 4,6,8,10

  • 2,4,6,8,10

  • 2,6,10

总共 7 个。

示例 2

数组 [7,7,7,7,7]

所有子序列都是等差(差为 0),结果为 16。

示例 3

数组 [1,1,2,5,7]

没有形成长度 ≥ 3 的等差子序列,结果为 0。

时间复杂度

整体双层循环:

  • 外层 i,内层 j → O(n²)

dp 字典查找为均摊 O(1)

总时间复杂度:

O(n²)

在 n = 1000 情况下可接受。

空间复杂度

dp 数组大小大约为:

  • n 个字典
  • 差值数量不超过 n

平均空间:

O(n²)

虽然字典没那么满,但最坏情况下确实是 n² 级别。

总结

LeetCode 446 是一道非常经典的 Hard 动态规划题目,不仅考察 DP 思维,还考察你对"子序列"这个概念的理解。

整道题的关键点在于:

  1. 使用 dp[i][diff] 记录以 nums[i] 结尾、差为 diff 的等差子序列数量
  2. 每次组合 (j, i) 时,用 dp[j][diff] 来扩展
  3. +1 代表新的长度为 2 的子序列
  4. 只有 dp[j][diff] 才能贡献到最终答案
  5. 时间 O(n²) 空间 O(n²)
相关推荐
AI科技星3 分钟前
张祥前统一场论宇宙大统一方程的求导验证
服务器·人工智能·科技·线性代数·算法·生活
Fuly102443 分钟前
大模型剪枝(Pruning)技术简介
算法·机器学习·剪枝
Xの哲學1 小时前
Linux网卡注册流程深度解析: 从硬件探测到网络栈
linux·服务器·网络·算法·边缘计算
bubiyoushang8881 小时前
二维地质模型的表面重力值和重力异常计算
算法
仙俊红1 小时前
LeetCode322零钱兑换
算法
颖风船1 小时前
锂电池SOC估计的一种算法(改进无迹卡尔曼滤波)
python·算法·信号处理
551只玄猫2 小时前
KNN算法基础 机器学习基础1 python人工智能
人工智能·python·算法·机器学习·机器学习算法·knn·knn算法
charliejohn2 小时前
计算机考研 408 数据结构 哈夫曼
数据结构·考研·算法
POLITE32 小时前
Leetcode 41.缺失的第一个正数 JavaScript (Day 7)
javascript·算法·leetcode