动态规划 —— 子序列问题(下)

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程程(ಥ_ಥ)-CSDN博客

所属专栏:动态规划

动态规划 —— 子序列系列(上)-CSDN博客

感兴趣可以先看上面的题目。

目录

1218.最长定差子序列

873.最长的斐波那契子序列的长度

1027.最长等差数列

446.等差数列划分II-子序列


1218.最长定差子序列

题目:

给你一个整数数组 arr 和一个整数 difference,请你找出并返回 arr 中最长等差子序列的长度,该子序列中相邻元素之间的差等于 difference 。

子序列 是指在不改变其余元素顺序的情况下,通过删除一些元素或不删除任何元素而从 arr 派生出来的序列。

示例 1:

输入:arr = [1,2,3,4], difference = 1
输出:4
解释:最长的等差子序列是 [1,2,3,4]。

示例 2:

输入:arr = [1,3,5,7], difference = 1
输出:1
解释:最长的等差子序列是任意单个元素。

示例 3:

输入:arr = [1,5,7,8,5,3,4,2,1], difference = -2
输出:4
解释:最长的等差子序列是 [7,5,3,1]。

提示:

  • 1 <= arr.length <= 10^5
  • -10^4 <= arr[i], difference <= 10^4

思路:

代码实现:

超时代码:  

class Solution {
    public int longestSubsequence(int[] arr, int difference) {
        int n = arr.length;
        int[] dp = new int[n];

        Arrays.fill(dp, 1);

        for (int i = 1; i < n; i++) { // 为整个dp数组赋值
            int a = arr[i], b = arr[i] - difference;
            for (int j = i-1; j >= 0; j--) { // 求单个dp值
                if (arr[j] == b) {
                    dp[i] = dp[j] + 1;
                    break; // 只需要找到距离i最近的一个即可
                }
            }
        }

        int ret = 0;
        for (int i = 0; i < n; i++) {
            ret = Math.max(ret, dp[i]);
        }

        return ret;
    }
}

按照我们上述思路去编写代码最终会超时,问题就出现了 找 j 的 循环上,导致最终的时间复杂度为 O(N^2)了,接下来就是想如何进行优化?哈希表中寻找一个值的效率是最高的,时间复杂度为O(1),那可以实现吗?答案是可以的。将 arr[i] 作为 哈希表的键,dp[i] 作为哈希表的值,之所以不使用 i 作为键,而是使用 arr[i] 作为键,是因为找 b 的时候,非常方便,不用去for循环遍历。

class Solution {
    public int longestSubsequence(int[] arr, int difference) {
        Map<Integer, Integer> hash = new HashMap<>();
        int ret = 1;
        for (int a : arr) {
            // 如果有相同的arr[j]的话,都是更新为最后一个,即离i最近
            hash.put(a, hash.getOrDefault(a - difference, 0) + 1);
            ret = Math.max(ret, hash.get(a));
        }
        return ret;
    }
}

873.最长的斐波那契子序列的长度

题目:

如果序列 X_1, X_2, ..., X_n 满足下列条件,就说它是 斐波那契式 的:

  • n >= 3
  • 对于所有 i + 2 <= n,都有 X_i + X_{i+1} = X_{i+2}

给定一个严格递增的正整数数组形成序列 arr ,找到 arr 中最长的斐波那契式的子序列的长度。如果一个不存在,返回  0 。

(回想一下,子序列是从原序列 arr 中派生出来的,它从 arr 中删掉任意数量的元素(也可以不删),而不改变其余元素的顺序。例如, [3, 5, 8] 是 [3, 4, 5, 6, 7, 8] 的一个子序列)

    示例 1:

    输入: arr = [1,2,3,4,5,6,7,8]
    输出: 5
    解释: 最长的斐波那契式子序列为 [1,2,3,5,8] 。
    

    示例 2:

    输入: arr = [1,3,7,11,12,14,18]
    输出: 3
    解释: 最长的斐波那契式子序列有 [1,11,12]、[3,11,14] 以及 [7,11,18] 。
    

    提示:

    • 3 <= arr.length <= 1000
    • 1 <= arr[i] < arr[i + 1] <= 10^9

    思路:

    代码实现:

    class Solution {
        public int lenLongestFibSubseq(int[] arr) {
            int n = arr.length;
            int[][] dp = new int[n][n];
    
            Map<Integer, Integer> hash = new HashMap<>();
            for (int i = 0; i < n; i++) {
                // 绑定 元素值 和 下标的关系
                hash.put(arr[i], i);
            }
    
            for (int i = 0; i < n; i++) {
                Arrays.fill(dp[i], 2); // 只能填充一维数组
            }
    
            int ret = 2;
            // 斐波那契子序列最少要有三个才行
            // 因此 i = 2, j = 1, k = 0这是最基本的
            for (int i = 2; i < n; i++) {
                for (int j = 1; j < i; j++) { // j 要小于 i
                    // 填的是一个dp[i][j]
                    int a = arr[i] - arr[j];
                    // 存在a,且 a < arr[j]
                    if (hash.containsKey(a) && a < arr[j]) {
                        // 注意这里的写法 dp[j][i]
                        dp[j][i] = dp[hash.get(a)][j] + 1; // 子序列长度+1
                        ret = Math.max(dp[j][i], ret);
                    }
                }
            }
    
            // 由于数组全部初始化为2了,因此可能原来是0的情况,也变为了2
            return ret == 2 ? 0 : ret;
        }
    }

    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:

    输入:nums = [3,6,9,12]
    输出:4
    解释: 
    整个数组是公差为 3 的等差数列。
    

    示例 2:

    输入:nums = [9,4,7,2,10]
    输出:3
    解释:
    最长的等差子序列是 [4,7,10]。
    

    示例 3:

    输入:nums = [20,1,15,3,10,5,8]
    输出:4
    解释:
    最长的等差子序列是 [20,15,10,5]。
    

    提示:

    • 2 <= nums.length <= 1000
    • 0 <= nums[i] <= 500

    思路:

    代码实现:

    class Solution {
        public int longestArithSeqLength(int[] nums) {
            int n = nums.length;
            int[][] dp = new int[n][n];
    
            for (int i = 0; i < n; i++) {
                Arrays.fill(dp[i], 2);
            }
    
            int ret = 2;
            for (int i = 2; i < n; i++) {
                for (int j = 1; j < i; j++) {
                    // 确定了dp[i][j]的值
                    int a = 2 * nums[j] - nums[i];
                    // 寻找离b最近的a
                    for (int k = j-1; k >= 0; k--) {
                        if (nums[k] == a) {
                            dp[j][i] = dp[k][j] + 1;
                            break; // 只需要找到一个即可
                        }
                    }
                    ret = Math.max(ret, dp[j][i]);
                }
            }
    
            return ret; // 这里不需要判断是否等于2
        }
    }

    注意:在数学中等差数列的定义是要数列中的元素个数不少于三,且满足相邻元素之间的差值是相等的,但是本题中,如果冒然在 最后返回 ret 时,去特判的话,就会在特殊示例下报错。因此只能直接返回 ret 的值,这里可能是题目有意为之,我们不用去纠结。

    在最上面的实现版本中,寻找 a 时,是直接去遍历寻找,这里可以去尝试优化。同样依旧是使用查找效率最高的数据结构哈希表,将原始数组的值 和 下标数组 绑定到一起存放到哈希表中,只需要找到下标数组中满足 [0,i-1] 且最大即可。

    class Solution {
        public int longestArithSeqLength(int[] nums) {
            int n = nums.length;
            int[][] dp = new int[n][n];
    
            for (int i = 0; i < n; i++) {
                Arrays.fill(dp[i], 2);
            }
    
            // 绑定 原始数组的值 和 下标数组的关系
            Map<Integer, List<Integer>> hash = new HashMap<>();
            for (int i = 0; i < n; i++) {
                List<Integer> list = hash.get(nums[i]);
                if (list == null) {
                    list = new LinkedList<>();
                }
                list.add(i);
                hash.put(nums[i], list);
            }
    
            int ret = 2;
            for (int i = 2; i < n; i++) {
                for (int j = 1; j < i; j++) {
                    // 确定了dp[i][j]的值
                    int a = 2 * nums[j] - nums[i];
                    // 寻找离b最近的a
                    List<Integer> list = hash.get(a);
                    if (list != null) {
                        int index = -1;
                        for (int x : list) {
                            // 符合要求且离b最近
                            if (x < j && x > index) {
                                index = x;
                            }
                        }
                        if (index != -1) {
                            dp[j][i] = dp[index][j] + 1;
                        }
                    }
                    ret = Math.max(ret, dp[j][i]);
                }
            }
    
            return ret; // 这里不需要判断是否等于2
        }
    }

    上述代码在测试时,由于数据量较小,可以成功通过,但是在提交时,由于一下测试的数据比较多,反而会出现超时的现象,而之所以会出现超时的现象,主要是因为这里在填表以及从哈希表中取得合适的值时,所带来的时间负担,因此会超时,那是否可以实现不绑定下标数组,而是直接绑定下标呢?也是可以的,不过整体的实现和前面有所不同。 前面是在填dp表之前,将哈希表全部初始化完成,而这里是一边填dp表,一边初始化哈希表的值。由于哈希表中存储的是距离倒数第二个位置最近的下标,因此这里需要换一种方式来填dp表了,固定倒数第二个位置的值,枚举最后一个位置的值,这样才能保证更新完 i 位置 dp 的值之后,更新的哈希表是最后一次更新的。

    class Solution {
        public int longestArithSeqLength(int[] nums) {
            int n = nums.length;
            int[][] dp = new int[n][n];
    
            for (int i = 0; i < n; i++) {
                Arrays.fill(dp[i], 2);
            }
    
            // 绑定 原始数组的值 和 下标的关系
            Map<Integer, Integer> hash = new HashMap<>();
            hash.put(nums[0], 0); // 为第一次寻值做准备
    
            int ret = 2;
            // 固定倒数第二个位置的数,枚举最后一个位置的数
            for (int i = 1; i < n; i++) {
                for (int j = i+1; j < n; j++) {
                    // 填一个dp值
                    int a = 2 * nums[i] - nums[j];
                    // 要么设置默认值,要么不拆包
                    Integer index = hash.get(a);
                    if (index != null && index < i) {
                        dp[i][j] = dp[index][i] + 1;
                    }
                    // i 是倒数第二个位置,j 是最后一个位置
                    ret = Math.max(ret, dp[i][j]);
                }
                // 只有在 i 更新时,才更新
                hash.put(nums[i], i);
            }
    
            return ret; // 这里不需要判断是否等于2
        }
    }

    其实根据哈希表绑定的关系以及我们的处理方式可以得出一个结论:只要是存在于哈希表中,那么其一定是小于 i 的。因为哈希表的结果更新是落后于 i 的更新,即每次去哈希表中寻找对应的 a 以及其下标时,找到的下标一定是小于 i 的,因此还可以对上述代码进行优化处理。

    if (hash.containsKey(a)) { // 自动过滤了不合法的情况
        dp[i][j] = dp[hash.get(a)][i] + 1;
    }

    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:

    输入: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
    • -2^31 <= nums[i] <= 2^31 - 1

    思路:

    代码实现:

    class Solution {
        public int numberOfArithmeticSlices(int[] nums) {
            int n = nums.length;
            int[][] dp = new int[n][n];
    
            // 初始化为0,而默认就是0,因此无需处理
            
            int ret = 0;
            // 固定最后一个数,枚举倒数第二个数
            for (int i = 2; i < n; i++) {
                for (int j = 1; j < i; j++) {
                    long a = ((long)2 * nums[j] - (long)nums[i]); // 可能出现计算结果溢出的情况
                    // 遍历找倒数第三个数
                    // 可能存在多个 a,都需要统计到 dp[j][i] 中
                    // 因为都是以 j,i位置为结尾的子序列且满足等差性质 
                    for (int k = j-1; k >= 0; k--) {
                        if (nums[k] == a) {
                            dp[j][i] += dp[k][j] + 1;
                        }
                    }
                    // 统计
                    ret += dp[j][i];
                }
            }
    
            return ret;
        }
    }

    上述代码在找 a 对应的下标时,还是采用 for循环 的方式去遍历,时间效率比较低下,可以优化为哈希表的结构,将 原始数组的值 和 下标数组绑定到一起。这样最终就只需要去遍历哈希表中的下标数组即可。

    class Solution {
        public int numberOfArithmeticSlices(int[] nums) {
            int n = nums.length;
            int[][] dp = new int[n][n];
    
            // 初始化为0,而默认就是0,因此无需处理
    
            // 将 原始数组的值 和 下标数组 绑定到一起
            Map<Long, List<Integer>> hash = new HashMap<>();
            for (int i = 0; i < n; i++) {
                if (hash.get((long)nums[i]) == null) {
                    hash.put((long) nums[i], new ArrayList<>());
                }
                hash.get((long)nums[i]).add(i);
            }
    
            int ret = 0;
            // 固定最后一个数,枚举倒数第二个数
            for (int i = 2; i < n; i++) {
                for (int j = 1; j < i; j++) {
                    long a = ((long)2 * nums[j] - (long)nums[i]); // 可能出现计算结果溢出的情况
                    // 遍历找倒数第三个数
                    // 可能存在多个 a,都需要统计到 dp[j][i] 中
                    // 因为都是以 j,i位置为结尾的子序列且满足等差性质
                    List<Integer> list = hash.get(a);
                    if (list != null) {
                        for (int k : list) {
                            if (k < j) { // 要满足 小于 j
                                dp[j][i] += dp[k][j] + 1;
                            } else {
                                // 由于添加下标时是按照 0~n-1 的范围来添加的
                                // 因此下标数组中存放的值一定是有序的(从小到大)
                                break;
                            }
                        }
                    }
                    // 统计
                    ret += dp[j][i];
                }
            }
    
            return ret;
        }
    }

    好啦!本期 动态规划 —— 子序列问题(下)的刷题之旅 就到此结束啦!我们下一期再一起学习吧!