目录
一、最长递增子序列
第一步:确定状态表示
dp[i]:以 i 位置元素为结尾的所有子序列中,最长递增子序列的长度。
第二步:推出状态转移方程
以 i 位置元素为结尾的所有子序列可以分为两种情况:
1、i 位置元素本身就是子序列
2、i 位置元素跟在前面位置元素( 0 <= j < i)的后面构成子序列。
第三步:初始化dp表
因为一个元素单独就可以成为一个递增子序列。所以,以 i 位置元素为结尾的所有子序列中,最长递增子序列的长度至少是1。于是,dp表里面的所有值初始为1,就只需要考虑情况二了。
最后的返回值就是dp表中的最大值。
解题代码:
class Solution
{
public:
int lengthOfLIS(vector<int>& nums)
{
// 动态规划方法
int m = nums.size();
vector<int> dp(m, 1); // 以 i 位置元素结尾的所有子序列中,长度最长子序列的长度
int maxlen = 1;
for(int i = 1; i < m; i++)
{
for(int j = 0; j <= i-1; j++)
{
if(nums[j] < nums[i])
dp[i] = max(dp[j]+1, dp[i]);
}
maxlen = max(maxlen, dp[i]);
}
return maxlen;
}
};
二、摆动序列
第一步:确定状态表示
根据题目要求,题目要求找到作为摆动序列的最长子序列的长度。所以dp[i]表示以 i 位置元素为结尾的所有子摆动序列中,最长摆动序列的长度。
但是,一个摆动序列的最后一个位置有可能是呈现上升趋势或者是下降趋势,所以这又分出了两种状态。
f[i]:表示以 i 位置元素为结尾的所有子序列中,最后一个位置呈现上升趋势的最长摆动序列的长度。
g[i]:表示以 i 位置元素为结尾的所有子序列中,最后一个位置呈现下降趋势的最长摆动序列的长度。
第二步:推出状态转移方程
第三步:初始化dp表
因为一个元素单独就可以成为一个摆动序列。所以,以 i 位置元素为结尾的所有子序列中,最长摆动序列的长度至少是1。于是,f,g表里面的所有值初始为1,就只需要考虑情况二了。
最后的返回值就是f,g表中的最大值。
解题代码:
class Solution
{
public:
int wiggleMaxLength(vector<int>& nums)
{
int m = nums.size();
vector<int> f(m, 1);
vector<int> g(m, 1);
int len = 1;
for(int i = 1; i < m; i++)
{
for(int j = 0; j <= i-1; j++)
{
if(nums[j] > nums[i])
{
g[i] = max(g[i], f[j] + 1);
}
else if(nums[j] < nums[i])
{
f[i] = max(f[i], g[j] + 1);
}
}
len = max(len, max(g[i], f[i]));
}
return len;
}
};
三、最长递增子序列的个数
确定状态表示:
dp[i]:表示以 i 位置元素为结尾的所有子序列中,最长递增子序列的个数。
len[i]:表示以 i 位置元素为结尾的所有子序列中,最长递增子序列的长度。
解题代码:
class Solution
{
public:
int findNumberOfLIS(vector<int>& nums)
{
int m = nums.size();
vector<int> dp(m, 1); // 最长递增子序列的个数
vector<int> len(m, 1); // 最长递增子序列的长度
int ret = 1;
int maxlen = 1;
for(int i = 1; i < m; i++)
{
for(int j = 0; j < i; j++)
{
if(nums[i] > nums[j])
{
if(len[j] + 1 > len[i])
{
len[i] = len[j] + 1;
dp[i] = dp[j];
}
else if(len[j] + 1 == len[i])
dp[i] += dp[j];
}
}
if(maxlen < len[i])
{
maxlen = len[i];
ret = dp[i];
}
else if(maxlen == len[i])
ret += dp[i];
}
return ret;
}
};
四、最长数对链
预处理:对数组进行排序。
第一步:确定状态表示
dp[i]:表示以 i 位置元素为结尾的所有数对链,最长的数对链的长度。
第二步:推出状态转移方程
第三步:初始化dp表
将dp表中元素都初始化为1,这样就不用考虑情况二了。最后的返回值就是 dp 表里面的最大值。
解题代码:
class Solution
{
public:
int findLongestChain(vector<vector<int>>& pairs)
{
sort(pairs.begin(), pairs.end());
int m = pairs.size();
vector<int> dp(m, 1); // 最长数对链的长度
int ret = 1;
for(int i = 1; i < m; i++)
{
for(int j = 0; j < i; j++)
{
if(pairs[j][1] < pairs[i][0])
{
dp[i] = max(dp[i], dp[j]+1);
}
ret = max(ret, dp[i]);
}
}
return ret;
}
};
五、最长定差子序列
第一步:确定状态表示
dp[i]:表示以 i 位置元素为结尾的最长定差子序列的长度。
第二步:推出状态转移方程
第三步:初始化dp表
因为单独一个元素可以成为定差序列,所以序列长度至少为1,那么我们可以将dp表初始化为1,就可以只考虑情况二了。最后的返回值是dp表中的最大值。
那么,我们根据上面的思路来编写代码。
提交后,我们发现超时了,为什么呢?
因为这道题的数据量有点大,并且我们每次是从前往后依次遍历,去找位于 i 位置元素之前的元素,这样时间复杂度当然大了。所以我们需要进行优化。
代码优化
代码的优化思路就在于上面我们所提到的—— “每次是从前往后依次遍历,去找位于 i 位置元素之前的元素”。也就是说每次都要去遍历数组。
那么我们应该想一个方法,可以不经过遍历,直接获取到 i 位置元素的前一个元素的下标。
所以,我们可以使用 unordered_map,其中存 <arr[i],i>。
解题代码:
class Solution
{
public:
int longestSubsequence(vector<int>& arr, int difference)
{
int m = arr.size();
vector<int> dp(m, 1);
int ret = INT_MIN;
unordered_map<int, int> hash; // (arr[i], dp[i])
hash[arr[0]] = 1;
for(int i = 1; i < m; i++)
{
if(hash.count(arr[i] - difference))
dp[i] = max(hash[arr[i] - difference] + 1, dp[i]);
hash[arr[i]] = dp[i];
ret = max(ret, dp[i]);
}
return ret;
}
};
六、最长斐波那契子序列的长度
初步思考:dp[i]表示以 i 位置元素为结尾的最长的斐波那契子序列的长度。按照之前的思路,我们需要去找 i 位置元素的前一个元素。可是,对于斐波那契数来说,我们能够根据一个元素去确定它的前一个元素吗?
显然是不能的,因为一个斐波那契数由它前面的两个元素来确定。比如上面示例2,如果 i 位置元素是11,那么它的前面可以是1,3,7等等,无法确定它前面具体是哪一个元素。
因此,我们不能这么简单地确定状态表示。
第一步:确定状态表示
dp[i][j]:表示以 i 位置元素和 j 位置元素为结尾的最长的斐波那契子序列的长度。(其中规定i < j)。
第二步:推出状态转移方程
如果长度为2,我们表示无效。
第三步:初始化dp表
将dp表初始化为2。
为了能够快速找到 arr[x],我们可以使用 unordered_map<int, int> hash。存放 < arr[i],i>。
解题代码:
class Solution
{
public:
int lenLongestFibSubseq(vector<int>& arr)
{
int m = arr.size();
vector<vector<int>> dp(m, vector<int>(m, 2));
unordered_map<int, int> hash; // (arr[i], i)
for(int i = 0; i < m; i++)
hash[arr[i]] = i;
int len = INT_MIN;
for(int j = 2; j < m; j++)
{
for(int i = 1; i < j; i++)
{
int x = arr[j] - arr[i];
if(hash.count(x) && x < arr[i])
{
dp[i][j] = dp[hash[x]][i] + 1;
}
len = max(len, dp[i][j]);
}
}
return len == 2 ? 0 : len;
}
};
七、最长等差数列
初步思考:dp[i]表示以 i 位置元素为结尾的最长的等差序列的长度。按照之前的思路,我们需要去找 i 位置元素的前一个元素。可是,对于等差数列来说,我们能够根据一个元素去确定它的前一个元素吗?
显然是不能的,因为它的公差是不能确定的,我们至少需要两个数才能确定公差,进而去推出其他的数。
第一步:确定状态表示
dp[i][j]:表示以 i 位置元素和 j 位置元素为结尾的最长等差数列的长度。
第二步:推出状态转移方程
第三步:初始化dp表
因为单独两个数也能构成等差数列,所以将dp表初始化为2。
为了能够快速找到 arr[x],我们可以使用 unordered_map<int, int> hash。存放 < arr[i],i>。
解题代码:
class Solution
{
public:
int longestArithSeqLength(vector<int>& nums)
{
int m = nums.size();
vector<vector<int>> dp(m, vector<int>(m, 2));
unordered_map<int, int> hash; // (nums[i], i)
hash[nums[0]] = 0;
int len = 2;
for(int i = 1; i < m; i++)
{
for(int j = i+1; j < m; j++)
{
int a = 2 * nums[i] - nums[j];
if(hash.count(a))
dp[i][j] = dp[hash[a]][i] + 1;
len = max(len, dp[i][j]);
}
hash[nums[i]] = i;
}
return len;
}
};
八、等差序列划分II-子序列
第一步:确定状态表示
dp[i][j]:表示以 i 位置元素和 j 位置元素为结尾的等差子序列的个数。
第二步:推出状态转移方程
知道了最后两个元素,我们可以根据等差数列的性质求出前一个元素的值。
第三步:算法优化
在 i 位置之前,可能会有很多个值都为a,那么 i 位置元素和 j 位置元素可以跟在它们任何一个元素之后而形成等差数列。如果我们通过遍历去找这些元素的话,时间复杂度就会很大。
所以,我们可以使用 unordered_map<int,vector<int>> hash。存 <a,下标数组>,通过hash去快速找到所有a值所在的位置。
解题代码:
class Solution
{
public:
int numberOfArithmeticSlices(vector<int>& nums)
{
int m = nums.size();
unordered_map<long long, vector<int>> hash; // (元素,下标)
for(int i = 0; i < m; i++)
hash[nums[i]].push_back(i);
vector<vector<int>> dp(m, vector<int>(m));
int sum = 0;
for(int j = 2; j < m; j++)
{
for(int i = j-1; i >= 0; i--)
{
long long num = (long long)2 * nums[i] - nums[j];
if(hash.count(num))
{
for(auto k : hash[num])
{
if(k < i)
{
dp[i][j] += dp[k][i] + 1;
}
}
}
sum += dp[i][j];
}
}
return sum;
}
};