算法沉淀——动态规划之子序列问题

01.最长递增子序列02.摆动序列03.最长递增子序列的个数04.最长数对链

01.最长递增子序列

题目链接:https://leetcode.cn/problems/longest-increasing-subsequence/

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]

输出:4

解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

输入:nums = [0,1,0,3,2,3]

输出:4

示例 3:

输入:nums = [7,7,7,7,7,7,7]

输出:1

提示:

1 <= nums.length <= 2500-104 <= nums[i] <= 104

思路

1. 状态表达: 我们定义一个动态规划数组 dp,其中 dp[i] 表示以第 i 个位置元素为结尾的所有「子序列」中,最长递增子序列的长度。

2. 状态转移方程: 针对 dp[i] 的状态,我们考虑「子序列的构成方式」进行分类讨论:

当子序列长度为 1 时,只包含自身,因此 dp[i] = 1;当子序列长度大于 1 时,我们遍历前面的所有元素 j(0 <= j <= i - 1),检查是否可以将第 i 个位置的元素接在第 j 个元素之后形成递增子序列。如果 nums[j] < nums[i],说明可以构成递增序列,更新状态:dp[i] = max(dp[j] + 1, dp[i])。

3. 初始化: 初始时,每个元素都可以构成长度为 1 的递增子序列,因此将整个 dp 数组初始化为 1。

4. 填表顺序: 我们按照从左到右的顺序遍历数组。

5. 返回值: 由于不知道最长递增子序列以哪个元素结尾,我们返回 dp 数组中的最大值。这样的动态规划算法通过遍历数组,不断更新状态,最终得到以每个位置为结尾的子序列中最长递增子序列的长度。最后返回数组中所有元素的最大值。

代码

class Solution {

public:

int lengthOfLIS(vector& nums) {

int n=nums.size();

vector dp(n,1);

int ret=1;

for(int i=1;i

for(int j=0;j

if(nums[j]

dp[i]=max(dp[i],dp[j]+1);

}

ret=max(ret,dp[i]);

}

return ret;

}

};

02.摆动序列

题目链接:https://leetcode.cn/problems/wiggle-subsequence/

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 **摆动序列 。**第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。

示例 1:

输入:nums = [1,7,4,9,2,5]

输出:6

解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。

示例 2:

输入:nums = [1,17,5,10,13,15,10,5,16,8]

输出:7

解释:这个序列包含几个长度为 7 摆动序列。

其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。

示例 3:

输入:nums = [1,2,3,4,5,6,7,8,9]

输出:2

提示:

1 <= nums.length <= 10000 <= nums[i] <= 1000

思路

1. 状态表达: 我们定义两个动态规划数组 f 和 g,其中:

f[i] 表示以第 i 个位置元素为结尾的所有的子序列中,最后一个位置呈现「上升趋势」的最长摆动序列的长度。g[i] 表示以第 i 个位置元素为结尾的所有的子序列中,最后一个位置呈现「下降趋势」的最长摆动序列的长度。

2. 状态转移方程: 对于 f[i] 的状态,我们考虑子序列的构成方式:

当子序列长度为 1 时,只包含自身,因此 f[i] = 1。当子序列长度大于 1 时,遍历前面的所有元素 j(0 <= j <= i - 1),检查是否可以将第 i 个位置的元素接在第 j 个元素之后形成递增子序列。如果 nums[j] < nums[i],则在满足这个条件下,要使 j 结尾呈现下降状态,最长的摆动序列即为 g[j] + 1。更新状态:f[i] = max(g[j] + 1, f[i])。

对于 g[i] 的状态,我们同样考虑子序列的构成方式:

当子序列长度为 1 时,只包含自身,因此 g[i] = 1。当子序列长度大于 1 时,遍历前面的所有元素 j(0 <= j <= i - 1),检查是否可以将第 i 个位置的元素接在第 j 个元素之后形成递减子序列。如果 nums[j] > nums[i],则在满足这个条件下,要使 j 结尾呈现上升状态,最长的摆动序列即为 f[j] + 1。更新状态:g[i] = max(f[j] + 1, g[i])。

3. 初始化: 初始时,每个元素都可以构成长度为 1 的摆动序列,因此将整个 f 和 g 数组初始化为 1。

4. 填表顺序: 按照从左到右的顺序遍历数组。

5. 返回值: 返回 f 和 g 数组中的最大值。这样的动态规划算法通过遍历数组,不断更新状态,最终得到以每个位置为结尾的子序列中最长的摆动序列长度。最后返回两个数组中的最大值。

代码

class Solution {

public:

int wiggleMaxLength(vector& nums) {

int n=nums.size();

vector f(n,1),g(n,1);

int ret=1;

for(int i=1;i

for(int j=0;j

if(nums[j]

else if(nums[j]>nums[i]) g[i]=max(f[j]+1,g[i]);

}

ret=max(ret,max(g[i],f[i]));

}

return ret;

}

};

03.最长递增子序列的个数

题目链接:https://leetcode.cn/problems/number-of-longest-increasing-subsequence/

给定一个未排序的整数数组 nums , 返回最长递增子序列的个数 。

注意 这个数列必须是 严格 递增的。

示例 1:

输入: [1,3,5,4,7]

输出: 2

解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。

示例 2:

输入: [2,2,2,2,2]

输出: 5

解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。

提示:

1 <= nums.length <= 2000

-106 <= nums[i] <= 106

思路

1. 状态表达: 我们定义两个动态规划数组 len 和 count,其中:

len[i] 表示以第 i 个位置为结尾的最长递增子序列的长度。count[i] 表示以第 i 个位置为结尾的最长递增子序列的个数。

2. 状态转移方程: 首先,对于 len[i] 的状态,我们考虑子序列的构成方式:

当子序列长度为 1 时,只包含自身,因此 len[i] = 1。当子序列长度大于 1 时,遍历前面的所有元素 j(0 <= j < i),检查是否可以将第 i 个位置的元素接在第 j 个元素之后形成递增子序列。如果 nums[j] < nums[i],则在满足这个条件下,要使 j 结尾呈现上升状态,最长的递增子序列即为 len[j] + 1。更新状态:len[i] = max(len[j] + 1, len[i])。

接着,对于 count[i] 的状态,我们考虑子序列的构成方式:

我们此时已经知道 len[i] 的信息,还知道 [0, i - 1] 区间上的 count[j] 信息,使用 j 表示 [0, i - 1] 区间上的下标。我们可以再遍历一遍 [0, i - 1] 区间上的所有元素,只要能够构成上升序列,并且上升序列的长度等于 len[i],那么我们就把 count[i] 加上 count[j] 的值。这样循环一遍之后,count[i] 存的就是我们想要的值。更新状态:count[i] += count[j],其中 0 <= j < i 且 nums[j] < nums[i] && len[j] + 1 == len[i]。

3. 初始化:

对于 len[i],所有元素自己就能构成一个递增序列,直接全部初始化为 1。对于 count[i],我们先初始化为 0,然后在累加的时候判断。

4. 填表顺序: 从左往右遍历数组。

5. 返回值: 最终的最长递增子序列的长度为 maxLen。根据题目要求,返回所有长度等于 maxLen 的子序列的个数。

代码

class Solution {

public:

int findNumberOfLIS(vector& nums) {

int n = nums.size();

vector len(n, 1), count(n, 1);

int retlen = 1, retcount = 1;

for (int i = 1; i < n; i++)

{

for (int j = 0; j < i; j++)

{

if (nums[j] < nums[i])

{

if (len[j] + 1 == len[i])

count[i] += count[j];

else if (len[j] + 1 > len[i])

len[i] = len[j] + 1, count[i] = count[j];

}

}

if (retlen == len[i])

retcount += count[i];

else if (retlen < len[i])

retlen = len[i], retcount = count[i];

}

return retcount;

}

};

04.最长数对链

题目链接:https://leetcode.cn/problems/maximum-length-of-pair-chain/

给你一个由 n 个数对组成的数对数组 pairs ,其中 pairs[i] = [lefti, righti] 且 lefti < righti 。

现在,我们定义一种 跟随 关系,当且仅当 b < c 时,数对 p2 = [c, d] 才可以跟在 p1 = [a, b] 后面。我们用这种形式来构造 数对链 。

找出并返回能够形成的 最长数对链的长度 。

你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

示例 1:

输入:pairs = [[1,2], [2,3], [3,4]]

输出:2

解释:最长的数对链是 [1,2] -> [3,4] 。

示例 2:

输入:pairs = [[1,2],[7,8],[4,5]]

输出:3

解释:最长的数对链是 [1,2] -> [4,5] -> [7,8] 。

提示:

n == pairs.length1 <= n <= 1000-1000 <= lefti < righti <= 1000

思路

排序: 首先,对数对数组按照每个数对的第一个元素进行升序排序。这是为了方便后续动态规划的处理。状态表达: 我们定义动态规划数组 dp,其中 dp[i] 表示以第 i 个数对为结尾的最长数对链的长度。状态转移方程: 对于 dp[i],遍历所有 [0, i - 1] 区间内的数对,用 j 表示下标。找出所有满足 pairs[j][1] < pairs[i][0] 的 j,然后找出其中最大的 dp[j],最后加上 1 就是以第 i 个数对为结尾的最长数对链。状态转移方程为:dp[i] = max(dp[j] + 1, dp[i]),其中 0 <= j < i。初始化: 刚开始的时候,全部初始化为 1。填表顺序: 根据状态转移方程,填表顺序是从左往右。返回值: 根据状态表达,返回整个 dp 数组中的最大值。

代码

class Solution {

public:

int findLongestChain(vector>& pairs) {

sort(pairs.begin(),pairs.end());

int n=pairs.size();

vector dp(n,1);

int ret=1;

for(int i=1;i

for(int j=0;j

if(pairs[i][0]>pairs[j][1]) dp[i]=max(dp[i],dp[j]+1);

}

ret=max(ret,dp[i]);

}

return ret;

}

};

精彩链接

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。