leetcode
leetcode 401 ~ 450
等差数列划分 II - 子序列

等差数列划分 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:

输入:nums = [2,4,6,8,10]
输出:7
解释:所有的等差子序列为:
[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]

示例 2:

输入:nums = [7,7,7,7,7]
输出:16
解释:数组中的任意子序列都是等差子序列。

 

提示:

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

代码结果

运行时间: 259 ms, 内存: 0.0 MB


/*
题目思路:
1. 使用一个数组dp来记录以每个元素结尾的等差子序列的数目。
2. 使用一个HashMap数组dp来记录以每个元素结尾且公差为某值的等差子序列的数目。
3. 遍历每个元素,对于每个元素再遍历其之前的所有元素,计算公差。
4. 使用该公差更新dp数组。
5. 累加所有dp数组中的值,即为所求的等差子序列的数目。
6. 使用Java Stream API来实现遍历和累加。
*/
import java.util.HashMap;
import java.util.stream.IntStream;
 
public class ArithmeticSlicesStream {
    public int numberOfArithmeticSlices(int[] nums) {
        int n = nums.length;
        if (n < 3) return 0;
        HashMap<Integer, Integer>[] dp = IntStream.range(0, n)
            .mapToObj(i -> new HashMap<Integer, Integer>())
            .toArray(HashMap[]::new);
        int[] total = {0};
        IntStream.range(0, n).forEach(i -> {
            IntStream.range(0, i).forEach(j -> {
                long diff = (long) nums[i] - (long) nums[j];
                if (diff <= Integer.MIN_VALUE || diff > Integer.MAX_VALUE) return;
                int d = (int) diff;
                int count = dp[j].getOrDefault(d, 0);
                total[0] += count;
                dp[i].put(d, dp[i].getOrDefault(d, 0) + count + 1);
            });
        });
        return total[0];
    }
}

解释

方法:

这个题解使用动态规划的方法来解决问题。主要思路如下: 1. 使用两个字典 left 和 right 分别记录每个数字最左边和最右边的出现位置。 2. 从右往左遍历数组,对于每个位置 i 上的数字 m: - 更新 right[m],删除当前位置 - 枚举所有可能的等差数列的另外两个数 l 和 r,其中 m = (l + r) / 2 - 对于每个符合条件的 l 和 r,累加 dp[i][l] 的值到 dp[j][m] 上,其中 j 为 right[r] 中的每个位置 - 将 dp[i] 的所有值累加到答案 ans 上 - 将当前位置 i 加入到 left[m] 中 3. 最终返回 ans 即可

时间复杂度:

O(n^2)

空间复杂度:

O(n)

代码细节讲解

🦆
在动态规划的过程中,为何选择从右向左遍历数组?反向遍历对解题有何影响或优势?
从右向左遍历数组允许我们在处理每个元素时,已经有了该元素右侧所有元素的信息,这对于动态规划中状态的更新非常关键。通过这种方式,我们可以确保当我们计算以 nums[i] 结尾的等差数列个数时,其右侧的数列个数已经被计算过,从而可以直接使用这些信息来更新当前位置的状态。这样的遍历顺序帮助我们避免了重复计算并保证了每个状态是基于最终的、更新过的值。
🦆
解题中提到了动态规划数组dp[i][j],这里的j代表什么?是否公差?如何根据题目中的数组元素值和索引更新dp数组?
在这个动态规划解法中,dp[i][j] 中的 j 实际上代表的是等差数列的第一个数,而不是常见的公差。dp[i][j] 表示以 nums[i] 结尾,且以 j 作为等差数列第一个数时的等差数列的个数。更新 dp 数组时,我们需要检查所有可能的数对 (l, r) 使得 nums[i] = (l + r) / 2。对于每个符合条件的 l 和 r,我们更新 dp[j][m] 的值,其中 j 是 r 在数组中的位置,m 是当前的 nums[i]。这样,dp[j][m] 的值就是在位置 j 以 m 作为等差数列倒数第二个数时的数列个数。
🦆
在生成可能的等差数列的两个数l和r时,为什么需要同时考虑从left和right字典生成数对?这种方法有什么优势?
在生成数对 (l, r) 时同时考虑 left 和 right 字典可以优化算法的性能。这种方法通过考虑字典中元素较少的一方来减少需要检查的数对的数量。如果 left 中的元素比 right 少,我们从 left 生成数对,反之则从 right。这样可以减少循环的次数,因为我们只遍历较小的字典并检查另一个字典是否含有符合条件的元素,从而提高算法的效率。
🦆
算法实现中使用了两个字典left和right,这两个字典具体是如何在算法中使用和更新的?
在这个算法中,left 和 right 字典分别用来记录数组中每个数字出现的位置。right 字典在遍历数组时首先被填充,记录每个数字出现的所有位置。随着从右向左的遍历,每到一个新位置,该位置的索引从 right 字典对应的列表中删除。如果某个数字的索引列表变空,则从字典中完全移除该数字的条目。left 字典在遍历过程中逐步构建,记录每个数字最新的出现位置。这样,两个字典共同支持了算法中对于等差数列两端点的快速查找和状态更新。

相关问题

等差数列划分

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

  • 例如,[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