找往期文章包括但不限于本期文章中不懂的知识点:
个人主页:我要学编程程(ಥ_ಥ)-CSDN博客
所属专栏:动态规划
感兴趣可以先看上面的题目。
目录
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;
}
}
好啦!本期 动态规划 —— 子序列问题(下)的刷题之旅 就到此结束啦!我们下一期再一起学习吧!