

文章目录
-
- 摘要
- 描述
- 题解答案(核心思路)
- 题解代码分析
-
- [可运行 Demo(Swift)](#可运行 Demo(Swift))
- 题解代码分析(详细分解)
-
- [1. dp 数组的含义](#1. dp 数组的含义)
- [2. 遍历所有 j < i](#2. 遍历所有 j < i)
- [3. 计算差值 diff](#3. 计算差值 diff)
- [4. 从 dp[j] 查看能够延续的子序列数量](#4. 从 dp[j] 查看能够延续的子序列数量)
- [5. 更新 dp[i][diff]](#5. 更新 dp[i][diff])
- [6. 把 count 累加到最终结果](#6. 把 count 累加到最终结果)
- 示例测试及结果
-
- [示例 1](#示例 1)
- [示例 2](#示例 2)
- [示例 3](#示例 3)
- 时间复杂度
- 空间复杂度
- 总结
摘要
这一题看上去名字挺长,但核心其实就是:在一个数组里找出所有「长度至少为 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 思路如下:
-
对于每个下标
i,我们维护一个字典dp[i],里面记录所有可能差值对应的等差子序列数量。 -
对于每一对
(j, i)且j < i:-
计算
diff = nums[i] - nums[j] -
dp[i][diff] += dp[j][diff] + 1+1是因为(nums[j], nums[i])本身就是一个长度为 2 的"潜在"等差子序列
-
-
所有
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 思维,还考察你对"子序列"这个概念的理解。
整道题的关键点在于:
- 使用 dp[i][diff] 记录以 nums[i] 结尾、差为 diff 的等差子序列数量
- 每次组合 (j, i) 时,用 dp[j][diff] 来扩展
+1代表新的长度为 2 的子序列- 只有 dp[j][diff] 才能贡献到最终答案
- 时间 O(n²) 空间 O(n²)