文章目录
- 1. 前言
- 2. 例题
- 最长递增子序列
- 3. 算法题
- 3.1_摆动序列
- 3.2_最长递增子序列的个数
- 3.3_最长数对链
- [3.4_ 最长定差子序列](https://leetcode.cn/problems/longest-arithmetic-subsequence-of-given-difference/description/)
- 3.5_最长的斐波那契子序列的长度
- 3.6_最长等差数列
- 3.7_等差数列划分II-子序列
1. 前言
关于 动态规划的理解 与例题,点击👇
【动态规划】C++解决斐波那契模型题目(三步问题、爬楼梯、解码方法…)
并且我们之前做过有关 子数组的dp问题:C++子数组dp问题
子序列与子数组的区别主要在于:子数组是连续的,即下标连续的不中断的;而子序列可以连续可以不连续(子序列的范围更大)。
有了上面的经验,我们来解下面 子序列 问题 :
2. 例题
通过下面一道例题理解子序列问题,以及如何解子序列dp问题:
最长递增子序列
思路
- 题意分析
- 题目要求找到 最长的递增子序列的长度
根据题目要求,我们设置状态表示:
根据上面的思路,我们可以用两个for循环编写呆猫:
代码
class Solution {
public:int lengthOfLIS(vector<int>& nums) {int n = nums.size();// 创建dp数组 + 初始化vector<int> dp(n, 1); // dp[i]: 以i为结尾,严格递增子序列的最长长度int ret = 1; // 结果:for(int i = 1; i < n; ++i){for(int j = 0; j <= i-1; ++j){if(nums[j] < nums[i])dp[i] = max(dp[j]+1, dp[i]);}ret = max(ret, dp[i]);}return ret;}
};
需要注意的是:本题的最优解法并不是利用动态规划,但通过本道例题可以很好的对子序列问题进行理解:
(顺带贴上更优解法:贪心 + 二分)
class Solution {
public:int lengthOfLIS(vector<int>& nums) {// 贪心vector<int> ret;ret.push_back(nums[0]);for(int i = 0; i < nums.size(); ++i){if(nums[i] > ret.back()) {ret.push_back(nums[i]);} else { // 二分查找int left = 0, right = ret.size() - 1;while(left < right){int mid = (left + right) >> 1;if(ret[mid] < nums[i])left = mid + 1;elseright = mid;}ret[left] = nums[i]; // 插入}}return ret.size();}
};
3. 算法题
通过《最长递增子序列》一题给的经验,我们来解下面的算法题:
3.1_摆动序列
思路
- 题意分析
- 在子数组问题中,我们曾做过一道题《最长湍流子数组》,和本题类似,我们根据它的经验,可以创建两个dp表:
代码
class Solution {
public:int wiggleMaxLength(vector<int>& nums) {int n = nums.size();// dp数组的创建 + 元素初始化vector<int> f(n, 1); // 以i为结尾,最后呈现“上升”状态的 子序列的最长长度vector<int> g(n, 1); // 以i为结尾,最后呈现“下降”状态的 子序列的最长长度int ret = 1; // 最终结果for(int i = 1; i < n; ++i){for(int j = 0; j <= i - 1; ++j){if(nums[j] < nums[i]) f[i] = max(g[j]+1, f[i]); // 可以将第 i 个元素加入到以第 j 个元素结尾的递增子序列中if(nums[j] > nums[i]) g[i] = max(f[j]+1, g[i]);}ret = max(max(f[i], g[i]), ret);}return ret;}
};
3.2_最长递增子序列的个数
思路
- 题意分析
- 之前的例题,求的是最长递增子序列的长度,这里要的是 最长递增子序列的 个数
- 即我们不仅需要考虑长度,也需要考虑个数,即两个状态,可以设置两个dp表
- 一个小demo: 如何找到数组中最大值的出现次数?
- 遍历数组会有三种情况:
- x < maxVal: 无视
- x == maxVal: count++
- x > maxVal: 更新maxVal,count = 0
- 遍历数组会有三种情况:
根据这个思路,我们进行分析:
代码
class Solution {
public:int findNumberOfLIS(vector<int>& nums) {int n = nums.size();// 创建dp数组 + 初始化元素vector<int> len(n, 1), count(n, 1); // 分别为:以i为结尾,1.递增子序列的最长长度 与 2.最长递增子序列的个数int retLen = 1, retCount = 1;for(int i = 1; i < n; ++i){for(int j = 0; j <= i-1; ++j){if(nums[j] < nums[i]){if(len[j] + 1 == len[i]) count[i] += count[j]; // 第j位恰好与i组成最长递增子序列// else if(len[j] + 1 < len[i]) continue; // 无视此次,可以注释掉else if(len[j] + 1 > len[i]) len[i] = len[j] + 1, count[i] = count[j]; // j的递增序列更长}}// 更新结果if(retLen == len[i]) retCount += count[i];else if(retLen < len[i]){retCount = count[i];retLen = len[i];}// else 无视该情况}return retCount;}
};
3.3_最长数对链
思路
- 题意分析
- 题目要求找到 最长的数对链,数对链即对于[a, b], [c, d]仅当b < c时,才成立[a, b] -> [c, d]
- 根据这个规律,我们在找子序列时,首先要保证不能存在一种情况:
- 令存在三个数对x, y, z
- 当我们遍历到y时,即使不一定有y->z,但一定不能有z->y
- 根据数对链的性质,我们只需要 对数组根据首位元素进行排序 即可。
- 由此可以设置状态表示:
代码
class Solution {
public:int findLongestChain(vector<vector<int>>& pairs) {// 预处理:排序数组sort(pairs.begin(), pairs.end());int n = pairs.size();// 创建dp数组 + 初始化元素vector<int> dp(n, 1); // dp[i]: 以i为结尾,数对链的最长长度int ret = 1; // 结果for(int i = 1; i < n; ++i){for(int j = 0; j <= i-1; ++j){if(pairs[j][1] < pairs[i][0]){dp[i] = max(dp[j] + 1, dp[i]);}}ret = max(ret, dp[i]);}return ret;}
};
3.4_ 最长定差子序列
思路
- 题意分析
- 题目要求找 最长的定差子序列的长度 ,定差子序列即等差数列,给定了公差difference。
- 根据题目要求设置装填表示:
代码
class Solution {
public:int longestSubsequence(vector<int>& arr, int difference) {unordered_map<int, int> hash; // arr[i] : dp[i]hash[arr[0]] = 1; // 初始化int ret = 1, n = arr.size();for(int i = 1; i < n; ++i){hash[arr[i]] = hash[arr[i] - difference] + 1; // dp[i] = dp[j] + 1,可以保证是最后一个bret = max(ret, hash[arr[i]]);}return ret;}
};
3.5_最长的斐波那契子序列的长度
思路
- 题意分析
- 题目要求找 最长的斐波那契子序列的长度
- 我们首先试着将状态表示设置为:
- dp[i]:以i为结尾的所有子序列中,最长斐波那契子序列的长度
- 当我们尝试写状态表达式时,没有办法正确找到(斐波那契子序列的判断至少需要两个元素,通过差值我们可以判断另一位是什么)
- 所以我们更改状态表示:
代码
class Solution {
public:int lenLongestFibSubseq(vector<int>& arr) {int n = arr.size();// 创建dp数组 + 元素初始化vector<vector<int>> dp(n, vector<int>(n, 2));// 哈希表:值映射下标unordered_map<int, int> hash;for(int i = 0; i < arr.size(); ++i)hash[arr[i]] = i; int ret = 2; // 返回值for(int j = 2; j < n; ++j){for(int i = 1; i < j; ++i){// 填表int a = arr[j] - arr[i];if(a < arr[i] && hash.count(a))dp[i][j] = dp[hash[a]][i] + 1; // ret = max(ret, dp[i][j]);}}return ret == 2 ? 0 : ret;}
};
3.6_最长等差数列
思路
- 题意分析
- 本题与前面的斐波那契子序列有相似之处,下面进行状态表示的设置:
代码
class Solution {
public:int longestArithSeqLength(vector<int>& nums) {int n = nums.size();unordered_map<int, int> hash; // <元素 下标>hash[nums[0]] = 0;vector<vector<int>> dp(n, vector<int>(n, 2));// dp表的创建+初始化int ret = 2;for(int i = 1; i < n; ++i) // 固定倒数第二个数{for(int j = i + 1; j < n; ++j) // 固定最后一个数{int a = 2 * nums[i] - nums[j];if(hash.count(a))dp[i][j] = dp[hash[a]][i] + 1; // dp[k][i] + 1ret = max(ret, dp[i][j]);}hash[nums[i]] = i; // 保存最近的元素下标 }return ret;}
};
3.7_等差数列划分II-子序列
思路
- 题意分析
- 前面的题要求最长等差数列的长度,而本题要求个数
- 对于本题我们可以采用上题思路的①优化以及①填表顺序
代码
class Solution {
public:int numberOfArithmeticSlices(vector<int>& nums) {int n = nums.size();unordered_map<long long, vector<int>> hash; // <元素 下标>for(int i = 0; i < n; ++i) hash[nums[i]].push_back(i);vector<vector<long long>> dp(n, vector<long long>(n, 0));// dp表的创建+初始化long long sum = 0;for(int j = 2; j < n; ++j) // 固定倒数第二个数{for(int i = 1; i < j; ++i) // 固定最后一个数{long long a = (long long)2 * nums[i] - nums[j];if(hash.count(a))for(auto k : hash[a])if(k < i) dp[i][j] += dp[k][i] + 1; // += dp[k][i] + 1sum += dp[i][j];}}return sum;}
};