30. 最⻓数对链(medium)
1. 题⽬链接:646. 最长数对链 - 力扣(LeetCode)
2.解法(动态规划):
算法思路:
这道题⽬让我们在数对数组中挑选出来⼀些数对,组成⼀个呈现上升形态的最⻓的数对链。像不像我们整数数组中挑选⼀些数,让这些数组成⼀个最⻓的上升序列?因此,我们可以把问题转化成我
们学过的⼀个模型: 300. 最⻓递增⼦序列。因此我们解决问题的⽅向,应该在「最⻓递增⼦序列」这个模型上。
不过,与整形数组有所区别。在⽤动态规划结局问题之前,应该先把数组排个序。因为我们在计
算 dp[i] 的时候,要知道所有左区间⽐ pairs[i] 的左区间⼩的链对。排完序之后,只⽤
「往前遍历⼀遍」即可。
- 状态表⽰:
dp[i] 表⽰以 i 位置的数对为结尾时,最⻓数对链的⻓度。
- 状态转移⽅程:
对于 dp[i] ,遍历所有 [0, i - 1] 区间内数对⽤ j 表⽰下标,找出所有满⾜ pairs[j][1] < pairs[i][0] 的 j 。找出⾥⾯最⼤的 dp[j] ,然后加上 1 ,就是以 i 位置为结
尾的最⻓数对链。
- 初始化:
刚开始的时候,全部初始化为 1 。
- 填表顺序:
根据「状态转移⽅程」,填表顺序应该是「从左往右」。
- 返回值:
根据「状态表⽰」,返回整个 dp 表中的最⼤值。
3.C++ 算法代码:
cpp
class Solution {
public:
int findLongestChain(vector<vector<int>>& pairs) {
sort(pairs.begin(),pairs.end());//升序
int n=pairs.size();
vector<int> dp(n,1);
int res=1;
for(int i=1;i<n;i++)
{
for(int j=0;j<i;j++)
{
if(pairs[i][0]>pairs[j][1])
{
dp[i]=max(dp[i],dp[j]+1);
}
}
res=max(res,dp[i]);
}
return res;
}
};
31. 最⻓定差⼦序列(medium)
1. 题⽬链接:1218. 最长定差子序列 - 力扣(LeetCode)
2.解法(动态规划):
算法思路:
这道题和 300. 最⻓递增⼦序列 有⼀些相似,但仔细读题就会发现,本题的 arr.lenght ⾼达 10^5 ,使⽤ O(N^2) 的 lcs 模型⼀定会超时。
那么,它有什么信息是 300. 最⻓递增⼦序列 的呢?是定差。之前,我们只知道要递增,不知道前⼀个数应当是多少;现在我们可以计算出前⼀个数是多少了,就可以⽤数值来定义 dp 数组的
值,并形成状态转移。这样,就把已有信息有效地利⽤了起来。
- 状态表⽰:
dp[i] 表⽰:以 i 位置的元素为结尾所有的⼦序列中,最⻓的等差⼦序列的⻓度。
- 状态转移⽅程:
对于 dp[i] ,上⼀个定差⼦序列的取值定为 arr[i] - difference 。只要找到以上⼀个数字为结尾的定差⼦序列⻓度的 dp[arr[i] - difference] ,然后加上 1 ,就是以 i 为结
尾的定差⼦序列的⻓度。
因此,这⾥可以选择使⽤哈希表做优化。我们可以把「元素, dp[j] 」绑定,放进哈希表中。甚⾄不⽤创建 dp 数组,直接在哈希表中做动态规划。
- 初始化:
刚开始的时候,需要把第⼀个元素放进哈希表中, hash[arr[0]] = 1 。
- 填表顺序:
根据「状态转移⽅程」,填表顺序应该是「从左往右」。
- 返回值:
根据「状态表⽰」,返回整个 dp 表中的最⼤值。
3.C++ 算法代码:
cpp
class Solution {
public:
int longestSubsequence(vector<int>& arr, int difference) {
unordered_map<int,int> hash;
int n=arr.size();
int ret=1;
for(int i=0;i<n;i++)
{
hash[arr[i]]=hash[arr[i]-difference]+1;//没有解相当于初始化为1
ret=max(ret,hash[arr[i]]);
}
//子序列,可以不连续,值对应,长度 4 ,4-difference;
return ret;
}
};
32. 最⻓的斐波那契⼦序列的⻓度(medium)
1. 题⽬链接:873. 最长的斐波那契子序列的长度 - 力扣(LeetCode)
2. 解法(动态规划):
算法思路:
- 状态表⽰:
对于线性 dp ,我们可以⽤「经验 + 题⽬要求」来定义状态表⽰:
i. 以某个位置为结尾,巴拉巴拉;
ii. 以某个位置为起点,巴拉巴拉。
这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:
dp[i] 表⽰:以 i 位置元素为结尾的「所有⼦序列」中,最⻓的斐波那契⼦数列的⻓度。
但是这⾥有⼀个⾮常致命的问题,那就是我们⽆法确定 i 结尾的斐波那契序列的样⼦。这样就会导致我们⽆法推导状态转移⽅程,因此我们定义的状态表⽰需要能够确定⼀个斐波那契序列。
根据斐波那契数列的特性,我们仅需知道序列⾥⾯的最后两个元素,就可以确定这个序列的样⼦。因此,我们修改我们的状态表⽰为:
dp[i][j] 表⽰:以 i 位置以及 j 位置的元素为结尾的所有的⼦序列中,最⻓的斐波那契⼦序列的⻓度。规定⼀下 i < j 。
- 状态转移⽅程:
设 nums[i] = b, nums[j] = c ,那么这个序列的前⼀个元素就是 a = c - b 。我们根据 a 的情况讨论:
i. a 存在,下标为 k ,并且 a < b :此时我们需要以 k 位置以及 i 位置元素为结尾的
最⻓斐波那契⼦序列的⻓度,然后再加上 j 位置的元素即可。于是 dp[i][j] =dp[k][i] + 1 ;
ii. a 存在,但是 b < a < c :此时只能两个元素⾃⼰玩了, dp[i][j] = 2 ; iii. a 不存在:此时依旧只能两个元素⾃⼰玩了, dp[i][j] = 2 。
综上,状态转移⽅程分情况讨论即可。
优化点:我们发现,在状态转移⽅程中,我们需要确定 a 元素的下标。因此我们可以在 dp 之前,将所有的「元素 + 下标」绑定在⼀起,放到哈希表中。
- 初始化:
可以将表⾥⾯的值都初始化为 2 。
- 填表顺序:
a. 先固定最后⼀个数;
b. 然后枚举倒数第⼆个数。
- 返回值:
因为不知道最终结果以谁为结尾,因此返回 dp 表中的最⼤值 ret 。 但是 ret 可能⼩于 3 ,⼩于 3 的话说明不存在。
因此需要判断⼀下。
3.C++ 算法代码:
cpp
class Solution {
public:
int lenLongestFibSubseq(vector<int>& arr) {
int n=arr.size();
vector<vector<int>> dp(n,vector<int>(n,2));
int ret=2;
for(int j=2;j<n;j++)
{
int pre=0,i=j-1;
while(pre<i)
{
int sum=arr[pre]+arr[i];
if(sum==arr[j])
{
dp[i][j]=dp[pre][i]+1;
ret=max(ret,dp[i][j]);
i--;
}
if(sum>arr[j]) i--;// i不变,此时pre 前面的数,+arr[i]都不满足==arr[j] 由于逐渐递增arr[pre]+arr[i]>arr[j],此时这个i没有解了,所以缩小范围。
//说明arr[pre]小了,i还有可行解,pre++;
if(sum<arr[j]) pre++;
}
//一次while循环就解决了所有i下标问题, 避免了O(N3);
}
return (ret<3)?0:ret;
}
};
33. 最⻓等差数列(medium)
1. 题⽬链接:1027. 最长等差数列 - 力扣(LeetCode)
2.解法(动态规划):
算法思路:
- 状态表⽰:
对于线性 dp ,我们可以⽤「经验 + 题⽬要求」来定义状态表⽰:
i. 以某个位置为结尾,巴拉巴拉;
ii. 以某个位置为起点,巴拉巴拉。
这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:
dp[i] 表⽰:以 i 位置元素为结尾的「所有⼦序列」中,最⻓的等差序列的⻓度。
但是这⾥有⼀个⾮常致命的问题,那就是我们⽆法确定 i 结尾的等差序列的样⼦。这样就会导致
我们⽆法推导状态转移⽅程,因此我们定义的状态表⽰需要能够确定⼀个等差序列。
根据等差序列的特性,我们仅需知道序列⾥⾯的最后两个元素,就可以确定这个序列的样⼦。因此,我们修改我们的状态表⽰为:
dp[i][j] 表⽰:以 i 位置以及 j 位置的元素为结尾的所有的⼦序列中,最⻓的等差序列的⻓度。规定⼀下 i < j 。
- 状态转移⽅程:
设 nums[i] = b, nums[j] = c ,那么这个序列的前⼀个元素就是 a = 2 * b - c 。我们根据 a 的情况讨论:
a. a 存在,下标为 k ,并且 a < b :此时我们需要以 k 位置以及 i 位置元素为结尾的最
⻓等差序列的⻓度,然后再加上 j 位置的元素即可。于是 dp[i][j] = dp[k][i] +
1 。这⾥因为会有许多个 k ,我们仅需离 i 最近的 k 即可。因此任何最⻓的都可以以 k为结尾;
b. a 存在,但是 b < a < c :此时只能两个元素⾃⼰玩了, dp[i][j] = 2 ;
c. a 不存在:此时依旧只能两个元素⾃⼰玩了, dp[i][j] = 2 。
综上,状态转移⽅程分情况讨论即可。
优化点:我们发现,在状态转移⽅程中,我们需要确定 a 元素的下标。因此我们可以将所有的元素 +下标绑定在⼀起,放到哈希表中,这⾥有两种策略:
a. 在 dp 之前,放⼊哈希表中。这是可以的,但是需要将下标形成⼀个数组放进哈希表中。这样
时间复杂度较⾼,我帮⼤家试过了,超时。
b. ⼀边 dp ,⼀边保存。这种⽅式,我们仅需保存最近的元素的下标,不⽤保存下标数组。但是
⽤这种⽅法的话,我们在遍历顺序那⾥,先固定倒数第⼆个数,再遍历倒数第⼀个数。这样就
可以在 i 使⽤完时候,将 nums[i] 扔到哈希表中。
- 初始化:
根据实际情况,可以将所有位置初始化为 2 。
- 填表顺序:
a. 先固定倒数第⼆个数;
b. 然后枚举倒数第⼀个数。
- 返回值:
由于不知道最⻓的结尾在哪⾥,因此返回 dp 表中的最⼤值。
3.C++ 算法代码
cpp
class Solution {
public:
int longestArithSeqLength(vector<int>& nums) {
unordered_map<int,int> mp;
int n=nums.size();
vector<vector<int>> dp(n,vector<int>(n,2));//全部初始化为2
int ret=2;
for(int j=0;j<n;j++)
{
for(int i=j+1;i<n;i++)
{
int a=2*nums[j]-nums[i];
if(mp.count(a))
{
dp[j][i]=dp[mp[a]][j]+1;
}
ret=max(ret,dp[j][i]);
}
mp[nums[j]]=j;
}
return ret;
}
};
34. 等差数列划分II - ⼦序列(hard)
1. 题⽬链接:446. 等差数列划分 II - 子序列 - 力扣(LeetCode)
2. 解法(动态规划):
算法思路:
- 状态表⽰:
对于线性 dp ,我们可以⽤「经验 + 题⽬要求」来定义状态表⽰:
i. 以某个位置为结尾,巴拉巴拉;
ii. 以某个位置为起点,巴拉巴拉。
这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:
dp[i] 表⽰:以 i 位置元素为结尾的「所有⼦序列」中,等差⼦序列的个数。
但是这⾥有⼀个⾮常致命的问题,那就是我们⽆法确定 i 结尾的等差序列的样⼦。这样就会导致
我们⽆法推导状态转移⽅程,因此我们定义的状态表⽰需要能够确定⼀个等差序列。
根据等差序列的特性,我们仅需知道序列⾥⾯的最后两个元素,就可以确定这个序列的样⼦。因此,我们修改我们的状态表⽰为:
dp[i][j] 表⽰:以 i 位置以及 j 位置的元素为结尾的所有的⼦序列中,等差⼦序列的个数。规定⼀下 i < j 。
- 状态转移⽅程:
设 nums[i] = b, nums[j] = c ,那么这个序列的前⼀个元素就是 a = 2 * b - c 。我们根据 a 的情况讨论:
a. a 存在,下标为 k ,并且 a < b :此时我们知道以 k 元素以及 i 元素结尾的等差序列的个数 dp[k][i] ,在这些⼦序列的后⾯加上 j 位置的元素依旧是等差序列。但是这⾥会多出来⼀个以 k, i, j 位置的元素组成的新的等差序列,因此 dp[i][j] = dp[k][i] + 1 ;
b. 因为 a 可能有很多个,我们需要全部累加起来。 综上, dp[i][j] += dp[k][i] + 1 。
优化点:我们发现,在状态转移⽅程中,我们需要确定 a 元素的下标。因此我们可以在 dp 之前,将所有元素 + 下标数组绑定在⼀起,放到哈希表中。这⾥为何要保存下标数组,是因为我们要统计个数,所有的下标都需要统计。
- 初始化:
刚开始是没有等差数列的,因此初始化 dp 表为 0 。
- 填表顺序:
a. 先固定倒数第⼀个数;
b. 然后枚举倒数第⼆个数。
- 返回值:
我们要统计所有的等差⼦序列,因此返回 dp 表中所有元素的和。
3.C++ 算法代码:
cpp
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
unordered_map<long long,vector<int>> hash;//因为有重复值,vector里面存的是不同的下标;
int n=nums.size();
vector<vector<int>> dp(n,vector<int>(n,0));
int ret=0;
for(int i=0;i<n;i++)
{
for(int j=i+1;j<n;j++)
{
long long a=(long long)2*nums[i]-nums[j];//此处有超时风险
if(hash.count(a))
{
for(int ch:hash[a])//取下标
{
dp[i][j]+=dp[ch][i]+1;
}
}
ret+=dp[i][j];
}
hash[nums[i]].push_back(i);
}
return ret;
}
};
回⽂⼦串问题
35. 回⽂⼦串(medium)
1. 题⽬链接:647. 回文子串 - 力扣(LeetCode)
2.解法(动态规划)://枚举所有子串
算法思路:
我们可以先「预处理」⼀下,将所有⼦串「是否回⽂」的信息统计在 dp 表⾥⾯,然后直接在表⾥⾯统计 true 的个数即可。
- 状态表⽰:
为了能表⽰出来所有的⼦串,我们可以创建⼀个 n * n 的⼆维 dp 表,只⽤到「上三⻆部分」可。
其中, dp[i][j] 表⽰: s 字符串 [i, j] 的⼦串,是否是回⽂串。
- 状态转移⽅程:
对于回⽂串,我们⼀般分析⼀个「区间两头」的元素:
i. 当 s[i] != s[j] 的时候:不可能是回⽂串, dp[i][j] = 0 ;
ii. 当 s[i] == s[j] 的时候:根据⻓度分三种情况讨论:
• ⻓度为 1 ,也就是 i == j :此时⼀定是回⽂串, dp[i][j] = true ;
• ⻓度为 2 ,也就是 i + 1 == j :此时也⼀定是回⽂串, dp[i][j] = true ;
• ⻓度⼤于 2 ,此时要去看看 [i + 1, j - 1] 区间的⼦串是否回⽂: dp[i][j]= dp[i + 1][j - 1] 。
综上,状态转移⽅程分情况谈论即可。
- 初始化:
因为我们的状态转移⽅程分析的很细致,因此⽆需初始化。
- 填表顺序:
根据「状态转移⽅程」,我们需要「从下往上」填写每⼀⾏,每⼀⾏的顺序⽆所谓。
- 返回值:
根据「状态表⽰和题⽬要求」,我们需要返回 dp 表中 true 的个数。
3.C++ 算法代码:
cpp
class Solution {
public:
int countSubstrings(string s) {
int n=s.size();
vector<vector<bool>> dp(n,vector<bool>(n));
int ret=0;
for(int i=n-1;i>=0;i--)
{
for(int j=i;j<n;j++)//j=i可以说自己
{
if(s[i]==s[j])
dp[i][j]=i+1<j?dp[i+1][j-1]:true;//i+1<j可以防止越界;
if(dp[i][j]) ret++;
}
}
return ret;
}
};
36. 最⻓回⽂⼦串(medium)
1. 题⽬链接:5. 最长回文子串 - 力扣(LeetCode)
2. 解法(动态规划):
算法思路:
a. 我们可以先⽤ dp 表统计出「所有⼦串是否回⽂」的信息
b. 然后根据 dp 表⽰ true 的位置,得到回⽂串的「起始位置」和「⻓度」。
那么我们就可以在表中找出最⻓回⽂串。
关于「预处理所有⼦串是否回⽂」,已经在上⼀道题⽬⾥⾯讲过,这⾥就不再赘述啦~
3.C++ 算法代码
cpp
class Solution
{
public:
string longestPalindrome(string s)
{
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回值
int n = s.size();
vector<vector<bool>> dp(n, vector<bool>(n));
int len = 1, begin = 0; // 标记最⻓⼦串的起始位置和⻓度
for(int i = n - 1; i >= 0; i--)
{
for(int j = i; j < n; j++)
{
if(s[i] == s[j])
dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : true;
if(dp[i][j] && j - i + 1 > len) // 找出⼦串中最⻓的回⽂⼦串
len = j - i + 1, begin = i;
}
}
return s.substr(begin, len);
}
};