动态规划-第六篇

30. 最⻓数对链(medium)
1. 题⽬链接:646. 最长数对链 - 力扣(LeetCode)
2.解法(动态规划):

算法思路:

这道题⽬让我们在数对数组中挑选出来⼀些数对,组成⼀个呈现上升形态的最⻓的数对链。像不像我们整数数组中挑选⼀些数,让这些数组成⼀个最⻓的上升序列?因此,我们可以把问题转化成我

们学过的⼀个模型: 300. 最⻓递增⼦序列。因此我们解决问题的⽅向,应该在「最⻓递增⼦序列」这个模型上。

不过,与整形数组有所区别。在⽤动态规划结局问题之前,应该先把数组排个序。因为我们在计

算 dp[i] 的时候,要知道所有左区间⽐ pairs[i] 的左区间⼩的链对。排完序之后,只⽤

「往前遍历⼀遍」即可。

  1. 状态表⽰:

dp[i] 表⽰以 i 位置的数对为结尾时,最⻓数对链的⻓度。

  1. 状态转移⽅程:

对于 dp[i] ,遍历所有 [0, i - 1] 区间内数对⽤ j 表⽰下标,找出所有满⾜ pairs[j][1] < pairs[i][0] 的 j 。找出⾥⾯最⼤的 dp[j] ,然后加上 1 ,就是以 i 位置为结

尾的最⻓数对链。

  1. 初始化:

刚开始的时候,全部初始化为 1 。

  1. 填表顺序:

根据「状态转移⽅程」,填表顺序应该是「从左往右」。

  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 数组的

值,并形成状态转移。这样,就把已有信息有效地利⽤了起来。

  1. 状态表⽰:

dp[i] 表⽰:以 i 位置的元素为结尾所有的⼦序列中,最⻓的等差⼦序列的⻓度。

  1. 状态转移⽅程:

对于 dp[i] ,上⼀个定差⼦序列的取值定为 arr[i] - difference 。只要找到以上⼀个数字为结尾的定差⼦序列⻓度的 dp[arr[i] - difference] ,然后加上 1 ,就是以 i 为结

尾的定差⼦序列的⻓度。

因此,这⾥可以选择使⽤哈希表做优化。我们可以把「元素, dp[j] 」绑定,放进哈希表中。甚⾄不⽤创建 dp 数组,直接在哈希表中做动态规划。

  1. 初始化:

刚开始的时候,需要把第⼀个元素放进哈希表中, hash[arr[0]] = 1 。

  1. 填表顺序:

根据「状态转移⽅程」,填表顺序应该是「从左往右」。

  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. 解法(动态规划):

算法思路:

  1. 状态表⽰:

对于线性 dp ,我们可以⽤「经验 + 题⽬要求」来定义状态表⽰:

i. 以某个位置为结尾,巴拉巴拉;

ii. 以某个位置为起点,巴拉巴拉。

这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:

dp[i] 表⽰:以 i 位置元素为结尾的「所有⼦序列」中,最⻓的斐波那契⼦数列的⻓度。

但是这⾥有⼀个⾮常致命的问题,那就是我们⽆法确定 i 结尾的斐波那契序列的样⼦。这样就会导致我们⽆法推导状态转移⽅程,因此我们定义的状态表⽰需要能够确定⼀个斐波那契序列。

根据斐波那契数列的特性,我们仅需知道序列⾥⾯的最后两个元素,就可以确定这个序列的样⼦。因此,我们修改我们的状态表⽰为:

dp[i][j] 表⽰:以 i 位置以及 j 位置的元素为结尾的所有的⼦序列中,最⻓的斐波那契⼦序列的⻓度。规定⼀下 i < j 。

  1. 状态转移⽅程:

设 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 之前,将所有的「元素 + 下标」绑定在⼀起,放到哈希表中。

  1. 初始化:

可以将表⾥⾯的值都初始化为 2 。

  1. 填表顺序:

a. 先固定最后⼀个数;

b. 然后枚举倒数第⼆个数。

  1. 返回值:

因为不知道最终结果以谁为结尾,因此返回 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.解法(动态规划):

算法思路:

  1. 状态表⽰:

对于线性 dp ,我们可以⽤「经验 + 题⽬要求」来定义状态表⽰:

i. 以某个位置为结尾,巴拉巴拉;

ii. 以某个位置为起点,巴拉巴拉。

这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:

dp[i] 表⽰:以 i 位置元素为结尾的「所有⼦序列」中,最⻓的等差序列的⻓度。

但是这⾥有⼀个⾮常致命的问题,那就是我们⽆法确定 i 结尾的等差序列的样⼦。这样就会导致

我们⽆法推导状态转移⽅程,因此我们定义的状态表⽰需要能够确定⼀个等差序列。

根据等差序列的特性,我们仅需知道序列⾥⾯的最后两个元素,就可以确定这个序列的样⼦。因此,我们修改我们的状态表⽰为:

dp[i][j] 表⽰:以 i 位置以及 j 位置的元素为结尾的所有的⼦序列中,最⻓的等差序列的⻓度。规定⼀下 i < j 。

  1. 状态转移⽅程:

设 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] 扔到哈希表中。

  1. 初始化:

根据实际情况,可以将所有位置初始化为 2 。

  1. 填表顺序:

a. 先固定倒数第⼆个数;

b. 然后枚举倒数第⼀个数。

  1. 返回值:

由于不知道最⻓的结尾在哪⾥,因此返回 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. 解法(动态规划):

算法思路:

  1. 状态表⽰:

对于线性 dp ,我们可以⽤「经验 + 题⽬要求」来定义状态表⽰:

i. 以某个位置为结尾,巴拉巴拉;

ii. 以某个位置为起点,巴拉巴拉。

这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:

dp[i] 表⽰:以 i 位置元素为结尾的「所有⼦序列」中,等差⼦序列的个数。

但是这⾥有⼀个⾮常致命的问题,那就是我们⽆法确定 i 结尾的等差序列的样⼦。这样就会导致

我们⽆法推导状态转移⽅程,因此我们定义的状态表⽰需要能够确定⼀个等差序列。

根据等差序列的特性,我们仅需知道序列⾥⾯的最后两个元素,就可以确定这个序列的样⼦。因此,我们修改我们的状态表⽰为:

dp[i][j] 表⽰:以 i 位置以及 j 位置的元素为结尾的所有的⼦序列中,等差⼦序列的个数。规定⼀下 i < j 。

  1. 状态转移⽅程:

设 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 之前,将所有元素 + 下标数组绑定在⼀起,放到哈希表中。这⾥为何要保存下标数组,是因为我们要统计个数,所有的下标都需要统计。

  1. 初始化:

刚开始是没有等差数列的,因此初始化 dp 表为 0 。

  1. 填表顺序:

a. 先固定倒数第⼀个数;

b. 然后枚举倒数第⼆个数。

  1. 返回值:

我们要统计所有的等差⼦序列,因此返回 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 的个数即可。

  1. 状态表⽰:

为了能表⽰出来所有的⼦串,我们可以创建⼀个 n * n 的⼆维 dp 表,只⽤到「上三⻆部分」可。

其中, dp[i][j] 表⽰: s 字符串 [i, j] 的⼦串,是否是回⽂串。

  1. 状态转移⽅程:

对于回⽂串,我们⼀般分析⼀个「区间两头」的元素:

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] 。

综上,状态转移⽅程分情况谈论即可。

  1. 初始化:

因为我们的状态转移⽅程分析的很细致,因此⽆需初始化。

  1. 填表顺序:

根据「状态转移⽅程」,我们需要「从下往上」填写每⼀⾏,每⼀⾏的顺序⽆所谓。

  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);
}
};
相关推荐
奋进的小暄10 分钟前
贪心算法(15)(java)用最小的箭引爆气球
算法·贪心算法
Scc_hy23 分钟前
强化学习_Paper_1988_Learning to predict by the methods of temporal differences
人工智能·深度学习·算法
巷北夜未央24 分钟前
Python每日一题(14)
开发语言·python·算法
javaisC26 分钟前
c语言数据结构--------拓扑排序和逆拓扑排序(Kahn算法和DFS算法实现)
c语言·算法·深度优先
爱爬山的老虎27 分钟前
【面试经典150题】LeetCode121·买卖股票最佳时机
数据结构·算法·leetcode·面试·职场和发展
SWHL27 分钟前
rapidocr 2.x系列正式发布
算法
雾月551 小时前
LeetCode 914 卡牌分组
java·开发语言·算法·leetcode·职场和发展
想跑步的小弱鸡1 小时前
Leetcode hot 100(day 4)
算法·leetcode·职场和发展
Fantasydg1 小时前
DAY 35 leetcode 202--哈希表.快乐数
算法·leetcode·散列表
jyyyx的算法博客1 小时前
Leetcode 2337 -- 双指针 | 脑筋急转弯
算法·leetcode