最长等差子序列的个数
题目描述
给定一个整数数组 nums,返回数组中所有为等差数列的子序列(注意:子序列不要求连续)的个数。如果一个序列中至少有三个元素,并且任意两个相邻元素之差相同,则称该序列为等差数列。
例如,给定 nums = [2,4,6,8,10],返回 7,因为该数组中有 7 个等差数列子序列,长度至少为 3:
[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]。
注意:结果可能很大,你需要对结果取模 10^9+7。
解题过程循序渐进讲解
第一步:理解问题本质
题目要求统计所有长度至少为 3 的等差子序列的个数。注意是子序列,不要求连续,且元素的顺序必须保持原数组中的顺序。
我们需要考虑所有可能的差值(公差),并对每个位置,考虑它作为等差数列末尾时的情况。
第二步:初步思考状态定义
一个直观的想法是定义 dp[i][d] 表示以索引 i 结尾,公差为 d 的等差子序列的个数。但公差可能取值范围很大(例如数值范围大),因此不能直接用数组索引表示公差。
我们可以用哈希表来存储公差。更具体地,我们定义:
dp[i]是一个哈希表,键是公差diff,值是以nums[i]结尾、公差为diff的等差子序列的个数(这里子序列长度至少为 2)。
但题目要求长度至少为 3,所以我们需要区分长度至少为 2 的序列和长度至少为 3 的序列。
第三步:更精细的状态定义
我们定义两个状态:
dp_weak[i][diff]:以nums[i]结尾,公差为diff的等差子序列的个数,且该子序列长度至少为 2。dp_strong[i][diff]:以nums[i]结尾,公差为diff的等差子序列的个数,且该子序列长度至少为 3。
最终答案就是所有 dp_strong[i][diff] 的总和。
第四步:状态转移推导
假设我们当前在位置 i,我们查看之前的所有位置 j(0 <= j < i)。
令 diff = nums[i] - nums[j]。
我们考虑如何形成以 i 结尾、公差为 diff 的等差子序列:
-
对于
dp_weak[i][diff]:- 我们可以直接由
{nums[j], nums[i]}形成一个长度为 2 的序列,所以它至少贡献 1。 - 此外,如果以
j结尾、公差为diff的序列(长度至少为 2)已经存在,那么我们可以在这些序列后面追加nums[i],从而得到更长的序列。注意,这些序列的长度至少为 2,追加后长度至少为 3,所以这些追加后的序列应该计入dp_strong而不是dp_weak。
因此,dp_weak[i][diff]的增量来自于:从j开始的新长度为 2 的序列,即每次遇到一个j,就为dp_weak[i][diff]增加 1。
所以:
dp_weak[i][diff] += 1(对于每个j)。
- 我们可以直接由
-
对于
dp_strong[i][diff]:- 当我们看到
j时,如果以j结尾、公差为diff的弱序列(长度至少为 2)已经存在,那么在这些弱序列后面追加nums[i]就会形成一个长度至少为 3 的强序列。
因此,dp_strong[i][diff]的增量等于dp_weak[j][diff]。 - 同时,如果以
j结尾、公差为diff的强序列已经存在,那么在这些强序列后面追加nums[i]也会形成新的强序列,所以dp_strong[i][diff]还应加上dp_strong[j][diff]。
所以:
dp_strong[i][diff] += dp_weak[j][diff] + dp_strong[j][diff]。
- 当我们看到
注意:dp_weak[j][diff] 和 dp_strong[j][diff] 是在处理到 i 时已经计算好的(因为 j < i)。
第五步:优化实现
由于公差可能是负数,且数值范围可能很大,我们使用哈希表数组来表示 dp_weak 和 dp_strong。具体地:
dp_weak[i]是一个字典,键是公差(整数),值是对应序列个数(取模后)。dp_strong[i]类似。
初始化:所有 dp_weak[i] 和 dp_strong[i] 为空字典。
最终答案:对所有 i 和所有 diff 的 dp_strong[i][diff] 求和。
算法步骤:
- 初始化
total_count = 0。 - 遍历
i从 0 到 n-1:- 对于每个
j从 0 到 i-1:- 计算
diff = nums[i] - nums[j]。 - 从
dp_weak[j]中获取以j结尾、公差为diff的弱序列个数weak_j(如果没有,则为 0)。 - 从
dp_strong[j]中获取以j结尾、公差为diff的强序列个数strong_j(如果没有,则为 0)。 - 更新
dp_strong[i][diff] += weak_j + strong_j。 - 更新
dp_weak[i][diff] += 1。
- 计算
- 将
dp_strong[i]中所有值累加到total_count中(注意取模)。
- 对于每个
- 返回
total_count。
第六步:示例推演
以 nums = [2,4,6,8,10] 为例:
初始化:
i=0: 无操作。
i=1 (nums[1]=4):
j=0, diff=2:
- weak_j=0, strong_j=0
- dp_strong[1][2] += 0
- dp_weak[1][2] += 1 → 变为 1
total_count += dp_strong[1][2] = 0
i=2 (nums[2]=6):
j=0, diff=4:
- dp_weak[0][4] 不存在 → weak_j=0, strong_j=0
- dp_strong[2][4] += 0
- dp_weak[2][4] += 1
j=1, diff=2: - weak_j = dp_weak[1][2] = 1, strong_j=0
- dp_strong[2][2] += 1+0 = 1
- dp_weak[2][2] += 1 → 变为 1
total_count += dp_strong[2][2] = 1 (对应序列 [2,4,6])
i=3 (nums[3]=8):
j=0, diff=6: dp_weak[3][6] += 1
j=1, diff=4:
- weak_j=dp_weak[1][4]=0 → dp_strong[3][4] += 0, dp_weak[3][4] += 1
j=2, diff=2: - weak_j=dp_weak[2][2]=1, strong_j=dp_strong[2][2]=1
- dp_strong[3][2] += 1+1 = 2
- dp_weak[3][2] += 1
total_count 现在增加 dp_strong[3][2]=2(对应 [4,6,8] 和 [2,4,6,8])
i=4 (nums[4]=10):
j=0, diff=8: dp_weak[4][8] +=1
j=1, diff=6:
- weak_j=dp_weak[1][6]=0 → dp_strong[4][6] +=0, dp_weak[4][6]+=1
j=2, diff=4: - weak_j=dp_weak[2][4]=1, strong_j=0
- dp_strong[4][4] +=1, dp_weak[4][4]+=1
j=3, diff=2: - weak_j=dp_weak[3][2]=1, strong_j=dp_strong[3][2]=2
- dp_strong[4][2] += 1+2 = 3
- dp_weak[4][2] +=1
total_count 增加 dp_strong[4][2]=3(对应 [6,8,10], [4,6,8,10], [2,4,6,8,10])
另外,在 j=2, diff=4 时,dp_strong[4][4] 增加了 1(对应 [2,6,10])。
所以 total_count 最终 = 1+2+3+1 = 7。
第七步:复杂度与总结
- 时间复杂度:O(n²),因为我们需要两层循环遍历所有
(j, i)对。 - 空间复杂度:O(n²) 最坏情况,因为每个位置可能存储 O(n) 个不同的公差(实际上因为 diff 是整数对差值,最坏可能有 O(n²) 个不同的键值对)。
- 关键点:用两个状态分别记录长度至少为 2 和长度至少为 3 的序列,通过累加之前的状态来计数。
通过以上步骤,我们就能正确统计所有长度至少为 3 的等差子序列的个数。