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

news2024/10/6 20:38:19

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

    • 路径问题
      • 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) = 10
        int 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才是最大,不能为0
        return 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.coloradmin.cn/o/974118.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java】关于JDK 8的HashMap

文章目录 HashMap 简介数据结构Hash构造方法get(key)方法步骤一&#xff1a;通过key获取所在桶的第一个元素是否存在步骤二:该节点的hash和key是否与要查询的hash和key匹配步骤三:当对应桶中不止一个节点时&#xff0c;根据不同节点类型查询 put(key,value)为什么树化&#xff…

4.正则提取html中的img标签的src内容

我们以百度贴吧的1吧举例 目录 1 把网页搞下来 2 收集url 3 处理url 4 空的src 5 容错 6 不使用数字作为文件名 7 并不是所有的图片都用img标签表示 8 img标签中src请求下来不一定正确 9 分页 1 把网页搞下来 搞下来之后&#xff0c;双击打开是这样的 2 收…

leetcode 143. 重排链表

2023.9.5 先将链表中的节点存储到数组中&#xff0c;再利用双指针重新构造符合条件的链表。代码如下&#xff1a; /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNod…

java八股文面试[数据库]——Page页的结构

mysql中数据是存储在物理磁盘上的&#xff0c;而真正的数据处理又是在内存中执行的。由于磁盘的读写速度非常慢&#xff0c;如果每次操作都对磁盘进行频繁读写的话&#xff0c;那么性能一定非常差。为了上述问题&#xff0c;InnoDB将数据划分为若干页&#xff0c;以页作为磁盘与…

了解下iVX,它可能会刷新你对传统软件开发的认知!

知识目录 前言一、聊聊传统编程语言二、iVX的诞生三、iVX VS 传统编程语言3.1 图形化 vs 文本化3.2 逻辑与语法的解耦3.3 组件与库3.4 编译与代码生成3.5 IDE与语言设计的整合3.6 面向群体3.7 灵活性与便利性 四、iVX提供多样模板&#xff0c;快来 ~五、iVX VS 其他低代码平台结…

面试中的自我介绍:首印象决定一切

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

MES系统来料检验:优化制造业质量控制

一、MES系统来料检验的定义&#xff1a; MES系统来料检验是指制造执行系统中的质量管理模块&#xff0c;用于对供应商提供的原材料和零部件进行质量检查和验证。该模块涵盖了来料质量数据的采集、分析、报告和追溯等功能&#xff0c;以确保来料符合质量要求&#xff0c;并提供…

【web知识清单】你想要的都有:网络、HTTP、会话保持、认证授权......持续更新中

作者简介&#xff1a; 目录 1.网络 2.HTTP 2.1.报文结构 2.1.1.请求报文 2.1.2.响应报文 2.2.方法 2.3.HTTPS 2.4.跨域 3.会话保持 3.1.概述 3.2.cookie 3.3.session 4.认证授权 4.1.Token 4.2.JWT 4.3.oauth 1.网络 计算机网络&#xff1a; 计算机网络&…

Lesson4-1:OpenCV图像特征提取与描述---角点特征

学习目标 理解图像的特征知道图像的角点 1 图像的特征 大多数人都玩过拼图游戏。首先拿到完整图像的碎片&#xff0c;然后把这些碎片以正确的方式排列起来从而重建这幅图像。如果把拼图游戏的原理写成计算机程序&#xff0c;那计算机就也会玩拼图游戏了。 在拼图时&#xff…

Problem: 205. 同构字符串;力扣;python

题目描述 代码展示 # class Solution: # def isIsomorphic(self, s: str, t: str) -> bool: # dict {} # flag 0 # for i in range(len(s)): # #记录&#xff0c;s # if s[i] not in dict.keys(): # …

【真题解析】系统集成项目管理工程师 2023 年上半年真题卷(综合知识)

本文为系统集成项目管理工程师考试(软考) 2023 年上半年真题(全国卷),包含答案与详细解析。考试共分为两科,成绩均 ≥45 即可通过考试: 综合知识(选择题 75 道,75分)案例分析(问答题 4 道,75分)综合知识(选择题*75)1-10 题11-20 题21-30 题31-40 题41-50 题51-60 …

让GPT成为您的科研加速器丨GPT引领前沿与应用突破之GPT4科研实践技术与AI绘图

GPT对于每个科研人员已经成为不可或缺的辅助工具&#xff0c;不同的研究领域和项目具有不同的需求。如在科研编程、绘图领域&#xff1a;1、编程建议和示例代码:无论你使用的编程语言是Python、R、MATLAB还是其他语言&#xff0c;都可以为你提供相关的代码示例。​2、数据可视化…

vs code调试rust乱码问题解决方案

在terminal中 用chcp 65001 修改一下字符集&#xff0c;就行了。有的博主推荐 修改 区域中的设置&#xff0c;这会引来很大的问题。千万不要修改如下设置&#xff1a;

宁德时代龙头,比亚迪榜眼:1-7 月全球电动汽车电池市场份额公布

根据SNE Search公布的数据显示&#xff0c;今年1-7月全球电动汽车电池的消费总量为362.9 千兆瓦时&#xff08;GWh&#xff09;&#xff0c;比去年同期的243.2 千兆瓦时&#xff08;GWh&#xff09;增长了49.2%。这表明电动汽车市场的增长势头依然强劲。 在这些数据中&#xff…

QuantLib学习笔记——InterestRate的应用

⭐️ 单利还是复利 巴菲特老爷子有句名言&#xff1a;“人生就像滚雪球&#xff0c;重要的是发现很湿的雪和很长的坡。” 很湿的雪&#xff0c;指的就是复利。很长的坡&#xff0c;指的就是时间。很湿的雪和很长的坡组合起来&#xff0c;就能滚成巨大的雪球。 哈哈&#xff0…

南京市玄武区委常委、组织部部长任宁一行率企业家研修班莅临麒麟信安调研交流

为学习在先进科学技术、人才引育等方面的领先经验&#xff0c;进一步加强交流合作&#xff0c;9月4日上午&#xff0c;南京市玄武区委常委、组织部部长任宁&#xff0c;玄武区委组织部副部长、两新工委书记鲁琳&#xff0c;玄武区委组织部人才综合服务科科长韩静玮等领导一行带…

Apache NIFI将Json数据转为SQL语句并插入到数据库表中

说明 本文中的NIFI是使用docker进行安装的&#xff0c;所有的配置参考&#xff1a;docker安装Apache NIFI 需求背景 现在有一个文件&#xff0c;里面存储的是一些json格式的数据&#xff0c;要求将文件中的数据存入数据库表中&#xff0c;以下是一些模拟的数据和对应的数据库…

Linux_VMware_虚拟机磁盘扩容

来源文章 &#xff1a;VMware教学-虚拟机扩容篇_vmware虚拟机扩容_系统免驱动的博客-CSDN博客 由于项目逐步的完善&#xff0c;需要搭建的中间件&#xff0c;软件越来越多&#xff0c;导致以前虚拟机配置20G的内存不够用了&#xff0c;又不想重新创建新的虚拟机&#xff0c;退…

数据脱敏sensitive(前端或数据库加密,解密)

可以对数据加密&#xff0c;解密&#xff0c;对数据库加密的数据进行解密显示&#xff0c;对数据库没有加密的数据进行加密处理展示前端等待 1&#xff1a;引入数据如下结构 1-1&#xff1a;SensitiveDecode脱敏解密注解 package com.example.poi.desensitization.annotation;…

【python基础知识】3.input()函数

文章目录 前言input()函数input()函数的使用input()函数结果的赋值input()函数的数据类型input()函数结果的强制转换input()函数知识点总结 综合复习 前言 在前面的学习中&#xff0c;我们学会了用print()函数对计算机下简单的命令&#xff0c;开始接触Python里不同类型的数据…