动态规划:路径和子数组问题(C++)

动态规划:路径和子数组问题

    • 路径问题
      • 1.不同路径(中等)
      • 2.不同路径II(中等)
      • 3.下降路径最⼩和(中等)
      • 4.地下城游戏(困难)
    • 子数组问题
      • 1.最大子数组和(中等)
      • 2.环形子数组的最大和(中等)
      • 3.乘积最大子数组(中等)
      • 4.乘积为正数的最长子数组(中等)
      • 5.等差数列划分(中等)
      • 6.最长湍流子数组(中等)
      • 7.单词拆分(中等)
      • 8.环绕字符串中唯⼀的子字符串(中等)

路径问题

1.不同路径(中等)

链接:不同路径

  • 题目描述
    在这里插入图片描述

  • 做题步骤

  1. 状态表示
    尝试定义状态表示为到达[m, n]位置的路径数
    在这里插入图片描述

  2. 状态转移方程
    通过上述分析,可知状态转移方程为:
    dp[i][j] = dp[i - 1][j] + dp[i][j - 1]

  3. 初始化
    在这里插入图片描述

  4. 填表顺序
    保证填当前状态时,所需状态已经计算过,从起点出发,填表顺序为从上往下,每一行从左往右

  5. 返回值
    根据状态表示,返回的应该是dp[m][n],即到达终点的路径数。

  • 代码实现
class Solution {
public:int uniquePaths(int m, int n) {//dp[i][j]表示到达该位置的路径vector<vector<int>> dp(m+1, vector<int>(n+1,0));dp[0][1] = 1;for(int i = 1; i <= m; i++){for(int j = 1; j <= n;j++){dp[i][j] = dp[i][j - 1] + dp[i - 1][j];}} return dp[m][n];//时间复杂度:O(N)//空间复杂度:O(N^M)}
};//滚动数组优化
// class Solution {
// public:
//     int uniquePaths(int m, int n) 
//     {
//         vector<int> dp(n + 1);
//         dp[1] = 1;//         for(int i = 1; i <= m; i++)
//         {
//             for(int j = 1; j <= n;j++)
//             {
//                 dp[j] += dp[j-1];
//             }
//         } 
//         return dp[n];
//     }
// };

2.不同路径II(中等)

链接:不同路径II

  • 题目描述
    在这里插入图片描述

  • 做题步骤

  1. 状态表示
    这个题和第一题唯一不同就是加入了障碍物(1),我们只需要进行判断,如果该位置是障碍物就填0,否则依据转移方程填表

  2. 状态转移方程
    通过上述分析,可知状态转移方程为:
    dp[i][j] = dp[i - 1][j] + dp[i][j - 1]

  3. 初始化
    和第一题一样,多开一圈,dp[0][1]或dp[1][0]初始为1。

  4. 填表顺序
    和第一题一样,填表顺序为从上往下,每一行从左往右

  5. 返回值
    根据状态表示,返回的应该是dp[m][n],即到达终点的路径数。

  • 代码实现
class Solution {
public:int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {int m = obstacleGrid.size();int n = obstacleGrid[0].size();//dp[i][j]:到达该位置的路径数vector<vector<int>> dp(m + 1,vector<int>(n + 1));dp[0][1] = 1; //dp[1][0] = 1;for(int i = 1; i < m + 1; i++){for(int j = 1; j < n + 1; j++){//多开了一圈,注意下标映射if(obstacleGrid[i - 1][j - 1] == 0){dp[i][j] = dp[i-1][j] + dp[i][j-1];}//vector默认初始0,障碍物对应位置无需处理}}return dp[m][n];//时间复杂度:O(N)//空间复杂度:O(N^2)}
};

3.下降路径最⼩和(中等)

链接:下降路径最⼩和

  • 题目描述
    在这里插入图片描述

  • 做题步骤

  1. 状态表示
    在这里插入图片描述

  2. 状态转移方程
    由前面的分析可知,状态转移方程为:
    dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i - 1][j + 1]}) + matrix[i - 1][j - 1](本身的值)

  3. 初始化
    先全部初始化为极大值,然后第一行初始化为0

  4. 填表顺序
    从上往下,每一行从左往右。

  5. 返回值
    依据状态表示和题目要求,返回最后一行的最小值即可

  • 代码实现
class Solution {
public:int minFallingPathSum(vector<vector<int>>& matrix) {//dp[i][j]表示到这个位置最小路径和int n = matrix.size();vector<vector<int>> dp(n + 1, vector<int>(n + 2, INT_MAX));//初始化第一行for(int j = 0; j < n + 2; j++){dp[0][j] = 0;}for(int i = 1; i < n + 1; i++){for(int j = 1; j < n + 1; j++){dp[i][j] = min({dp[i-1][j-1], dp[i-1][j], dp[i-1][j+1]})+ matrix[i - 1][j - 1]; }}//再遍历一次找最小int ret = dp[n][0];for(auto e : dp[n]){ret = min(ret, e);           }return ret;//时间复杂度:O(N)//空间复杂度:O(N^2)}
};

4.地下城游戏(困难)

链接:地下城游戏

  • 题目描述

在这里插入图片描述

  • 做题步骤
  1. 状态表示

在这里插入图片描述

  1. 状态转移方程
    由前面的分析可知,状态转移方程为:
    dp[i][j] = min(dp[i][j + 1], dp[i + 1][j]) - dungeon[i][j](自身出去的消耗)

  2. 初始化
    先将所有值初始化为极大值,dp[m][n - 1] = dp[m - 1][n] = 1。

  3. 填表顺序
    从下往上,每一行从右向左。

  4. 返回值
    由状态表示可知,返回值为dp[0][0](即[0,0]位置到终点需要的最小生命)

  • 代码实现
// 看点位的状态表示
//从左上到右下:点位表示到这里需要的最小健康点数,点位并不是只受到左边和上面的影响,也要受后面点位的影响(后面点位可能使自己死亡),这种状态表示肯定不能//从右下到左上:点位表示从这里开始到终点所需要的最小健康点数。class Solution {
public:int calculateMinimumHP(vector<vector<int>>& dungeon) {//dp[i][j]表示以这个位置为起点到终点所需要的最小健康点数int m = dungeon.size();int n = dungeon[0].size();vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));//初始化dp[m][n - 1] = dp[m - 1][n] = 1;for(int i = m - 1; i >= 0; i--){for(int j = n - 1; j >=0; j--){dp[i][j] = min(dp[i][j + 1], dp[i + 1][j]) - dungeon[i][j];//如果dp[i][j]为负数,说明这个位置是奶//直接奶满了,从这里到终点所要的点数1就足够(小于1就死了)dp[i][j] = max(dp[i][j], 1);}}return dp[0][0];//时间复杂度:O(N)//空间复杂度:O(N^2)}
};

子数组问题

1.最大子数组和(中等)

链接:最大子数组和

  • 题目描述

在这里插入图片描述

  • 做题步骤
  1. 状态表示
    这个题目我们可以定义状态表示为以i位置为结尾的子数组的最大和
    因为子数组必须是连续的,所以i位置有两种选择:
    (1)接在以i - 1位置为结尾的子数组后面,即dp[i] = dp[i - 1] + 自身点数
    (2)不接在别人后面(可能dp[i - 1]是负值),就自己一个,即dp[i] = 自身点数
    从两种选择中选择最大的一种,即dp[i] = max(dp[i -1] + 自身点数, 自身点数)

  2. 状态转移方程
    由前面的分析可知,状态转移方程为:
    dp[i] = max(dp[i -1] + 自身点数, 自身点数)

  3. 初始化
    无需初始化。

  4. 填表顺序
    从左往右

  5. 返回值
    无法直接确定最大子数组的结尾位置,可以定义变量ret,一边dp一边更新最大值

  • 代码实现
class Solution {
public:int maxSubArray(vector<int>& nums) {//在原数组上面dp就行//dp[i]:以i位置为结尾的最大子数组和int ret = nums[0];for(int i = 1; i < nums.size(); i++){nums[i] = max(nums[i - 1] + nums[i], nums[i]);//遍历的过程顺便找最大ret = max(ret, nums[i]);      }                return ret;//时间复杂度:O(N)//空间复杂度:O(1)}
};

2.环形子数组的最大和(中等)

链接:环形子数组的最大和

  • 题目描述
    在这里插入图片描述

  • 做题步骤

  1. 状态表示
    这个题目和前一题类似,我们依然可以定义状态表示为以i位置为结尾的最大子数组和

    对于环形问题,最常见的做法就是分情况讨论,分解问题:
    (1)最大子数组不成环,比如[-1,2,3,-1],这个情况做法和前一道题一样

    (2)最大子数组成环,比如[2,1,-3,-2,1],最大子数组成环情况,数组中剩余的连续部分一定是最小子数组
    子数组是连续的,环形可以理解为左右扩张,所有有利于自己的连续部分一定会被吞并,剩下的一定是最小子数组和。
    但数组的总和是不变的,我们只需要用总和减去最小子数组和即可得到成环情况的最大和

  2. 状态转移方程
    不成环最大子数组和用f表来记录,最小子数组和用g表来记录
    状态转移方程为:
    f[i] = max(nums[i], f[i - 1] + nums[i])
    g[i] = min(nums[i], g[i - 1] + nums[i])

  3. 初始化
    为避免填表越界,处理第一个位置,f[0] = g[0] = nums[0]

  4. 填表顺序
    都是从左往右

  5. 返回值
    无法直接确定最大(小)子数组的结尾,所以一边dp一边记录最大(小)值
    (1)f表最大值->fmax
    (2)g表最小值->gmin
    (3)总和->sum
    还有一种特殊情况就是环形数组长度为0(数组中全是负数),这个时候最大值为fmax而不是0,所以返回值为sum == gmin ? fmax : max(fmax, sum - gmin)

  • 代码实现
class Solution {
public:int maxSubarraySumCircular(vector<int>& nums) {int n = nums.size();//一种情况是不需要环形,区间就在数组中//第二种情况是需要环形,数组总大小恒定//非目标区间是连续并且在数组中的,所以最大 = 总 - 非目标区间//比如 5 -3 5,第一种得到5,第二种为 7(总) - (-3) = 10int sum = nums[0];//f[i]:以i位置为结尾的不成环最大子数组和vector<int> f(n);//g[i]:以i位置为结尾的最小子数组和auto g = f;f[0] = g[0] = nums[0];int fmax = nums[0];int gmin = nums[0];for(int i = 1; i < n; i++){f[i] = max(nums[i], f[i - 1] + nums[i]);g[i] = min(nums[i], g[i - 1] + nums[i]);fmax = max(fmax, f[i]);gmin = min(gmin, g[i]);sum += nums[i];}//sum和gmin相同说明里面全是负数,这个时候fmax才是最大,不能为0return sum == gmin ? fmax : max(fmax, sum - gmin);//时间复杂度:O(N)//空间复杂度:O(N)}
};

3.乘积最大子数组(中等)

链接:乘积最大子数组

  • 题目描述
    在这里插入图片描述

  • 做题步骤

这个题目子数组长度最小可以为1,其实所有子数组默认乘了一个1

  1. 状态表示
    依据前面最大子数组和的经验,我们可以定义状态为以i位置为结尾的最大乘积

    但只有这一个状态表示是不够的,负数的加入对乘积影响是巨大的,比如[1,2,3,-1,-2,1],前面按最大和的做法来还没问题,但遇到多个负数就会出问题,这里[1,2,3,-1,-2]可以得到最大乘积12,如果按照最大和的做法只能得到6。

    出现上面情况的原因在于负数的出现使得原来的最大乘积变成了最小乘积,但如果保存最小乘积,当遇到负数时最小乘积就可以变成最大乘积

    综上所述,我们需要同时记录最大和最小乘积,其中最大用f表记录,最小用g表记录。
    (1)x = nums[i](当前位置的值)
    (2)y = f[i - 1](前一个位置的最大乘积)
    (3)z = g[i - 1](前一个位置的最小乘积)

  2. 状态转移方程
    一共就三种情况:
    (1)x:不接在别人后面,子数组长度为1。
    (2)x * y:接在前一个位置最大乘积子数组后面,有可能得到最大(小)乘积。
    (3)y * z:接在前一个位置最小乘积子数组后面,有可能得到最大(小)乘积。

    由此可知状态转移方程为:
    f[i] = max( {x, x * y, x * z} )
    g[i] = min( {x, x * y, x * z} )

  3. 初始化
    当前位置的f、g更新需要前一个位置,第一个位置的最大(小)乘积就是值本身,为了避免第一个位置越界,可以在前面多开一个空间并初始化为1,即f[0] = g[0] = 1,这样不会影响第一个位置。

  4. 填表顺序
    从左往右。

  5. 返回值
    无法直接确定最大子数组的结尾位置,一边dp一边更新最大值

  • 代码实现
class Solution {
public:int maxProduct(vector<int>& nums) {int n  = nums.size();//dp[i]:以i为结尾的最大乘积//f[i]表示最大乘积,g[i]表示最小乘积vector<int> f(n + 1);auto g = f;f[0] = g[0] = 1;//ret变量记录最大乘积int ret = INT_MIN;for(int i = 1; i <= n; i++){int x = nums[i - 1];//现在int y = f[i - 1]; //上个位置的最大乘积int z = g[i - 1]; //上个位置的最小乘积//如果遇到负数的情况,原本最大乘积可能会变成最小,原本最小可能会变成最大f[i] = max({x, x * y, x * z});g[i] = min({x, x * y, x * z});ret = max(ret, f[i]);}return ret;//时间复杂度:O(N)//空间复杂度:O(N)}
};

4.乘积为正数的最长子数组(中等)

链接:乘积为正数的最长子数组

  • 题目描述
    在这里插入图片描述

  • 做题步骤

  1. 状态表示
    这个题目和上一道类似,要考虑负数的加入,因此只有一个状态表示是不够的。
    (1)f表:以i位置为结尾,乘积为正数的最长子数组长度。
    (2)g表:以i位置为结尾,乘积为负数的最长子数组长度。

  2. 状态转移方程
    设当前位置的值为x
    (1)x为负数时,接在前一个位置的后面,原本乘积正数的子数组会变成负数,乘积负数的子数组会变成正数。
    即f[i] = g[i - 1] + 1 和 g[i] = f[i - 1] + 1,但前一个位置结尾的子数组乘积可能无法出现负数(前面都是正数),即g[i - 1] == 0,这个时候f[i]应该也为0。
    故状态转移方程为:
    f[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1
    g[i] = f[i - 1] + 1


    (2)当x为正数时,接在前一个位置的后面,原本乘积正数的子数组还是正数,乘积负数的子数组还是负数。
    (也要考虑前一个位置乘积负数不存在的情况
    故状态转移方程为:
    f[i] = f[i - 1] + 1
    g[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1

  3. 初始化
    当前位置的f、g更新需要前一个位置,第一个位置为负数,g[i]为1,f[i]为0,值为正数则相反。为了避免第一个位置越界,可以在前面多开一个空间并初始化为0,即f[0] = g[0] = 0,这样不会影响第一个位置。

  4. 填表顺序
    保证填当前状态时,所需状态已经计算过,填表顺序很明显是从左往右

  5. 返回值
    无法直接确定乘积为正数的最长子数组结尾位置,定义变量ret,一边dp一边更新最大值

  • 代码实现
class Solution {
public:int getMaxLen(vector<int>& nums) {//dp[i] 表示这个位置乘积为负数/正数时的最大长度int n = nums.size();vector<int> f(n + 1); // 正数auto g = f;//负数int ret = -1;for(int i = 1; i < n + 1; i++){//这个数是正数if(nums[i - 1] > 0){f[i] = f[i - 1] + 1;//要考虑前一个位置乘积负数不存在的情况g[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1;                }else if(nums[i - 1] < 0)  //负数{f[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1;g[i] = f[i - 1] + 1;           }//这个数为0,两个状态都不存在,不用处理(本来就是0)ret = max(ret, f[i]);}return ret;//时间复杂度:O(N)//空间复杂度:O(N)}
};

5.等差数列划分(中等)

链接:等差数列划分

  • 题目描述

在这里插入图片描述

  • 做题步骤
  1. 状态表示
    依据前面的经验,我们可以定义状态表示为以i位置为结尾的等差数组个数

  2. 状态转移方程
    以[1,2,3,4]为例子进行分析:
    (1)像1、2这样的位置为结尾数组长度不足3,是一定不能构成等差数组的,即dp[i] = 0。
    (2)像4这样的位置,先看能不能和前面两个元素构成等差数组(满足nums[i] + nums[i - 2] == 2* nums[i - 1]),如果可以的话那4也一定可以接在以3为结尾的等差数组后面,即dp[i] = dp[i - 1] + 1。

    综上所述,状态转移方程为:
    可以和前两个数构成等差数组:dp[i] = dp[i - 1] + 1
    不能和前两个数构造等差数组:dp[i] = 0

  3. 初始化
    无需初始化。

  4. 填表顺序
    保证填当前状态时,所需状态已经计算过,填表顺序很明显是从左往右

  5. 返回值
    题目要求返回所有等差子数组,定义变量sum,一边dp一边累加

  • 代码实现
class Solution {
public:int numberOfArithmeticSlices(vector<int>& nums) {// 1 2 3 4 5//3位置1种   4位置除了拼在3位置可能的后面,还可以抢2 3 来组成//4位置2种   5位置除了拼在4位置可能的后面,还可以抢3 4 来组成//………………对更长的序列也是如此int n = nums.size();//dp[i]:以i位置为结尾的等差数组个数vector<int> dp(n);int sum = 0;for(int i = 2; i < n; i++){//等差数列的性质:num[i] + nums[i - 2] == 2 * num[i - 1]if(nums[i] + nums[i - 2] == 2* nums[i - 1]){dp[i] = dp[i - 1] + 1;}//不满足等差数组为0,vector默认给0,不用处理sum += dp[i];}return sum;//时间复杂度:O(N)//空间复杂度:O(N)}
};

6.最长湍流子数组(中等)

链接:最长湍流子数组

  • 题目描述
    在这里插入图片描述

  • 做题步骤

  1. 状态表示
    依据前面的经验,我们定义状态表示为以i位置为结尾的最长湍流子数组长度

    但只知道i-1位置的最大长度是无法推导出i位置的最大长度的,因为不知道前一个最长湍流子数组结束是处于上升状态(最后的比较符号为’<‘)还是下降状态(最后的比较符号为’>')

    因此可以对状态进行细分:
    (1)f[i]表示以i位置为结尾并处于上升状态(最后的比较符号为’<')的最长湍流子数组长度
    (2)g[i]表示以i位置为结尾并处于下降状态(最后的比较符号为’>')的最长湍流子数组长度

  2. 状态转移方程
    因为湍流子数组比较符号必须在每个相邻元素之间翻转,所以状态转移方程与当前的比较符号相关。
    (1)arr[i-1] < arr[i],现在处于上升状态,需要前置状态处于下降状态的最长湍流子数组长度,即f[i] = g[i - 1] + 1
    (2)arr[i-1] > arr[i],现在处于下降状态,需要前置状态处于上升状态的最长湍流子数组长度,即g[i] = f[i - 1] + 1
    (3)arr[i-1] = arr[i],不能和前面组合,只能自己重新开始,即f[i] = g[i] = 1

  3. 初始化
    推导当前状态需要前一个状态,像第一个位置不能跟在别人后面,两个状态的长度都为1,f[0] = g[0] = 1。因为arr[i-1] = arr[i]时f[i] = g[i] = 1,所以干脆一开始全都初始化为1,就不用单独处理arr[i-1] = arr[i]的情况。

  4. 填表顺序
    保证填当前状态时,所需状态已经计算过,填表顺序很明显是从左往右

  5. 返回值
    无法直接确定最长湍流数组的结尾位置以及结尾是处于上升还是下降状态,所以定义变量ret,一边dp一边更新最大值

  • 代码实现
class Solution {
public:int maxTurbulenceSize(vector<int>& arr) {int n = arr.size();//dp[i]表示以i位置为结尾并且处于上升(下降)状态的最长湍流子数组的长度 vector<int> f(n, 1); //f表示处于上升(<)状态//初始化为1,可以把'=='的情况直接处理了auto g = f; //g表示处于下降(>)状态int ret = 1;for(int i = 1; i < n; i++){if(arr[i-1] < arr[i])f[i] = g[i - 1] + 1;else if(arr[i - 1] > arr[i])g[i] = f[i - 1] + 1;ret = max( {ret, f[i], g[i]} );}return ret;//时间复杂度:O(N)//空间复杂度:O(N)}
};

7.单词拆分(中等)

链接:单词拆分

  • 题目描述

在这里插入图片描述

  • 做题步骤
  1. 状态表示
    依据前面的经验,我们可以定义状态表示为以i位置为结尾的字符串能否由字典中的单词拼出

  2. 状态转移方程
    以s = “leetcode”, wordDict = [“leet”, “code”]为例进行分析:
    (1)先看字符’t’位置(下标3位置),以这个位置为结尾的字符串如果能由字典中的单词拼出,一共有下面几种可能:
    ①"lee"可以由字典中的单词拼出(即dp[2] = true),"t"也可以由字典中的单词拼出。
    ②"le"可以由字典中的单词拼出(即dp[1] = true),"et"也可以由字典中的单词拼出。
    ③"l"可以由单词中的单词拼出(即dp[0] = true),"eet"也可以由字典中的单词拼出。
    ④再往前就没有了,"leet"可以由字典中的单词拼出。

    其它位置的分析也和上述一致,将当前字符串分成[0, j - 1]区间和[j, i]区间,从 0 ~ i 枚举 j ,只要 dp[j - 1] = true并且后面部分的子串 s.substr(j, i - j + 1) 能够在字典中找到,那么 dp[i] = true 。

  3. 初始化
    处理④这样的情况,可以多加一个虚拟节点并初始化true(dp[0] = true),可以理解为空串能在字典中找到。同时为了方便处理下标的映射关系,我们可以在字符串s前面加一个占位符(s = ’ ’ + s),这样就不用考虑下标的映射了。

  4. 填表顺序
    保证填当前状态时,所需状态已经计算过,填表顺序很明显是从左往右

  5. 返回值
    根据状态表示,假设字符串长度为n,返回的应该是dp[n]

  6. 优化
    为了方便查询字符串是否在字典中,可以把字典的单词存储到哈希表中

  • 代码实现
class Solution
{
public:bool wordBreak(string s, vector<string>& wordDict) {// 将字典⾥⾯的单词存在哈希表⾥⾯unordered_set<string> hash;for(auto& s : wordDict) hash.insert(s);int n = s.size();vector<bool> dp(n + 1);dp[0] = true; // 保证后续填表是正确的s = ' ' + s; // 使原始字符串的下标统⼀ for(int i = 1; i <= n; i++) {for(int j = i; j >= 1; j--) //最后⼀个单词的起始位置{if(dp[j - 1] && hash.count(s.substr(j, i - j + 1))){dp[i] = true;break; //已经确定为真就可以跳出这一层循环了}}}return dp[n];//时间复杂度:O(N^2)//空间复杂度:O(N)}
};

8.环绕字符串中唯⼀的子字符串(中等)

链接:环绕字符串中唯⼀的子字符串

  • 题目描述

在这里插入图片描述

  • 做题步骤
  1. 状态表示
    依据前面的经验,我们定义状态表示为以i位置为结尾并且在base中出现的子字符串个数

  2. 状态转移方程
    这个题目中的base数组是按照abcd……zabcd这样的顺序来的,要注意base成环。
    以i位置为结尾并在base中出现的子字符串有下面三种可能:
    (1)不拼在别人后面,就单独自己一个,该字符串一定会在base中出现。
    (2)拼在别人后面,并且满足s[i] = s[i - 1] + 1(即满足abcd递增)
    (3)拼在别人后面,并且满足s[i] == ‘a’ && s[i - 1] == ‘z’(刚好成环)

    综上所述,状态转移方程为:
    ①满足(2)(3)中任意一个,dp[i] = dp[i - 1] + 1(这个1是自己,dp[i - 1]是拼在别人后面)
    ②不满足(2)(3),dp[i] = 1

  3. 初始化
    每个位置最少也有自己单独一个的情况,所以全都初始化为1

  4. 填表顺序
    保证填当前状态时,所需状态已经计算过,填表顺序很明显是从左往右

  5. 返回值
    这个题目最需要注意的就是对dp表数据的处理,因为dp表中可能有大量重复的数据,比如"abcdcd"中’d’字符出现了两次,"cd"和"d"这两个字符串在dp表中是多次记录了的,我们需要对dp表数据进行去重。

    每个字符都对应了固定的ASCLL码,因此可以可以创建⼀个大小为 26 的数组,遍历dp表,对于出现多次的字符,只需保留以该字符为结尾的最大dp值

    去重完成后再进行累加就可以得到结果。

  • 代码实现
class Solution {
public:int findSubstringInWraproundString(string s) {//base是abcd……连续的//s[i]表示现在位置//所以字串要存在要么s[i] == s[i - 1] + 1//要么(s[i - 1] == 'z' && a[i]=='a') int n = s.size();//dp[i]:以i位置为结尾并且在base中出现的子字符串数vector<int> dp(n, 1);for(int i = 1; i < n; i++){if(s[i] == s[i-1] + 1 || (s[i-1] == 'z' && s[i] == 'a')){dp[i] = dp[i - 1] + 1; // 这个1是自己}}int hash[26] = {0};//遍历一次,统计对应字符最大的出现次数//"abcdd"这样的后面那个d的1是无效的,要去掉for(int i = 0; i < n; i++){int index = s[i] - 'a';hash[index] = max(hash[index], dp[i]);}//最后累加int sum = 0;for(auto e : hash){sum += e;}return sum;//时间复杂度:O(N)//空间复杂度:O(N)}
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/98242.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

无涯教程-JavaScript - MINUTE函数

描述 MINUTE函数返回时间值的分钟。分钟以整数形式给出,范围为0到59。 语法 MINUTE (serial_number)争论 Argument描述Required/OptionalSerial_number 您想要找到的包含分钟的时间。 可以输入时间 作为引号内的文本字符串(如" 6:45 PM") 为十进制数字(如0.7812…

JVM垃圾回收机制和常用算法(简洁版)

垃圾收集 (Garbage Collection,GC) 垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的&#xff0c;只存在于线程的生命周期内&#xff0c;线程结束之后就会消失&#xff0c;因此不需要对这三个区域进行垃圾回收。 判断一个对象是…

基本介绍——数据挖掘

1.数据挖掘的定义 数据挖掘是采用数学的、统计的、人工智能和神经网络等领域的科学方法&#xff0c;如记忆推理、聚类分析、关联分析、决策树、神经网络、基因算法等技术&#xff0c;从大量数据中挖掘出隐含的、先前未知的、对决策有潜在价值的关系、模式和趋势&#xff0c;并…

基于Springcloud的基础框架,统一gateWay网关鉴权demo,附下载地址

基于Springcloud的基础框架&#xff0c;统一gateWay网关鉴权demo&#xff0c;附下载地址 使用方式&#xff1a; 1、搭建nacos环境&#xff0c;修改对应nacos地址 2、修改mysql地址,导入sql语句 ###框架内容 SpringcloudGatewayJWTNacosFeginmysqlMybatis plus 具体功能 基于…

Swift 周报 第三十六期

文章目录 前言新闻和社区消息称苹果公司和印度财政部官员磋商&#xff0c;扩大在印度的制造产能iPhone 15 Pro 机型新增泰坦灰iPhone 15 全系配 USB-C 苹果拒绝接口和安卓互通 提案正在审查的提案 Swift论坛推荐博文话题讨论关于我们 前言 本期是 Swift 编辑组整理周报的第三十…

为什么良好的客户服务对于成功的企业至关重要

图片来源于&#xff1a;SaleSmartly官网 良好的客户服务是任何成功企业的重要组成部分。提供卓越的客户服务有助于建立客户的信任和忠诚度&#xff0c;从而增加销售额和利润。它还对任何企业的声誉产生积极影响&#xff0c;带来更多客户和更好的口碑推荐。因此&#xff0c;企业…

说说 TCP的粘包、拆包

分析&回答 拆包和粘包是在socket编程中经常出现的情况&#xff0c; 在socket通讯过程中&#xff0c;如果通讯的一端一次性连续发送多条数据包&#xff0c;tcp协议会将多个数据包打包成一个tcp报文发送出去&#xff0c;这就是所谓的粘包。如果通讯的一端发送的数据包超过一…

OJ题库:计算日期到天数转换、打印从1到最大的n位数 、尼科彻斯定理

前言&#xff1a;在部分大厂笔试时经常会使用OJ题目&#xff0c;这里对《华为机试》和《剑指offer》中的部分题目进行思路分析和讲解&#xff0c;希望对各位读者有所帮助。 题目来自牛客网&#xff0c;欢迎各位积极挑战&#xff1a; HJ73:计算日期到天数转换_牛客网 JZ17:打印…

基于单片机的万年历温度无线传输控制系统系统

一、系统方案 本设计采用DS1302采集年月日时分秒&#xff0c;DS18B20采集温度值&#xff0c;按键设置温度报警上下限&#xff0c;实际测量温度低于下限或高于上限&#xff0c;蜂鸣器报警&#xff0c;同时将测量温度上传到蓝牙助手。 二、硬件设计 原理图如下&#xff1a; 三…

一文彻底吃透自动化测试框架所有知识

定义测试自动化 在任何行业中&#xff0c;自动化通常被解释为自动处理流程&#xff0c;而这些流程几乎不需要人工干预。在软件行业&#xff0c;测试自动化意味着使用许可版本或开源的自动化工具对软件应用程序执行各种测试。用技术术语来说&#xff0c;测试自动化框架是一组定制…

22 元类技术(面向切片编程)|ORM的实现|抽象类与接口类

文章目录 前情知识补充hasattr 函数setattr函数getattr函数join 函数 元类技术使用type创建类什么是元类&#xff08;概念总结&#xff09;\_\_metaclass\_\_属性使用metaclass 的函数方式进行创建类使用metaclass 的类方式进行创建类 自定义元类 元类实现ORM接口类与抽象类抽象…

初识c++

文章目录 前言一、C命名空间1、命名空间2、命名空间定义 二、第一个c程序1、c的hello world2、std命名空间的使用惯例 三、C输入&输出1、c输入&输出 四、c中缺省参数1、缺省参数概念2、缺省参数分类3、缺省参数应用 五、c中函数重载1、函数重载概念2、函数重载应用 六、…