题解 | 「力扣」第 413 题:等差数列划分(中等、滑动窗口、动态规划)

「力扣」第 413 题:等差数列划分(中等)

如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。

  • 例如,[1, 3, 5, 7, 9][7, 7, 7, 7][3, -1, -5, -9] 都是等差数列。

给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组 个数。

子数组 是数组中的一个连续序列。

示例 1:

输入:nums = [1, 2, 3, 4]
输出:3
解释:nums 中有三个子等差数组:[1, 2, 3]、[2, 3, 4] 和 [1,2,3,4] 自身。

示例 2:

输入:nums = [1]
输出:0

提示:

  • 1 <= nums.length <= 5000
  • -1000 <= nums[i] <= 1000

理解题意

  • 关键字:至少有三个元素;
  • 子数组 是数组中的一个连续序列,「连续」重要;
  • 求个数,不用求具体的连续子数组是哪些。

思路分析

  • 暴力解法,可以枚举所有长度大于 2 2 2 的连续子数组,如果是等差数列,给计数器加 1 1 1
  • 只问个数,不问具体的连续子数组是哪些,有可能使用「动态规划」;
  • 「连续」有可能使用「滑动窗口」。

方法一:滑动窗口

核心思想:所有长度大于等于 3 3 3 的连续等差数列,一定来自输入数组里能够找到的若干个当前可以延伸的最长的连续等差数列(可以看下面的动图)。

根据当前最长的连续等差数列的长度可以计算出长度大于等于 3 3 3 的连续等差数列的长度(简单推导一下就可以得到规律,后面讲)。因此可以使用「滑动窗口」找到当前输入数组上的 最长的 连续等差数列的长度 L L L,计算 L L L 对结果的贡献。以数组 [2, 4, 6, 8, 12, 16, 20] 为例。

已经知道 [2, 4, 6, 8] 是首项为 2 2 2 公差为 2 2 2 的等差数列时,加入 12 ,发现[2, 4, 6, 8, 12] 不是等差数列,因此左端点是 2右端点更靠右的所有连续子数组都不会是(以 2 开头)的等差数列,并且以 46 为起点连续子区间也不用看了,从 8 开始继续找(8 有可能是下一段等差数列的开头,本例就特别地选择了这种特殊情况)。这是「滑动窗口」可以使用的原因:它是暴力解法的优化,少考虑了很多不用考虑的情况。

413. 等差数列划分-裁剪.gif

长度为 L L L 的等差数列对结果的贡献,可以举几个例子找规律,例如长度为 6 6 6 的等差数列对结果的贡献:

  • 长度为 3 3 3 的连续的等差数列,有 4 4 4 个,如下图绿色线段;
  • 长度为 4 4 4 的连续的等差数列,有 3 3 3 个,如下图黄色线段;
  • 长度为 5 5 5 的连续的等差数列,有 2 2 2 个,如下图红色线段;
  • 长度为 6 6 6 的连续的等差数列,有 1 1 1 个,如下图蓝色线段。

image.png

因此,长度为 L L L 的等差数列对结果的贡献为:

( L − 2 ) + ( L − 1 ) + ⋯ + 2 + 1 = ( L − 2 + 1 ) ( L − 2 ) 2 = ( L − 1 ) ( L − 2 ) 2 . (L - 2) + (L - 1) + \cdots + 2 + 1 = \cfrac{(L - 2 + 1)(L - 2)}{2} = \cfrac{(L - 1)(L - 2)}{2}. (L2)+(L1)++2+1=2(L2+1)(L2)=2(L1)(L2).

补充:长度为 L L L 的连续子序列中,长度为 3 3 3 的连续等差数列(下图中绿色线段)的个数为 L − 2 L - 2 L2 ,可以从下面这张图中看出来。

image.png

虽然上式需要在 L ≥ 3 L \ge 3 L3 的情况下才成立,但上式在 L = 1 L = 1 L=1 以及 L = 2 L = 2 L=2 的时候等于 0 0 0,因此 L ≥ 3 L \ge 3 L3 的判断可以省去。

说明:以下「滑动窗口」的代码实际上没有写成一般的「滑动窗口」的样子,只需要从第 2 个元素开始,比较当前看到的元素和上一个元素的差,看一看「差」是否发生变化,就可以知道输入数组上等差数列的长度。

参考代码 1

public class Solution {

    public int numberOfArithmeticSlices(int[] nums) {
        int len = nums.length;
        if (len < 3) {
            return 0;
        }
        // 初始化
        int preDiff = nums[1] - nums[0];
        // 当前得到的等差数列的长度,有「差」必有两个元素,因此初始化的时候 L = 2
        int 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;
    }
}

复杂度分析

  • 时间复杂度: O ( N ) O(N) O(N),这里 N N N 是输入数组的长度;
  • 空间复杂度: O ( 1 ) O(1) O(1)

方法二:动态规划

核心思想:长度为 L 的等差数列,如果后面接上一个数,得到长度为 L + 1 的等差数列,那么它对结果的贡献可以从之前长度为 L 的等差数对结果的贡献得到。这一点表示了 不同子问题的结果之间的关系,因此可以使用「动态规划」。

状态定义dp[i] 表示:以 nums[i] 结尾的、且长度大于等于 3 3 3 的连续等差数列的个数。这个定义比较长,拆解一下:

  • 必需以 nums[i] 结尾,nums[i] 必需被选取;
  • 长度大于等于 3
  • 记录的是个数(不同于「方法一」的 L),就是题目要我们找的长度大于等于 3 的连续子数组(且是等差数列)的个数。

说明:为什么是以 nums[i] 结尾呢?任何一个等差数列都会以某一个数结尾,再结合我们以前做过的「力扣」第 300 题 最长递增子序列,「力扣」第 53 题 最大子序和,连续子数组和子序列一般都定义成「以什么什么结尾」,不同规模的子问题的结果的联系比较容易找到。

状态转移方程:如果 nums[i] 能够接在 nums[i - 1] 之后形成一个长度更长的(在原数组上连续的)等差数列,那么 dp[i] = dp[i - 1] + 1 。这一点可以画个图找规律。

image.png

参考代码 2

public class Solution {

    public int numberOfArithmeticSlices(int[] nums) {
        int len = nums.length;
        if (len < 3) {
            return 0;
        }
        
        // dp[i] 表示以:nums[i] 结尾的、且长度大于等于 3 的连续等差数列的个数
        int[] dp = new int[len];
        int res = 0;
        // 从下标 2 开始,才有可能构成长度至少大于等于 3 的等差数列
        for (int i = 2; i < len; i++) {
            if (nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]) {
                dp[i] = dp[i - 1] + 1;
                res += dp[i];
            }
        }
        return res;
    }
}

说明nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2] 这么写隐含了判断等差数列的长度大于等于 3 3 3

复杂度分析

  • 时间复杂度: O ( N ) O(N) O(N),这里 N N N 是输入数组的长度;
  • 空间复杂度: O ( N ) O(N) O(N)。由于 dp[i] 只参考了 dp[i - 1],可以使用「滚动变量」优化空间复杂度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值