深入解析: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)

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 数组的含义

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

for i in 0..<n {
for j in 0..<i {
...
}
}

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

3. 计算差值 diff

let diff = Int64(nums[i]) - Int64(nums[j])

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

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

let count = dp[j][Int(diff)] ?? 0

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

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

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

5. 更新 dp[i][diff]

dp[i][Int(diff), default: 0] += count + 1

这里的逻辑是:

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

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

6. 把 count 累加到最终结果

result += count

为什么不是 count + 1?

因为题目要求序列长度 ≥ 3
长度为 2 的等差子序列不计入结果
count 是长度 ≥ 2 的序列,可以被延长成长度 ≥ 3,所以它们都有效。

示例测试及结果

运行 Demo 得到如下输出:

输入: [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²)
posted on 2026-01-09 17:38  ljbguanli  阅读(14)  评论(0)    收藏  举报