【动态规划专栏】--简单-- 动态规划经典题型

news2024/11/26 12:48:51

目录

动态规划

动态规划思维(基础)

状态表示(最重要)

状态转移方程(最难)

初始化(细节)

填表顺序(细节)

返回值(结果)

解码方法⭐⭐

【题目解析】 

 【算法原理】

C++ 算法代码

复杂度分析

【空间优化 - 滚动数组】

C++ 算法代码

复杂度分析

【DP边界、初始化技巧】

C++ 算法代码

【空间优化 - 滚动数组】

C++ 算法代码

不同路径⭐⭐

【题目解析】 

【算法原理】

C++ 算法代码 

复杂度分析

【DP边界、初始化】

C++ 算法代码

【空间优化 - 滚动数组】

C++ 算法代码

复杂度分析

不同路径Ⅱ⭐⭐

【算法原理】

C++ 算法代码 

复杂度分析

【DP边界、初始化】

【空间优化 - 滚动数组】

C++ 算法代码

复杂度分析

礼物的最大价值⭐⭐ 

【算法原理】

C++ 算法代码 

复杂度分析

【DP边界、初始化】

【空间优化 - 滚动数组】

C++ 算法代码

复杂度分析

下降路径最小和⭐⭐

【算法原理】

C++ 算法代码 

复杂度分析

【空间优化 - 滚动数组】

C++ 算法代码

复杂度分析

最小路径和⭐⭐

【算法原理】

C++ 算法代码

复杂度分析

【DP边界、初始化】

【空间优化 - 滚动数组】

C++ 算法代码

复杂度分析


动态规划

动态规划思维(基础)

        动态规划一般会先定义一个dp表,dp表一般为一维数组 / 二位数组。如:一维数组,会先创建一个一维数组(dp表),接下来就是想办法将这个dp填满,而填满之后里面的某一个值就是最终结果。

状态表示(最重要)

#问:是什么?

  • 就是dp[i]所代表的含义。

#问:怎么来?

  • 题目要求。
  • 经验 + 题目要求。
  • 分析问题的过程中,发现重复子问题。

状态转移方程(最难)

#问:是什么?

  • dp[i] = ?。

初始化(细节)

#问:有什么作用?

  • 保证填表的时候不越界。

        dp表是根据状态转移方程进行的,而状态转移方程是通过已有状态推出未知状态。

填表顺序(细节)

#问:有什么作用?

  • 为了填写当前状态的时候,所需要的状态已经计算过了。

返回值(结果)

        题目要求 + 状态表示。


解码方法⭐⭐

91. 解码方法 - 力扣(LeetCode)


【题目解析】 

        dp[i] 表示:字符串中 [0,i] 区间上,⼀共有多少种编码方法。

 【算法原理】

#:状态表示:

        根据以往的经验,对于大多数线性 dp ,我们经验上都是「以某个位置结束或者开始」做⽂章,这里我们继续尝试「用 i 位置为结尾」结合「题目要求」来定义状态表示。dp[i] 表示:字符串中 [0 i] 区间上,⼀共有多少种编码方法。

#:状态转移方程:

        定义好状态表示,我们就可以分析 i 位置的 dp 值,如何由「前面」或者「后面」的信息推导出来。 关于 i 位置的编码状况,我们可以分为下⾯两种情况:
  1. 让 i 位置上的数单独解码成一个字母,成功:dp[i] += dp[i - 1],失败:dp[i] += 0
  2. 让 i 位置上的数与 i - 1 位置上的数结合,解码成一个字母,成功:dp[i] += dp[i - 2],失败:dp[i] += 0

#:初始化:

         从我们的递推公式可以看出, dp[i] 在 i = 0 以及 i = 1 的时候是没有办法进行推导的,因为 dp[-2] 或 dp[-1] 不是⼀个有效的数据。 因此我们需要在填表之前,将 0, 1 位置的值初始化。

#:填表顺序:

        毫⽆疑问是「从左往右」

#:返回值:

        应该返回 dp[n - 1] 的值,表示在 [0, n - 1] 区间上的编码方法。


C++ 算法代码

        使用⼀维数组:

class Solution {
public:
    int numDecodings(string s) {
        // 处理边界情况
        if(s[0] == '0' || s.empty()) return 0;
        if(s.size() == 1) return 1;

        // 1、创建dp表
        vector<int> dp(s.size(), 0);

        // 2、初始化
        dp[0] = 1;
        if(s[1] == '0' && s.substr(0, 2) > "26") dp[1] = 0;
        else if(s[1] == '0' && s.substr(0, 2) <= "26") dp[1] = 1;
        else if(s.substr(0, 2) > "26") dp[1] = 1;
        else dp[1] = 2;


        // 3、填表
        for(int i = 2; i < s.size(); i++)
        {
            if(s[i] != '0')
                dp[i] += dp[i - 1];
            
            // 计算两位组成是否合格
            if(s[i - 1] != '0' && s.substr(i - 1, 2) <= "26")
                dp[i] += dp[i - 2];
        }

        // 4、返回值
        return dp[s.size() - 1];
    }
};

复杂度分析

  • 时间复杂度:O(n),一层for循环。
  • 空间复杂度:O(n)

【空间优化 - 滚动数组】

        通过上述图可以发现。

        dp[i]是只与前两个元素相关,所以我们可以将代码写为如下: 

C++ 算法代码

class Solution {
public:
    int numDecodings(string s) {
        // 处理边界情况
        if(s[0] == '0' || s.empty()) return 0;
        if(s.size() == 1) return 1;

        // 1、创建dp表
        vector<int> dp(2, 0);

        // 2、初始化
        dp[0] = 1;
        if(s[1] == '0' && s.substr(0, 2) > "26") dp[1] = 0;
        else if(s[1] == '0' && s.substr(0, 2) <= "26") dp[1] = 1;
        else if(s.substr(0, 2) > "26") dp[1] = 1;
        else dp[1] = 2;

        // 3、填表
        for(int i = 2; i < s.size(); i++)
        {
            int tmp = 0;
            if(s[i] != '0')
                tmp += dp[1];
            
            // 计算两位组成是否合格
            if(s[i - 1] != '0' && s.substr(i - 1, 2) <= "26")
                tmp += dp[0];
            dp[0] = dp[1], dp[1] = tmp;
        }

        // 4、返回值
        return dp[1];
    }
};

复杂度分析

  • 时间复杂度:O(n),一层for循环。
  • 空间复杂度:O(1)

【DP边界、初始化技巧】

        在上述的代码中可以发现,代码的初始化写的好长,并且尤其是对于dp[1]的初始化和后续的dp[i]填表的相似度非常的高。那岂不是将初始化dp[1]部分放到填表的环节里更好!

        也就是在DP中经常会出现处理,边界复杂、繁琐的情况,而为了能更好的处理这种类似的情况,是由一个技巧的:就是将整个dp表统一向后移动一位,也就是多开一个位置

        虚拟位置的作用。

        越过令人讨厌的dp[1],让其在填表里面去搞定。这个看似是很舒服的,但是我们需要注意两个注意事项:

  1. 虚拟节点里面的值,要保证后面的填表的值是正确的。
  2. 下标的映射关系。

        分析上述,根据填表的规则,并且由于原dp[1]变为现在的dp[2],所以是dp[2] = dp[0] + dp[1]。而dp[1]完完全全就是我们初始化的,所以不会出错,于是就看dp[0]。而dp[0]是我们进行虚拟出来的,于是其的值是至关重要的。

        一般情况下新增的虚拟位置都是0,但是此处不一样。此处如果dp[1] + dp[2]位置的值符合解码为字母,那么就需要加上dp[0]的值,所以证明是找到了一种解码方式,那么就是1,于是dp[0] = 1。

C++ 算法代码

class Solution {
public:
    int numDecodings(string s) {
        // 处理边界情况
        if(s[0] == '0' || s.empty()) return 0;
        if(s.size() == 1) return 1;

        // 1、创建dp表
        vector<int> dp(s.size() + 1, 0);

        // 2、初始化
        dp[0] = 1, dp[1] = 1;

        // 3、填表
        for(int i = 1; i < s.size(); i++)
        {
            if(s[i] != '0')
                dp[i + 1] += dp[i];
            
            // 计算两位组成是否合格
            if(s[i - 1] != '0' && s.substr(i - 1, 2) <= "26")
                dp[i + 1] += dp[i - 1];
        }

        // 4、返回值
        return dp[s.size()];
    }
};

【空间优化 - 滚动数组】

C++ 算法代码

class Solution {
public:
    int numDecodings(string s) {
        if(s[0] == '0' || s.empty()) return 0;
        if(s.size() == 1) return 1;

        // 1、创建dp表
        vector<int> dp(2, 0);

        // 2、初始化
        dp[0] = 1, dp[1] = 1;

        // 3、填表
        for(int i = 1; i < s.size(); i++)
        {
            int tmp = 0;
            if(s[i] != '0')
                tmp += dp[1];
            
            // 计算两位组成是否合格
            if(s[i - 1] != '0' && s.substr(i - 1, 2) <= "26")
                tmp += dp[0];
            dp[0] = dp[1], dp[1] = tmp;
        }

        // 4、返回值
        return dp[1];
    }
};

不同路径⭐⭐

​​​​​​62. 不同路径 - 力扣(LeetCode)


【题目解析】 

        dp[i][j]表示:到达 [i, j] 位置的路径个数。

【算法原理】

#:状态表示:

对于这种「路径类」的问题,我们的状态表示⼀般有两种形式:
  1. 从 [i, j] 位置出发,……;
  2. 从起始位置出发,到达 [i, j] 位置,……。
        这里选择第二种定义状态表示的方式: dp[ i ][ j ] 表示:走到 [ i, j ] 位置处,⼀共有多少种方式。

#:状态转移方程:

        如果 dp[ i ][ j ] 表示 到达 [ i, j ] 位置的方法数,那么到达 [ i, j ] 位置之前的⼀小步,有两种情况:
  1. 从 [ i, j ] 位置的上方( [ i - 1, j ] 的位置)向下走⼀步,转移到 [ i, j ] 位置
  2. 从 [ i, j ] 位置的左方( [ i, j - 1 ] 的位置)向右走⼀步,转移到 [ i, j ] 位置
        由于我们要求的是有多少种方法,因此状态转移方程就呼之欲出了: dp[ i ][ j ] = dp[ i - 1 ][ j ] + dp[ i ][ j - 1 ]。

#:初始化:

        对第一行,第一列继续初始化,由于 dp[ 0 ][ j ] 只能由前所来,dp[ i ][ 0 ] 只能由上所来。

#:填表顺序:

        根据「状态转移方程」的推导来看,填表的顺序就是「从上往下」填每⼀行,在填写每⼀行的时候「从左往右」。

#:返回值:

        根据「状态表示」,我们要返回 dp[m - 1][n - 1] 的值。


C++ 算法代码 

class Solution {
public:
    int uniquePaths(int m, int n) {
        // 处理边界情况
        if(m == 1 && n == 1) return 1;

        // 1、创建dp表
        vector<vector<int>> dp(m, vector(n, 0));

        // 2、初始化
        for(auto& v : dp[0]) v = 1;
        for(int i = 0; i < m; i++) dp[i][0] = 1;

        // 3、填表
        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];
            }
        }
        
        // 4、返回值
        return dp[m -1][n - 1];
    }
};

复杂度分析

  • 时间复杂度:O(n*m),两层for循环。
  • 空间复杂度:O(n*m)

【DP边界、初始化】

可以在最前⾯加上⼀个「辅助结点」,帮助我们初始化。使用这种技巧要注意两个点:

  1. 辅助结点里面的值要「保证后续填表是正确的」。
  2. 「下标的映射关系」。

        在上述的代码中可以发现,代码的初始化需要写两个for循环,无疑也是麻烦的,所以采用DP边界、初始化技巧无疑是很好的。通过上述分析可以发现,由于第一行与第一列,在状态转移方程中 [i - 1]、[j - 1]的越界而导致无法进入填表环节 —— 所以采取多一行,多一列。

        将上述的两个for循环初始化值引入:

        而这个初始化的原因就是第一行只能从左往右,第一列只能从上往下 —— 所以重点是有效范围内的第一个元素为1。

        在本题中,添加一行,并且添加⼀列后,只需将 dp[1][0] 的位置初始化为 1 或 将 dp[0][1] 的位置初始化为 1 即可。

C++ 算法代码

class Solution {
public:
    int uniquePaths(int m, int n) {
        // 处理边界情况
        if(m == 1 && n == 1) return 1;

        // 1、创建dp表
        vector<vector<int>> dp(m + 1, vector(n + 1, 0));

        // 2、初始化
        dp[0][1] = 1;
        // dp[1][0] = 1;

        // 3、填表
        for(int i = 1; i < m + 1; i++)
        {
            for(int j = 1; j < n + 1; j++)
            {
                dp[i][j] += dp[i][j - 1] + dp[i - 1][j];
            }
        }
        // 4、返回值
        return dp[m][n];
    }
};

【空间优化 - 滚动数组】

C++ 算法代码

class Solution {
public:
    int uniquePaths(int m, int n) {
        // 处理边界情况
        if(m == 1 && n == 1) return 1;

        // 1、创建dp表
        vector<int> dp(n + 1, 0);

        // 2、初始化
        dp[1] = 1;

        // 3、填表
        for(int i = 1; i < m + 1; i++)
        {
            for(int j = 0; j < n + 1; j++)
            {
                if(j != 0)
                {
                    dp[j] += dp[j - 1];
                }
            }
        }
        
        // 4、返回值
        return dp[n];
    }
};

复杂度分析

  • 时间复杂度:O(n*m),两层for循环。
  • 空间复杂度:O(n)

不同路径Ⅱ⭐⭐

63. 不同路径 II - 力扣(LeetCode)

【算法原理】

#:状态表示:

对于这种「路径类」的问题,我们的状态表示⼀般有两种形式:
  1. 从 [i, j] 位置出发,……;
  2. 从起始位置出发,到达 [i, j] 位置,……。
        这里选择第二种定义状态表示的方式: dp[ i ][ j ] 表示:走到 [ i, j ] 位置处,⼀共有多少种方式。

#:状态转移方程:

        如果 dp[ i ][ j ] 表示 到达 [ i, j ] 位置的方法数,那么到达 [ i, j ] 位置之前的⼀小步,有两种情况:
  1. 从 [ i, j ] 位置的上方( [ i - 1, j ] 的位置)向下走⼀步,转移到 [ i, j ] 位置
  2. 从 [ i, j ] 位置的左方( [ i, j - 1 ] 的位置)向右走⼀步,转移到 [ i, j ] 位置
        但是, [i - 1, j] [i, j - 1] 位置都是可能有障碍的,此时从上⾯或者左边是不可能 到达 [i, j] 位置的,也就是说,此时的方法数应该是 0。
        由此我们可以得出⼀个结论,只要这个位置上「有障碍物」,那么我们就不需要计算这个位置上的值,直接让它等于 0 即可。

#:初始化:

        对第一行,第一列继续初始化,由于 dp[ 0 ][ j ] 只能由前所来,dp[ i ][ 0 ] 只能由上所来,并且需要注意的是小心一行 / 一列中出现阻碍,如果有阻碍就会将为唯一的路线卡死。 

#:填表顺序:

        根据「状态转移方程」的推导来看,填表的顺序就是「从上往下」填每⼀行,在填写每⼀行的时候「从左往右」。

#:返回值:

        根据「状态表示」,我们要返回 dp[m - 1][n - 1] 的值。


C++ 算法代码 

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int row = obstacleGrid.size();
        int col = obstacleGrid[0].size();

        // 处理边界情况
        if(row == 1 && col == 1 && obstacleGrid[0][0] == 0) return 1;
        if(row == 1 && col == 1 && obstacleGrid[0][0] == 1) return 0;

        // 1、创建dp表
        vector<vector<int>> dp(row, vector<int>(col, 0));

        // 2、初始化
        int flag = 1;
        for(int i = 0; i<row; i++)
        {
            if(obstacleGrid[i][0] == 1) flag = 0;
            dp[i][0] = flag;
        }
        flag = 1;
        for(int i = 0; i<col; i++)
        {
            if(obstacleGrid[0][i] == 1) flag = 0;
            dp[0][i] = flag;
        }

        // 3、填表
        for(int i = 1; i<row; i++)
        {
            for(int j = 1; j<col; j++)
            {
                if(obstacleGrid[i][j] == 1) continue;
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }

        // 4、返回值
        return dp[row - 1][col - 1];
    }
};

复杂度分析

  • 时间复杂度:O(n*m),两层for循环。
  • 空间复杂度:O(n*m)

【DP边界、初始化】

        与上一题类似的思想。

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int row = obstacleGrid.size();
        int col = obstacleGrid[0].size();

        // 处理边界情况
        if(row == 1 && col == 1 && obstacleGrid[0][0] == 0) return 1;
        if(row == 1 && col == 1 && obstacleGrid[0][0] == 1) return 0;

        // 1、创建dp表
        vector<vector<int>> dp(row + 1, vector<int>(col + 1, 0));

        // 2、初始化
        dp[0][1] = 1;

        // 3、填表
        for(int i = 1; i < row + 1; i++)
        {
            for(int j = 1; j < col + 1; j++)
            {
                if(obstacleGrid[i - 1][j - 1] == 1) continue;
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }

        // 4、返回值
        return dp[row][col];
    }
};

【空间优化 - 滚动数组】

C++ 算法代码

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int row = obstacleGrid.size();
        int col = obstacleGrid[0].size();

        // 处理边界情况
        if(row == 1 && col == 1 && obstacleGrid[0][0] == 0) return 1;
        if(row == 1 && col == 1 && obstacleGrid[0][0] == 1) return 0;

        // 1、创建dp表
        vector<int> dp(col + 1, 0);

        // 2、初始化
        dp[1] = 1;

        // 3、填表
        for(int i = 1; i < row + 1; i++)
        {
            for(int j = 1; j < col + 1; j++)
            {
                if(obstacleGrid[i - 1][j - 1] == 1) 
                    dp[j] = 0;
                else
                    if(j != 0)
                        dp[j] = dp[j - 1] + dp[j];
            }
        }

        // 4、返回值
        return dp[col];
    }
};

复杂度分析

  • 时间复杂度:O(n*m),两层for循环。
  • 空间复杂度:O(n)

礼物的最大价值⭐⭐ 

剑指 Offer 47. 礼物的最大价值 - 力扣(LeetCode)

【算法原理】

#:状态表示:

对于这种「路径类」的问题,我们的状态表示⼀般有两种形式:
  1. 从 [i, j] 位置出发,……;
  2. 从起始位置出发,到达 [i, j] 位置,……。
        这里选择第二种定义状态表示的方式: dp[ i ][ j ] 表示:走到 [ i, j ] 位置处,此时的最大价值。

#:状态转移方程:

        如果 dp[ i ][ j ] 表示 到达 [ i, j ] 位置的方法数,那么到达 [ i, j ] 位置之前的⼀小步,有两种情况:
  1. 从 [ i, j ] 位置的上方( [ i - 1, j ] 的位置)向下走一步,此时到达 [i, j] 位置能拿到的礼物价值为 dp[ i - 1 ][ j ] + grid[ i ][ j ]
  2. 从 [ i, j ] 位置的左方( [ i, j - 1 ] 的位置)向右走一步,此时到达 [i, j] 位置能拿到的礼物价值为 dp[ i ][ j - 1 ] + grid[ i ][ j ]
        我们要的是最大值,因此 状态转移方程为:dp[ i ][ j ] = max(dp[ i - 1 ][ j ], dp[ i ][ j - 1 ]) + grid[ i ][ j ]

#:初始化:

        对第一行,第一列继续初始化,由于 dp[ 0 ][ j ] 只能由前所来,dp[ i ][ 0 ] 只能由上所来。

#:填表顺序:

        根据「状态转移方程」的推导来看,填表的顺序就是「从上往下」填每⼀行,在填写每⼀行的时候「从左往右」。

#:返回值:

        根据「状态表示」,我们要返回 dp[m - 1][n - 1] 的值。


C++ 算法代码 

class Solution {
public:
    int maxValue(vector<vector<int>>& grid) {
        int row = grid.size();
        int col = grid[0].size();

        // 1、创建dp表
        vector<vector<int>> dp(row, vector<int>(col, 0));
        
        // 2、初始化
        dp[0][0] = grid[0][0];
        for(int i = 1; i<col; i++)
            dp[0][i] = dp[0][i - 1] + grid[0][i];
        
        for(int i = 1; i<row; i++)
            dp[i][0] = dp[i - 1][0] + grid[i][0];

        // 3、填表
        for(int i = 1; i<row; i++)
            for(int j = 1; j<col; j++)
                dp[i][j] = grid[i][j] + max(dp[i - 1][j], dp[i][j - 1]);

        // 4、返回值
        return dp[row - 1][col - 1];
    }
};

复杂度分析

  • 时间复杂度:O(n*m),两层for循环。
  • 空间复杂度:O(n*m)

【DP边界、初始化】

class Solution {
public:
    int maxValue(vector<vector<int>>& grid) {
        int row = grid.size();
        int col = grid[0].size();

        // 1、创建dp表
        vector<vector<int>> dp(row + 1, vector<int>(col + 1, 0));
        
        // 2、初始化 - 就是0,创建时已初始化

        // 3、填表
        for(int i = 1; i < row + 1; i++)
            for(int j = 1; j < col + 1; j++)
                dp[i][j] = grid[i - 1][j - 1] + max(dp[i - 1][j], dp[i][j - 1]);

        // 4、返回值
        return dp[row][col];
    }
};

【空间优化 - 滚动数组】

C++ 算法代码

class Solution {
public:
    int maxValue(vector<vector<int>>& grid) {
        int row = grid.size();
        int col = grid[0].size();

        // 1、创建dp表
        vector<int> dp(col + 1, 0);
        
        // 2、初始化 - 就是0,创建时已初始化

        // 3、填表
        for(int i = 1; i < row + 1; i++)
        {
            for(int j = 1; j < col + 1; j++)
            {
                dp[j] = grid[i - 1][j - 1] + max(dp[j], dp[j - 1]);  
            }
        }

        // 4、返回值
        return dp[col];
    }
};

复杂度分析

  • 时间复杂度:O(n*m),两层for循环。
  • 空间复杂度:O(n)

下降路径最小和⭐⭐

931. 下降路径最小和 - 力扣(LeetCode)

【算法原理】

#:状态表示:

对于这种「路径类」的问题,我们的状态表示⼀般有两种形式:
  1. 从 [i, j] 位置出发,……;
  2. 从起始位置出发,到达 [i, j] 位置,……。
        这里选择第二种定义状态表示的方式: dp[ i ][ j ] 表示:走到 [ i, j ] 位置处,所有下降路径中的最小和。

#:状态转移方程:

对于普遍位置 [i, j] ,根据题意得,到达 [i, j] 位置可能有三种情况:
  1. 从正上方 [i - 1, j] 位置转移到 [i, j] 位置。
  2. 从左上方 [i - 1, j - 1] 位置转移到 [i, j] 位置。
  3. 从右上⽅ [i - 1, j + 1] 位置转移到 [i, j] 位置。
        我们要的是三种情况下的「最小值」,然后再加上矩阵在 [i, j] 位置的值,于是 状态转移方程为:dp[i][j] = min(dp[i - 1][ j ], min(dp[i - 1][j - 1], dp[i - 1][j + 1])) + matrix[ i ][ j ]

#:初始化:

可以在最前⾯加上⼀个「辅助结点」,帮助我们初始化。使⽤这种技巧要注意两个点:
  1. 辅助结点里面的值要「保证后续填表是正确的」。
  2. 「下标的映射关系」。
        在本题中,需要「加上⼀行」,并且「加上两列」。所有的位置都初始化为无穷大,然后将第⼀行初始化为 0 即可。

#:填表顺序:

        根据「状态表示」,填表的顺序是「从上往下」。

#:返回值:

        注意:这里不是返回 dp[m][n] 的值!题目要求「只要到达最后一行」就行了,因此这里应该返回「 dp 表中最后一行的最小值」。


C++ 算法代码 

class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& matrix) {
        int row = matrix.size();
        int col = matrix[0].size();

        // 1、创建dp表
        vector<vector<int>> dp(row + 1, vector<int>(col + 2, INT_MAX));

        // 2、初始化
        for(int i = 0; i < col + 2; i++)
            dp[0][i] = 0;

        // 3、填表
        for(int i = 1; i < row + 1; i++)
        {
            for(int j = 1; j < col + 1; j++)
            {
                dp[i][j] = matrix[i - 1][j - 1] + min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i - 1][j + 1]));
            }
        }

        return *min_element(dp[row].begin(), dp[row].end());
    }
};

复杂度分析

  • 时间复杂度:O(n*m),两层for循环。
  • 空间复杂度:O(n*m)

【空间优化 - 滚动数组】

C++ 算法代码

class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& matrix) {
        int row = matrix.size();
        int col = matrix[0].size();

        // 1、创建dp表
        vector<vector<int>> dp(2, vector<int>(col + 2, INT_MAX));

        // 2、初始化
        for(int i = 1; i < col + 1; i++)
            dp[0][i] = 0;

        // 3、填表
        for(int i = 1; i < row + 1; i++)
        {
            for(int j = 1; j < col + 1; j++)
            {
                int tmp_i = i % 2;
                int before_i = (i + 1) % 2;
                dp[tmp_i][j] = matrix[i - 1][j - 1] + min(dp[before_i][j - 1], min(dp[before_i][j], dp[before_i][j + 1]));
            }
        }

        // 4、返回值
        return *min_element(dp[row % 2].begin(), dp[row % 2].end());
    }
};

复杂度分析

  • 时间复杂度:O(n*m),两层for循环。
  • 空间复杂度:O(n)

最小路径和⭐⭐

64. 最小路径和 - 力扣(LeetCode)


【算法原理】

#:状态表示:

对于这种「路径类」的问题,我们的状态表示⼀般有两种形式:
  1. 从 [i, j] 位置出发,……;
  2. 从起始位置出发,到达 [i, j] 位置,……。
        这里选择第二种定义状态表示的方式: dp[ i ][ j ] 表示:走到 [ i, j ] 位置处,最小路径和是多少。

#:状态转移方程:

        如果 dp[ i ][ j ] 表示 到达 [ i, j ] 位置的方法数,那么到达 [ i, j ] 位置之前的⼀小步,有两种情况:
  1. 从 [i - 1, j] 向下走一步,转移到 [i, j] 位置
  2. 从 [i, j - 1] 向右走一步,转移到 [i, j] 位置
        由于到 [ i, j ] 位置两种情况,并且我们要找的是最小路径,因此只需要这两种情况下的最小值,再加上 [ i, j ] 位置上本⾝的值即可。
         也就是状态转移方程为:dp[ i ][ j ] = min(dp[ i - 1 ][ j ], dp[ i ][ j - 1 ]) + grid[ i ][ j ]

#:初始化:

        对第一行,第一列继续初始化,由于 dp[ 0 ][ j ] 只能由前所来,dp[ i ][ 0 ] 只能由上所来。

#:填表顺序:

        根据「状态转移方程」的推导来看,填表的顺序就是「从上往下」填每⼀行,在填写每⼀行的时候「从左往右」。

#:返回值:

        根据「状态表示」,我们要返回 dp[m - 1][n - 1] 的值。


C++ 算法代码

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int row = grid.size();
        int col = grid[0].size();

        // 1、创建dp表
        vector<vector<int>> dp(row, vector<int>(col, 0));
        
        // 2、初始化
        dp[0][0] = grid[0][0];
        for(int i = 1; i<col; i++)
            dp[0][i] = dp[0][i - 1] + grid[0][i];
        
        for(int i = 1; i<row; i++)
            dp[i][0] = dp[i - 1][0] + grid[i][0];

        // 3、填表
        for(int i = 1; i<row; i++)
            for(int j = 1; j<col; j++)
                dp[i][j] = grid[i][j] + min(dp[i - 1][j], dp[i][j - 1]);

        // 4、返回值
        return dp[row - 1][col - 1];
    }
};

复杂度分析

  • 时间复杂度:O(n*m),两层for循环。
  • 空间复杂度:O(n*m)

【DP边界、初始化】

        极度类似于上述题型,但是不同的是此处要的是最小,于是需要将一行,一列先初始化为最大值201(此处由于dp其余范围初始值不重要,直接全最大值即可)。

        然后便是利用 dp[ 0 ][ 1 ] = 1 / dp[ 1 ][ 0 ] = 1,于是推出有效范围中的第一个元素 [ 1 ][ 1 ]。然后利用第一行与第一列的201,一定大于有效范围中的有效值,保证可以在填表环节中不越界的运算。

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int row = grid.size();
        int col = grid[0].size();

        // 1、创建dp表
        vector<vector<int>> dp(row + 1, vector<int>(col + 1, 201));
        
        // 2、初始化
        dp[0][1] = 0;

        // 3、填表
        for(int i = 1; i < row + 1; i++)
            for(int j = 1; j < col + 1; j++)
                dp[i][j] = grid[i - 1][j - 1] + min(dp[i - 1][j], dp[i][j - 1]);

        // 4、返回值
        return dp[row][col];
    }
};

【空间优化 - 滚动数组】

C++ 算法代码

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int row = grid.size();
        int col = grid[0].size();

        // 1、创建dp表
        vector<int> dp(col + 1, 201);
        
        // 2、初始化
        dp[1] = 0;

        // 3、填表
        for(int i = 1; i < row + 1; i++)
            for(int j = 1; j < col + 1; j++)
                dp[j] = grid[i - 1][j - 1] + min(dp[j], dp[j - 1]);

        // 4、返回值
        return dp[col];
    }
};

复杂度分析

  • 时间复杂度:O(n*m),两层for循环。
  • 空间复杂度:O(n)

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

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

相关文章

【Python FTP/SFTP】零基础也能轻松掌握的学习路线与参考资料

一、Python FTP/SFTP的学习路线 Python FTP/SFTP是Python语言的两种常用的文件传输协议。在学习Python网络编程过程中&#xff0c;学习FTP/SFTP是非常重要的一步。下面给出Python FTP/SFTP的学习路线&#xff1a; 了解FTP/SFTP协议 在开始学习Python FTP/SFTP之前&#xff0…

LSB信息隐藏——Python实现(完整解析版)

系列文章目录 仿射密码实验-Python实现 仿射密码实验——Python实现(完整解析版) DES密码实验-C语言实现 MD5密码实验——Python实现(完整解析版) 文章目录 系列文章目录前言实验方法实验环境实验内容实验步骤1.LSB原理2.确定设计模块Lsb——embdedLsb——extract 实验结果实验…

performance_schema 初相识 配置详解 应用

千金良方&#xff1a;MySQL性能优化金字塔法则 第4章 performance_schema初相识 第5章 performance_schema配置详解 第6章 performance_schema应用示例荟萃 简介 1、实时监控Server性能监控和诊断的工具 2、它提供了丰富的性能指标和事件&#xff0c;可以帮助你深入了解 MyS…

2023年6月PMP®项目管理认证招生简章

PMP认证是Project Management Institute在全球范围内推出的针对评价个人项目管理知识能力的资格认证体系。国内众多企业已把PMP认证定为项目经理人必须取得的重要资质。 【PMP认证收益】 1、能力的提升&#xff08;领导力&#xff0c;执行力&#xff0c;创新能力&#xff0c…

python 编译安装指定版本 for linux

python环境是linux中必备的&#xff0c;部分发行版会自带python&#xff0c;有时候需要安装手动安装 注意&#xff1a;如果需要多个版本并存&#xff0c;建议使用conda环境&#xff0c;如果自己配置多版本&#xff0c;需要用多个软链接 conda环境&#xff0c;可以参考&#x…

【CSS Zoro 01】说在前面 CSS概念 CSS语法 CSS选择器 元素 id 类 组合 通用 分组 属性 后代 子元素 相邻兄弟

CSS 说在前面概念语法 syntaxCSS选择器 说在前面 最近挺喜欢看One Piece的&#xff0c;并且发现前端三剑客如果对应上Sanji&#xff0c;Zoro和Luffy的话会很有趣&#xff0c;所以说非常想在博客里面对应上小彩蛋&#xff0c;即使会损失一些SEO&#xff0c;但是这样做对我来说很…

算法修炼之筑基篇——筑基一层(解决01背包问题)

✨博主&#xff1a;命运之光 ✨专栏&#xff1a;算法修炼之练气篇​​​​​ ✨博主的其他文章&#xff1a;点击进入博主的主页 前言&#xff1a;学习了算法修炼之练气篇想必各位蒟蒻们的基础已经非常的扎实了&#xff0c;下来我们进阶到算法修炼之筑基篇的学习。筑基期和练气期…

raise AssertionError(“Torch not compiled with CUDA enabled“)

1、运行代码可知&#xff0c;当前cuda不可用。 import torch print(torch.cuda.is_available()) # False 2、打开power shell or cmd&#xff0c;输入nvidia-smi命令&#xff0c;检查当前英伟达显卡信息。 可知当前驱动版本512.78&#xff0c;支持的cuda最高版本为11.6&…

时间序列教程 六、深度学习与时间序列分析结合

一、深度学习方法 与传统的时间序列预测模型相比,神经网络有以下几个好处: 1、自动学习如何将趋势、季节性和自相关等系列特征纳入预测。 2、能够捕捉非常复杂的模式。 3、可以同时建模许多相关的系列,而不是单独处理每个系列。 但是神经网络有一些劣势: 1、模型的构建可能…

PyCharm开发工具的安装与使用

PyCharm集成开发工具(IDE),是当下全球python开发者&#xff0c;使用最频繁的工具软件。绝大多数的python程序&#xff0c;都是在PyCharm工具内完成的开发。 1.先进行下载并安装它 下载官网地址&#xff1a;https://www.jetbrains.com/pycharm/download/#sectionwindows 宝子…

SpringBoot自定义拦截器实现权限过滤功能(基于责任链模式)

前段时间写过一篇关于自定义拦截器实现权限过滤的文章&#xff0c;当时是用了自定义mybatis拦截器实现的&#xff1a;SpringBoot自定义Mybatis拦截器实现扩展功能(比如数据权限控制)。最近学习设计模式发现可以用责任链模式实现权限过滤&#xff0c;因此本篇采用责任链模式设计…

Docker(概述、安装、配置、镜像操作)

一、docker是什么&#xff1f; docker是一种go语言开发的应用容器引擎&#xff0c;运行容器里的应用。docker是用来管理容器和镜像的一种工具。 容器引擎&#xff1a;docker、rocket、podman、containerd 容器与虚拟机的区别 容器&#xff1a;所有容器共享宿主机内核。使用…

【手撕Spring源码】AOP

文章目录 AOP 实现之 ajc 编译器AOP 实现之 agent 类加载AOP 实现之 proxyJDK代理CGLIB代理JDK动态代理进阶CGLIB代理进阶MethodProxy JDK 和 CGLIB 在 Spring 中的统一切点匹配从 Aspect 到 Advisor通知转换调用链执行静态通知调用动态通知调用 AOP 底层实现方式之一是代理&am…

Java/Compose Desktop项目中进行python调用

写在前面 开发compose desktop项目爬网站时遇到验证码处理不方便需要借助python庞大的处理能力&#xff0c;这时需要再项目中调用python去执行获取结果&#xff0c;这里记录一下使用过程。 本次开发记录基于&#xff1a;python 3.9&#xff0c;compose 1.3 java17 工具&#x…

2年测试我迷茫了,软件测试大佬都会哪些技能?我的测试进阶之路...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 Python自动化测试&…

【MCS-51单片机汇编语言】期末复习总结④——求定时器初值(题型四)

文章目录 重要公式T~机器~ 12 / ∫~晶振~(2^n^ - X) * T~机器~ T~定时~ 工作方式寄存器TMOD常考题型例题1题解方式0方式1 关于定时器的常考题目为已知晶振 ∫ 、定时时间&#xff0c;求定时器初值。 重要公式 T机器 12 / ∫晶振 (2n - X) * T机器 T定时 其中n为定时器位数…

线性代数2:矩阵(1)

目录 矩阵&#xff1a; 矩阵的定义&#xff1a; 0矩阵 方阵 同型矩阵&#xff1a; 矩阵相等的判定条件 矩阵的三则运算&#xff1a; 乘法的适用条件 矩阵与常数的乘法&#xff1a; 矩阵的乘法&#xff1a; 矩阵的乘法法则&#xff1a; Note1&#xff1a; Note2&…

【数据库】表数据delete了,表文件大小不变

背景 在本周的时候&#xff0c;接到了短信数据空间报警短信&#xff0c;提示的是磁盘空间占用80以上&#xff0c;而这个数据库总体的存储量一共100G&#xff0c;商量之后决定在不升配置的前提下&#xff0c;删除一些不需要的数据表。比如针对A表删除1000W数据。但是和DBA沟通后…

FAST-LIO2论文阅读

目录 迭代扩展卡尔曼滤波增量式kd-tree&#xff08;ikd-tree&#xff09;增量式维护示意图ikd-tree基本结构与构建ikd-tree的增量更新&#xff08;Incremental Updates&#xff09;逐点插入与地图下采样使用lazy labels的盒式删除属性更新 ikd-tree重平衡平衡准则重建及并行重建…

SMTP简单邮件传输协议(C/C++ 发送电子邮件)

SMTP是用于通过Internet发送电子邮件的协议。电子邮件客户端&#xff08;如Microsoft Outlook或macOS Mail应用程序&#xff09;使用SMTP连接到邮件服务器并发送电子邮件。邮件服务器还使用SMTP将邮件从一个邮件服务器交换到另一个。它不用于从服务器下载电子邮件&#xff1b;相…