题目来源
题目描述
题目解析
暴力
class Solution {
bool isArithmetic(vector<int> & nums, int start, int end){
if(end - start < 2){
return false;
}
for (int i = start; i < end - i; ++i) {
if(nums[i + 1] * 2 != nums[i] + nums[i + 2]){
return false;
}
}
return true;
}
public:
int numberOfArithmeticSlices(vector<int>& nums) {
int len = nums.size();
int res = 0;
for (int i = 0; i < len - 2; ++i) {
for (int j = i + 1; j < len; ++j) {
if(isArithmetic(nums,i, j)){
res++;
}
}
}
return res;
}
};
双指针(滑动窗口)
在上面的暴力解法中,我们对每个子数组都进行了是否为等差数列的判断。
- 其实,如果我们已经知道一个子数组的前面部分不是等差数列之后,那么后面部分就不用判断了
- 同时,我们知道等差数列的所有的相邻数字的差是固定的。
因此,对于每个起始位置,我们只需要向后进行一遍扫描,直到不再构成等差数列为止,此时已经没有必要再向后扫描。
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
int len = nums.size();
int res = 0;
for (int i = 0; i < len - 2; ++i) {
int d = nums[i + 1] - nums[i];
for (int j = i + 1; j < len - 1; ++j) {
if(nums[j + 1] - nums[j] == d){
res++;
}else{
break;
}
}
}
return res;
}
};
双指针(滑动窗口)
核心思想:所有长度大于等于3的数组,一定来自输入数组里能够找到的若干个当前可以延伸的最长的连续等差数列
根据当前最长的连续等差数列的长度可以计算出长度大于等于3的连续等差数列的长度。因此可以使用滑动窗口找到当前输入数组上的最长的连续等差数列的长度L,计算L对结果的贡献。以数组[2, 4, 6, 8, 12, 16, 20]
为例。
已经知道 [2, 4, 6, 8] 是首项为 22 公差为 22 的等差数列时,加入 12 ,发现[2, 4, 6, 8, 12] 不是等差数列,因此左端点是 2,右端点更靠右的所有连续子数组都不会是(以 2 开头)的等差数列,并且以 4、6 为起点连续子区间也不用看了,从 8 开始继续找(8 有可能是下一段等差数列的开头,本例就特别地选择了这种特殊情况)。这是「滑动窗口」可以使用的原因:它是暴力解法的优化,少考虑了很多不用考虑的情况。
长度为 L 的等差数列对结果的贡献,可以举几个例子找规律,例如长度为 6 的等差数列对结果的贡献:
- 长度为 3 的连续的等差数列,有 4 个,如下图绿色线段;
- 长度为 4 的连续的等差数列,有 3 个,如下图黄色线段;
- 长度为 5 的连续的等差数列,有 2 个,如下图红色线段;
- 度为 6 的连续的等差数列,有 1 个,如下图蓝色线段。
因此,长度为 L 的等差数列对结果的贡献为:
补充:长度为 L的连续子序列中,长度为 3 的连续等差数列(下图中绿色线段)的个数为 L - 2 ,可以从下面这张图中看出来。
虽然上式需要在 L≥3 的情况下才成立,但上式在 L = 1 以及 L = 2的时候等于 0,因此 L ≥3 的判断可以省去。
说明:以下「滑动窗口」的代码实际上没有写成一般的「滑动窗口」的样子,只需要从第 2 个元素开始,比较当前看到的元素和上一个元素的差,看一看「差」是否发生变化,就可以知道输入数组上 连续的 等差数列的长度。
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
int len = nums.size();
if(len < 3){
return 0;
}
int preDiff = nums[1] - nums[0];
int L = 2; // 当前得到的等差数列的长度,有「差」必有两个元素,因此初始化的时候 L = 2
int res = 0;
// 从下标 2 开始比较「当前的差」与「上一轮的差」是否相等
for (int i = 2; i < len; ++i) {
int diff = nums[i] - nums[i - 1];
if(diff == preDiff){
++L;
}else{
// 加入结果,然后重置 L 和 preDiff
res += (L - 1) * (L - 2) / 2;
L = 2;
preDiff = diff;
}
}
// 最后还要再计算一下结果
res += (L - 1) * (L - 2) / 2;
return res;
}
};
动态规划
核心思想:长度为L的等差数列,如果后面接上长度为L+1的等差数列,那么它对结果的贡献可以从之前长度为L的等差数对结果的贡献得到。这一点表示了不同子问题的结果之间的关系,因此可以使用动态规划。
状态定义:dp[i]
表示:以nums[i]
结尾的,而且长度大于等于3的连续等差数列的个数。拆解如下:
- 必须以
nums[i]
结尾,nums[i]
必须被选取 - 长度大于等于3
- 记录的是个数,就是题目要我们找的长度大于等于 3 的连续子数组(且是等差数列)的个数。
上面:为什么是以nums[i]
结尾呢?任何一个等差数列都会以某一个数结尾,连续子数组和子序列一般都定义成「以什么什么结尾」,不同规模的子问题的结果的联系比较容易找到。
状态转移方程:如果nums[i]
能够接在nums[i - 1]
之后形成一个长度更长的(在原数组上连续的)等差数列,那么 dp[i] = dp[i - 1] + 1
。这一点可以画个图找规律。
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
int len = nums.size();
if(len < 3){
return 0;
}
std::vector<int> dp(len); // dp[i] 表示以:nums[i] 结尾的、且长度大于等于 3 的连续等差数列的个数
int res = 0;
for (int i = 2; i < len; ++i) {// 从下标 2 开始,才有可能构成长度至少大于等于 3 的等差数列
if(nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 1]){
dp[i] = dp[i - 1] + 1;
res += dp[i];
}
}
return res;
}
};
说明:nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]
这么写隐含了判断等差数列的长度大于等于 3。
复杂度分析:
- 时间复杂度:
O(N)
,这里 NN 是输入数组的长度; - 空间复杂度:
O(N)
。由于 dp[i] 只参考了dp[i - 1]
,可以使用「滚动变量」优化空间复杂度。