最长等差数列的变种:最长等差数列的长度与具体序列(允许公差为负且统计最长等差数列的个数)
题目描述
给你一个整数数组 nums,返回这个数组中所有等差数列的最长长度。同时,如果有多条最长等差数列,你需要统计出最长等差数列的个数。等差数列的定义是:一个序列中任意相邻两项的差相等,这个差值可以是正数、负数或零。注意,等差数列至少需要3个元素。你需要返回一个数组 [length, count],其中 length 是最长等差数列的长度,count 是最长等差数列的个数。
示例
输入:nums = [2,4,6,8,10]
输出:[5, 1]
解释:整个数组就是一个公差为2的等差数列,长度为5,只有这一个最长等差数列。
输入:nums = [7,7,7,7,7]
输出:[5, 1]
解释:整个数组是公差为0的等差数列,长度为5,只有这一个最长等差数列。
输入:nums = [3,6,9,12,15,2,4,6,8,10]
输出:[5, 2]
解释:最长等差数列长度为5,有两个:[3,6,9,12,15] 和 [2,4,6,8,10]。
解题过程
1. 问题分析
我们需要找出所有可能的等差数列,并记录最长长度及其个数。等差数列由公差 diff 决定,对于任意两个位置 i 和 j(i < j),diff = nums[j] - nums[i]。一个直观想法是枚举所有数对作为等差数列的最后两项,然后查看前面是否有合适的数字能接上,形成更长的等差数列。
2. 状态定义
定义 dp[i][diff] 为一个哈希表(字典),其中键是公差 diff,值是一个二元组 [len, cnt],表示以第 i 个元素结尾、公差为 diff 的等差数列的长度和个数。注意,这里的“长度”指的是以 i 结尾的等差数列的长度,“个数”指的是以 i 结尾、公差为 diff 的等差数列的个数。
但这样直接存储会面临一个问题:公差可能很大(数值范围大),而且我们需要高效地找到前面某个数字对应的状态。因此,更常见的做法是使用一个数组 dp[i],它是一个哈希表,键是公差,值是 [len, cnt]。
3. 状态转移方程
考虑以 nums[i] 结尾的等差数列,我们枚举它前面的一个位置 j(0 <= j < i),计算公差 diff = nums[i] - nums[j]。
- 如果存在以
nums[j]结尾、公差为diff的等差数列,那么我们可以接上去,形成更长的等差数列。 - 具体来说,设从
dp[j]中查到以j结尾、公差为diff的等差数列长度为len_j、个数为cnt_j,那么接上nums[i]后,长度变为len_j + 1,个数继承为cnt_j。 - 如果
dp[j]中没有公差为diff的项,说明以j和i作为最后两项是一个新的等差数列(长度为2),但我们题目要求至少3个元素,所以长度为2的等差数列我们不视为有效等差数列(不参与最长统计)。不过,长度为2的序列可以作为中间状态,用于后续延长。
为了统一处理,我们可以这样设计:
dp[i][diff] 直接表示以 i 结尾、公差为 diff 的等差数列的长度和个数(长度至少为2)。当我们从 j 转移到 i 时:
- 如果
dp[j]中有键diff,设其值为(len_j, cnt_j),那么dp[i][diff]可以更新为(len_j + 1, cnt_j)。 - 如果
dp[j]中没有键diff,那么说明这是一个新的二元组,我们可以初始化dp[i][diff] = (2, 1)(表示以j和i开始的等差数列,长度为2,目前有1个这样的序列)。
但注意:当我们统计最终的最长等差数列时,只考虑长度 >=3 的。另外,可能存在多个不同的 j 都能以同样的公差 diff 转移到 i,此时我们需要合并个数(即 cnt 相加)。
4. 合并重复状态
由于不同的 j 可能产生相同的 diff 转移到 i,我们需要在遍历所有 j 时,对每个 diff 合并信息。具体做法:
- 对于固定的
i和j,计算diff = nums[i] - nums[j]。 - 查看
dp[j]中是否有diff:- 如果有,得到
(len_j, cnt_j),那么候选长度为len_j + 1,候选个数为cnt_j。 - 如果没有,则候选长度为
2,候选个数为1。
- 如果有,得到
- 然后,查看
dp[i]中是否已经有公差diff的条目:- 如果没有,则直接插入
(候选长度, 候选个数)。 - 如果有,设已有值为
(len_old, cnt_old),比较候选长度和已有长度:- 如果候选长度 > 已有长度,则更新为
(候选长度, 候选个数)。 - 如果候选长度 == 已有长度,则个数累加:
cnt_new = cnt_old + 候选个数。 - 如果候选长度 < 已有长度,则忽略。
- 如果候选长度 > 已有长度,则更新为
- 如果没有,则直接插入
5. 更新全局答案
在每次更新 dp[i][diff] 后,如果其长度 >=3,我们就用这个长度去更新全局最长长度 max_len,并相应地更新全局个数 max_cnt:
- 如果长度 >
max_len,则更新max_len为这个长度,max_cnt为这个个数。 - 如果长度 ==
max_len,则max_cnt加上这个个数。
6. 边界条件
- 数组长度小于3时,没有长度 >=3 的等差数列,返回
[0, 0]。 - 初始化:每个位置
i的dp[i]是一个空哈希表。
7. 示例推演
以 nums = [2,4,6,8,10] 为例:
i=1(值4):j=0,diff=2,dp[0]为空,所以dp[1][2] = (2,1)。i=2(值6):j=0,diff=4,dp[0]空,dp[2][4] = (2,1)。j=1,diff=2,dp[1]中有(2,1),所以候选(3,1)。dp[2]中无diff=2,插入(3,1)。此时长度3 > 当前全局最大长度3,更新max_len=3, max_cnt=1。
i=3(值8):j=0,diff=6,dp[3][6] = (2,1)。j=1,diff=4,dp[1]无diff=4(因为dp[1]只有diff=2),所以dp[3][4] = (2,1)。j=2,diff=2,dp[2]中有(3,1),候选(4,1)。插入dp[3][2] = (4,1),更新全局max_len=4, max_cnt=1。
- 依此类推,最终在
i=4得到dp[4][2] = (5,1),全局max_len=5, max_cnt=1。
8. 复杂度分析
- 时间复杂度:O(n²),因为需要双重循环枚举
(j, i)。哈希表操作可以视为 O(1)。 - 空间复杂度:O(n²),最坏情况下每个
dp[i]可能存储 O(i) 个不同的公差。
9. 代码框架(Python风格)
def longestArithSeqLengthAndCount(nums):
n = len(nums)
if n < 3:
return [0, 0]
dp = [{} for _ in range(n)] # dp[i][diff] = (length, count)
max_len, max_cnt = 0, 0
for i in range(n):
for j in range(i):
diff = nums[i] - nums[j]
length, cnt = 2, 1 # 默认从j,i开始的新序列
if diff in dp[j]:
length_j, cnt_j = dp[j][diff]
length = length_j + 1
cnt = cnt_j
# 合并到dp[i]
if diff in dp[i]:
len_old, cnt_old = dp[i][diff]
if length > len_old:
dp[i][diff] = (length, cnt)
elif length == len_old:
dp[i][diff] = (length, cnt_old + cnt)
# 如果length < len_old,保持不变
else:
dp[i][diff] = (length, cnt)
# 更新全局答案(只考虑长度>=3的)
if length >= 3:
if length > max_len:
max_len = length
max_cnt = dp[i][diff][1] # 注意这里要用dp[i][diff]的个数,因为可能合并过
elif length == max_len:
max_cnt += dp[i][diff][1]
return [max_len, max_cnt] if max_len >= 3 else [0, 0]
10. 总结
本题是经典最长等差数列问题的扩展,不仅要求最长长度,还要求个数。核心在于在状态中同时记录长度和个数,并在状态转移时正确处理合并逻辑。注意,长度为2的等差数列不参与最终答案统计,但作为中间状态必须保留,以便后续延长。