1027. 最长等差数列
1027. 最长等差数列
题目描述:
给你一个整数数组 nums
,返回 nums
中最长等差子序列的长度。
回想一下,nums
的子序列是一个列表 nums[i1], nums[i2], ..., nums[ik]
,且 0 <= i1 < i2 < ... < ik <= nums.length - 1
。并且如果 seq[i+1] - seq[i]
( 0 <= i < seq.length - 1
) 的值都相同,那么序列 seq
是等差的。
解题思路:
算法思路:
1.
状态表⽰:
对于线性
dp
,我们可以⽤「经验 + 题⽬要求」来定义状态表⽰:
i.
以某个位置为结尾,巴拉巴拉;
ii.
以某个位置为起点,巴拉巴拉。
这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:
dp[i]
表⽰:以
i
位置元素为结尾的「所有⼦序列」中,最⻓的等差序列的⻓度。
但是这⾥有⼀个⾮常致命的问题,那就是我们⽆法确定
i
结尾的等差序列的样⼦。这样就会导致
我们⽆法推导状态转移⽅程,因此我们定义的状态表⽰需要能够确定⼀个等差序列。
根据等差序列的特性,我们仅需知道序列⾥⾯的最后两个元素,就可以确定这个序列的样⼦。因
此,我们修改我们的状态表⽰为:
dp[i][j]
表⽰:以
i
位置以及
j
位置的元素为结尾的所有的⼦序列中,最⻓的等差序列的
⻓度。规定⼀下
i < j
。
2.
状态转移⽅程:
设
nums[i] = b, nums[j] = c
,那么这个序列的前⼀个元素就是
a = 2 * b - c
。我们
根据
a
的情况讨论:
a.
a
存在,下标为
k
,并且
a < b
:此时我们需要以
k
位置以及
i
位置元素为结尾的最
⻓等差序列的⻓度,然后再加上
j
位置的元素即可。于是
dp[i][j] = dp[k][i] +
1
。这⾥因为会有许多个
k
,我们仅需离
i
最近的
k
即可。因此任何最⻓的都可以以
k
为结尾;
b.
a
存在,但是
b < a < c
:此时只能两个元素⾃⼰玩了,
dp[i][j] = 2
;
c.
a
不存在:此时依旧只能两个元素⾃⼰玩了,
dp[i][j] = 2
。
综上,状态转移⽅程分情况讨论即可。
优化点:我们发现,在状态转移⽅程中,我们需要确定
a
元素的下标。因此我们可以将所有的元素 +
下标绑定在⼀起,放到哈希表中,这⾥有两种策略:
a.
在
dp
之前,放⼊哈希表中。这是可以的,但是需要将下标形成⼀个数组放进哈希表中。这样
时间复杂度较⾼,我帮⼤家试过了,超时。
b.
⼀边
dp
,⼀边保存。这种⽅式,我们仅需保存最近的元素的下标,不⽤保存下标数组。但是
⽤这种⽅法的话,我们在遍历顺序那⾥,先固定倒数第⼆个数,再遍历倒数第⼀个数。这样就
可以在
i
使⽤完时候,将
nums[i]
扔到哈希表中。
3.
初始化:
根据实际情况,可以将所有位置初始化为
2
。
4.
填表顺序:
a.
先固定倒数第⼆个数;
b.
然后枚举倒数第⼀个数。
5.
返回值:
由于不知道最⻓的结尾在哪⾥,因此返回
dp
表中的最⼤值。
解题代码:
class Solution {
public:
int longestArithSeqLength(vector<int>& nums) {
int n=nums.size();
vector<vector<int>>dp(n,vector<int>(n,2));
unordered_map<int,int>hash;
int ret=2;
hash[nums[0]]=0;
for(int i=1;i<n-1;i++)
{
for(int j=i+1;j<n;j++)
{
int a=2*nums[i]-nums[j];
// int k=hash.count(a);
if(hash.count(a))
{
dp[i][j]=dp[hash[a]][i]+1;
}
ret=max(ret,dp[i][j]);
}
hash[nums[i]]=i;
}
return ret;
}
};
446. 等差数列划分 II - 子序列
题目描述:
给你一个整数数组 nums
,返回 nums
中所有 等差子序列 的数目。
如果一个序列中 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该序列为等差序列。
- 例如,
[1, 3, 5, 7, 9]
、[7, 7, 7, 7]
和[3, -1, -5, -9]
都是等差序列。 - 再例如,
[1, 1, 2, 5, 7]
不是等差序列。
数组中的子序列是从数组中删除一些元素(也可能不删除)得到的一个序列。
- 例如,
[2,5,10]
是[1,2,1,2,4,1,5,10]
的一个子序列。
题目数据保证答案是一个 32-bit 整数。
解题思路:
算法思路:
1.
状态表⽰:
对于线性
dp
,我们可以⽤「经验 + 题⽬要求」来定义状态表⽰:
i.
以某个位置为结尾,巴拉巴拉;
ii.
以某个位置为起点,巴拉巴拉。
这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:
dp[i]
表⽰:以
i
位置元素为结尾的「所有⼦序列」中,等差⼦序列的个数。
但是这⾥有⼀个⾮常致命的问题,那就是我们⽆法确定
i
结尾的等差序列的样⼦。这样就会导致
我们⽆法推导状态转移⽅程,因此我们定义的状态表⽰需要能够确定⼀个等差序列。
根据等差序列的特性,我们仅需知道序列⾥⾯的最后两个元素,就可以确定这个序列的样⼦。因
此,我们修改我们的状态表⽰为:
dp[i][j]
表⽰:以
i
位置以及
j
位置的元素为结尾的所有的⼦序列中,等差⼦序列的个
数。规定⼀下
i < j
。
2.
状态转移⽅程:
设
nums[i] = b, nums[j] = c
,那么这个序列的前⼀个元素就是
a = 2 * b - c
。我们
根据
a
的情况讨论:
a.
a
存在,下标为
k
,并且
a < b
:此时我们知道以
k
元素以及
i
元素结尾的等差序列
的个数
dp[k][i]
,在这些⼦序列的后⾯加上
j
位置的元素依旧是等差序列。但是这⾥会多
出来⼀个以
k, i, j
位置的元素组成的新的等差序列,因此
dp[i][j] = dp[k][i]
+ 1
;
b.
因为
a
可能有很多个,我们需要全部累加起来。
综上,
dp[i][j] += dp[k][i] + 1
。
优化点:我们发现,在状态转移⽅程中,我们需要确定
a
元素的下标。因此我们可以在 dp 之前,将
所有元素 + 下标数组绑定在⼀起,放到哈希表中。这⾥为何要保存下标数组,是因为我们要统计个
数,所有的下标都需要统计。
3.
初始化:
刚开始是没有等差数列的,因此初始化
dp
表为
0
。
4.
填表顺序:
a.
先固定倒数第⼀个数;
b.
然后枚举倒数第⼆个数。
5.
返回值:
我们要统计所有的等差⼦序列,因此返回
dp
表中所有元素的和。
解题代码:
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
int n=nums.size();
vector<vector<int>>dp(n,vector(n,0));
unordered_map<long long,vector<int>>hash;
hash[nums[0]].push_back(0);
int sum=0;
for(int i=1;i<n-1;i++)
{
for(int j=i+1;j<n;j++)
{
long long a=2*(long long)nums[i]-nums[j];
if(hash.count(a))
{
int length=hash[a].size();
for(int k=0;k<length;k++)
{
dp[i][j]+=dp[hash[a][k]][i]+1;
}
}
sum+=dp[i][j];
}
hash[nums[i]].push_back(i);
}
return sum;
}
};