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²)
相关推荐
算家计算2 小时前
AI真的懂你!阿里发布Qwen3-Omni-Flash 全模态大模型:超强交互,人设任选
人工智能·算法·机器学习
l1t2 小时前
利用Duckdb求解Advent of Code 2025第9题 最大矩形面积
数据库·sql·算法·duckdb·advent of code
hetao17338372 小时前
2025-12-10 hetao1733837的刷题笔记
c++·笔记·算法
步达硬件3 小时前
【matlab】代码库-一维线性插值
数据结构·算法·matlab
myw0712053 小时前
湘大oj-数码积性练习笔记
c语言·数据结构·笔记·算法
普贤莲花3 小时前
得物面试总结20251210
程序人生·算法·leetcode
hz_zhangrl3 小时前
CCF-GESP 等级考试 2025年9月认证C++五级真题解析
开发语言·数据结构·c++·算法·青少年编程·gesp·2025年9月gesp
亭上秋和景清3 小时前
qsort函数(快速排序)
数据结构·算法
轻描淡写6063 小时前
二进制存储数据
java·开发语言·算法