当前位置: 首页 > article >正文

LeetCode 热题100 之 多维动态规划

1.不同路径

在这里插入图片描述
思路分析:动规五部曲

  • dp数组定义:dp[i][j]表示从起点(0,0)到位置(i,j)的路径数量
  • 递推公式:dp[i][j] = dp[i-1][j] + dp[i][j-1]
    • 从 (i-1, j) 位置到 (i, j) 需要走一步向下的路径。
    • 从 (i, j-1) 位置到 (i, j) 需要走一步向右的路径。
    • 所以,当前格子 (i, j) 的路径总数等于其上方和左方的路径数之和。
  • 初始化方式
    • 因为递推公式里面的状态要依赖于上一行和上一列的状态,所以开始应该初始化第一行和第一列。
    • 在第一行上,dp[0][j] = 1(所有位置均为 1)。因为从 (0, 0) 到 (0, j) 只能通过向右的路径到达,即没有选择,只能一路往右走。
    • 在第一列上,dp[i][0] = 1(所有位置均为 1)。因为从 (0, 0) 到 (i, 0) 只能通过向下的路径到达,即没有选择,只能一路往下走。
  • 遍历顺序
    • 遍历每一行
    • 遍历每一列
  • 打印dp数组:最后应该返回dp[m - 1][n - 1];

具体实现代码(详解版):

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m, vector<int>(n, 0));  // 初始化 dp 数组

        // 初始化第一行和第一列
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                dp[i][0] = 1;  // 第一行
                dp[0][j] = 1;  // 第一列
            }
        }

        // 根据递推公式填充 dp 数组
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];  // 递推公式
            }
        }

        return dp[m - 1][n - 1];  // 返回右下角的路径数
    }
};

优化:在这个问题中,我们可以观察到每一行的 dp 数组的计算只依赖于当前行和上一行的值。因此,我们只需要存储当前行和上一行的值,而不需要存储整个二维数组

  • 我们只需要一个一维数组 dp 来保存当前行的计算结果,并用一个临时数组保存上一行的结果。
  • 每次计算新的行时,我们只需要根据上一行的值和当前行的值来更新 dp 数组,因此可以将二维 dp 数组压缩为一维。
  • 递推关系
    • 对于每一行 i(从第二行开始),我们依次更新每一列 j。更新的方式是根据左边的 dp[j-1] 和上面的 dp[j] 来计算当前格子的路径数:
      dp[j] = dp[j] + dp[j-1]。
    • 当所有的行都遍历完毕,最终的结果会存储在 dp[n-1] 中,也就是右下角的位置。

具体实现代码(详解版):

class Solution {
public:
    int uniquePaths(int m, int n) {
        // 创建一个一维 dp 数组,用来存储当前行的路径数
        vector<int> dp(n, 1);  // 初始化第一行为 1

        // 从第二行开始计算
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                // dp[j] 存储的是上一行的 dp[j],dp[j-1] 存储的是上一行的 dp[j-1]
                dp[j] = dp[j] + dp[j - 1];  // 更新当前行的 dp[j]
            }
        }

        // 最终的结果存储在 dp[n-1] 中,表示右下角的路径数
        return dp[n - 1];
    }
};

2.最小路径和

在这里插入图片描述
思路分析:动规五部曲

  • dp数组定义:dp[i][j]表示从左上角 (0, 0) 到达位置 (i, j) 的路径的最小路径和
  • 递推公式
    • 每个位置 (i, j) 的最小路径和可以通过选择从上方或左方到达 (i, j) 的路径中的较小者,再加上当前格子的值 grid[i][j] 来获得。
    • 的·因此,递推公式为:dp[i][j]=grid[i][j]+min(dp[i−1][j],dp[i][j−1]);
    • 特殊情况:当 i == 0 或 j == 0 时,只能从左边或上边到达 (i, j),需要单独处理。
  • 初始化
    • dp[0][0] = 0;
    • 第一行和第一列只能从左到右或从上到下移动,因此可以逐步累加初始化。
  • 遍历顺序
    • 遍历每一行
    • 遍历每一列
  • 打印dp数组:返回dp[m - 1][n - 1]即可。

这里特别要注意初始化的时候,具体来说,第一行和第一列的初始化应该在外部循环中进行,而不仅仅是在两个内部循环中。否则会导致每次进入 j 循环时都会重新赋值,这会导致结果错误。

具体实现代码(详解版):

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

        vector<vector<int>> dp(m, vector<int>(n, 0));  // 初始化 dp 数组
        dp[0][0] = grid[0][0];  // 起始点赋值

        // 初始化第一行
        for (int j = 1; j < n; ++j) {
            dp[0][j] = dp[0][j - 1] + grid[0][j];  // 第一行只能从左边来
        }

        // 初始化第一列
        for (int i = 1; i < m; ++i) {
            dp[i][0] = dp[i - 1][0] + grid[i][0];  // 第一列只能从上面来
        }

        // 动态规划填充 dp 表
        for (int i = 1; i < m; ++i) {
            for (int j = 1; j < n; ++j) {
                dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];  // 选择上方或左方的最小路径和
            }
        }

        return dp[m - 1][n - 1];  // 返回右下角的最小路径和
    }
};

优化:当前的算法空间复杂度是 O(m * n),因为我们使用了一个二维数组 dp 来存储每个位置的最小路径和。实际上,计算每个位置的最小路径和时,只需要用到当前行和上一行的结果,因此可以将 dp 数组优化为一维数组,从而将空间复杂度降到 O(n)。

  • 用一个一维数组 dp[j] 来表示当前行的最小路径和。每次更新时,我们从左到右依次计算当前行的值,每一列的最小路径和都只依赖于当前列和上一列的值。
  • 使用 dp[j] 表示当前行的值,而上一行的结果会被覆盖。所以在计算时,只需要 dp[j] 和 dp[j-1] 来更新当前的最小路径和。
  • 递推公式:dp[j] = min(dp[j], dp[j - 1]) + grid[i][j];
  • 初始化
    • dp[0] 初始化为 grid[0][0],表示从起点开始的最小路径和。
    • 初始化第一行

具体实现代码(详解版):

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int m = grid.size();
        int n = grid[0].size();
        
        // 使用一个一维数组来表示每一行的最小路径和
        vector<int> dp(n, 0);
        
        // 初始化第一行
        dp[0] = grid[0][0];  // dp[0]表示第一行第一个位置的最小路径和
        for (int j = 1; j < n; ++j) {
            dp[j] = dp[j - 1] + grid[0][j];  // 第一行只能从左边来
        }

        // 更新后续行
        for (int i = 1; i < m; ++i) {
            dp[0] += grid[i][0];  // 第一列只能从上方来
            for (int j = 1; j < n; ++j) {
                dp[j] = min(dp[j], dp[j - 1]) + grid[i][j];  // 选择最小路径
            }
        }

        return dp[n - 1];  // 返回右下角的最小路径和
    }
};

3.最长回文字串

在这里插入图片描述
思路分析1:还是使用动态规划

  • dp数组定义:表示从字符串 s[i…j] 是否是回文串
  • dp数组更新
    • s[i] == s[j]:首先要判断子串的两端字符是否相同。
    • (j - i <= 1 || dp[i+1][j-1]):如果 i 和 j 相邻(即长度为2),或者子串 s[i+1…j-1] 是回文串,那么 s[i…j] 也一定是回文串。
    • 此时,dp[i][j] = true;
  • 初始化:初始化时,所有的 dp[i][j] 默认值是 false。
  • 遍历顺序
    • 外层循环:i 从字符串的最后一位遍历到第一位,这样可以保证对于每一个位置,i 到 j 的子串已经根据先前的字符状态得到了正确的回文性。
    • 内层循环:j 从 i 开始遍历到字符串的最后一位,表示检查从 i 到 j 的子串。
  • 更新最长回文字串的长度和内容
    • 每次找到一个回文子串时,更新 result,并且如果找到的回文子串长度大于当前的最长回文串,更新 str 为当前的回文子串。
  • 返回结果:最后返回保存的最长回文子串str

dp[i][j] 需要满足两个条件:s[i] == s[j] 和 s[i+1…j-1] 是回文(或者子串长度为2)

具体实现代码(详解版):

class Solution {
public:
   string longestPalindrome(string s) {
       // 定义一个二维dp数组,dp[i][j]表示子串s[i..j]是否是回文串
       vector<vector<int>> dp(s.size(), vector<int> (s.size(),false));
       
       int result = 0;  // 用于保存当前找到的最长回文串的长度
       string str;      // 用于保存最长回文子串

       // 从右到左遍历字符串s,外层循环是i从s.size()-1到0
       for(int i = s.size()-1; i >= 0; i--){ 
           // 内层循环是j从i到s.size(),遍历从i到j的子串
           for(int j = i; j < s.size(); j++){
               
               // 如果s[i] == s[j],并且子串s[i+1..j-1]是回文串(或者i和j之间只有一个字符)
               if(s[i] == s[j] && (j-i <= 1 || dp[i+1][j-1])){
                   dp[i][j] = true;  // 将dp[i][j]设为true,表示s[i..j]是回文串
                   
                   // 更新最长回文串的长度result
                   result = max(result, j - i);
                   
                   // 如果当前子串长度大于result,更新最长回文子串str
                   if(j-i >= result) str = s.substr(i, j-i+1);
               }
           }
       }
       return str;  // 返回最长回文子串
   }
};
  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2)

思路分析2:中心扩展法

  • 回文串的特性:回文串是对称的,因此我们可以从每个字符(或每对字符之间)出发,扩展到两边,检查是否满足回文的条件。
    • 单字符中心:例如“aba”,“b”是回文的中心,左右扩展。
    • 双字符中心:例如“abba”,两个字符之间“bb”是回文的中心,左右扩展。
  • 对于每个字符 s[i],我们考虑它作为回文的中心,分别向左和向右扩展,检查回文的长度。可以通过两种方式扩展。
  • 每次扩展时,expandAroundCenter函数:这个辅助函数从给定的中心 left 和 right 开始,向两边扩展,直到遇到不匹配的字符为止。它返回扩展后的回文子串。记录当前找到的最长回文子串。
    • left-- 和 right++ 分别向左和向右扩展,直到左右字符不相等或者越界。
    • s.substr(left + 1, right - left - 1) 计算当前回文子串并返回。

具体实现代码(详解版):

class Solution {
public:
    string longestPalindrome(string s) {
        if (s.empty()) return "";
        
        int n = s.size();
        string result = "";
        
        // 中心扩展法
        for (int i = 0; i < n; i++) {
            // 1. 以 i 为中心,扩展回文串
            string odd = expandAroundCenter(s, i, i);
            // 2. 以 i 和 i+1 为中心,扩展回文串
            string even = expandAroundCenter(s, i, i + 1);
            
            // 更新最长回文子串
            if (odd.size() > result.size()) {
                result = odd;
            }
            if (even.size() > result.size()) {
                result = even;
            }
        }
        
        return result;
    }
    
    // 辅助函数,扩展回文串
    string expandAroundCenter(const string& s, int left, int right) {
        while (left >= 0 && right < s.size() && s[left] == s[right]) {
            left--;
            right++;
        }
        return s.substr(left + 1, right - left - 1);
    }
};

  • 时间复杂度: o ( n 2 ) o(n^2) o(n2)
  • 空间复杂度: O ( 1 ) O(1) O(1)

4.最长公共子序列

在这里插入图片描述
思路分析:动规五部曲

  • dp数组定义:dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
  • 递推公式
    • 主要就是两大情况: text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[j - 1]不相同
    • 如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;
    • 如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
  • 初始化:test1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0;同理dp[0][j]也是0。其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。
  • 确定遍历顺序:从递推公式,可以看出,有三个方向可以推出dp[i][j],那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。
    -打印dp数组 :dp[text1.size()][text2.size()]为最终结果

具体实现代码(详解版):

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        // 定义一个二维 dp 数组,用于存储子问题的解
        // dp[i][j] 表示 text1[0...i-1] 和 text2[0...j-1] 的最长公共子序列的长度
        vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
        
        // 遍历 text1 和 text2
        for (int i = 1; i <= text1.size(); i++) {
            for (int j = 1; j <= text2.size(); j++) {
                // 如果 text1[i-1] == text2[j-1],说明当前字符匹配
                if (text1[i - 1] == text2[j - 1]) {
                    // 在前一个状态 dp[i-1][j-1] 的基础上增加 1
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    // 否则取前一个状态 dp[i-1][j] 或 dp[i][j-1] 中的较大值
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        
        // 最终的最长公共子序列长度为 dp[text1.size()][text2.size()]
        return dp[text1.size()][text2.size()];
    }
};

5.编辑距离

在这里插入图片描述
思路分析:动规五部曲

  • dp数组定义:dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
  • 递推公式:dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
if (word1[i - 1] == word2[j - 1])
    不操作
if (word1[i - 1] != word2[j - 1])
    增
    删
    换
  • if (word1[i - 1] == word2[j - 1]) 那么说明不用任何编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][j - 1];
    此时可能有同学有点不明白,为啥要即dp[i][j] = dp[i - 1][j - 1]呢?
    那么就在回顾上面讲过的dp[i][j]的定义,word1[i - 1] 与 word2[j - 1]相等了,那么就不用编辑了,以下标i-2为结尾的字符串word1和以下标j-2为结尾的字符串word2的最近编辑距离dp[i - 1][j - 1]就是 dp[i][j]了。
  • if (word1[i - 1] != word2[j - 1]),此时就需要编辑了,如何编辑呢?
    • 操作一:word1删除一个元素,那么就是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上一个操作。
      dp[i][j] = dp[i - 1][j] + 1;

    • 操作二:word2删除一个元素,那么就是以下标i - 1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上一个操作。
      dp[i][j] = dp[i][j - 1] + 1

    • word2添加一个元素,相当于word1删除一个元素,例如 word1 = “ad” ,word2 = “a”,word1删除元素’d’ 和 word2添加一个元素’d’,变成word1=“a”, word2=“ad”, 最终的操作数是一样! dp数组如下图所示意的:在这里插入图片描述

    • 操作三:替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增删加元素。那么只需要一次替换的操作,就可以让 word1[i - 1] 和 word2[j - 1] 相同。所以 dp[i][j] = dp[i - 1][j - 1] + 1;

    • 综上,当 if (word1[i - 1] != word2[j - 1]) 时取最小的,即:dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;

  • 初始化
    • p[i][0] :以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0]。那么dp[i][0]就应该是i,对word1里的元素全部做删除操作,即:dp[i][0] = i;同理dp[0][j] = j;
  • 遍历顺序:
    • 我们从递推公式可以看出,dp[i][j]是依赖左上方、左边和上边元素的,所以在dp矩阵中一定是从左到右从上到下去遍历。
  • 打印dp数组:最后返回dp[wors1.size()][word2.size()]就是我们所求的编辑距离。

具体实现代码(详解版)

class Solution {
public:
    int minDistance(string word1, string word2) {
        // 创建二维动态规划数组 dp,大小为 (word1.size() + 1) x (word2.size() + 1)
        // dp[i][j] 代表将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最小操作数
        vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));

        // 初始化 dp 数组的第一列:dp[i][0] 代表将 word1 的前 i 个字符转换为空字符串的操作数,即删除操作
        for (int i = 0; i <= word1.size(); i++) dp[i][0] = i;
        
        // 初始化 dp 数组的第一行:dp[0][j] 代表将空字符串转换为 word2 的前 j 个字符的操作数,即插入操作
        for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;

        // 从 dp[1][1] 开始填充整个 dp 数组
        for (int i = 1; i <= word1.size(); i++) {
            for (int j = 1; j <= word2.size(); j++) {
                // 如果 word1 的第 i 个字符与 word2 的第 j 个字符相同,不需要任何操作,直接继承上一个状态
                if (word1[i - 1] == word2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1];
                }
                // 如果字符不同,则需要考虑三种操作:
                // 1. 删除 word1 的字符:dp[i - 1][j]
                // 2. 插入 word2 的字符:dp[i][j - 1]
                // 3. 替换 word1 的字符为 word2 的字符:dp[i - 1][j - 1]
                // 取三者的最小值,然后加 1,因为执行了一个操作(删除、插入或替换)
                else {
                    dp[i][j] = min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}) + 1;
                }
            }
        }

        // 返回最终的结果,dp[word1.size()][word2.size()] 即为将 word1 完全转换为 word2 所需的最小操作数
        return dp[word1.size()][word2.size()];
    }
};

参考文章:代码随想录


http://www.kler.cn/a/391251.html

相关文章:

  • Rust 所有权机制
  • 实现一个BLE HID鼠标
  • Python多进程间通讯(包含共享内存方式)
  • 开源 2 + 1 链动模式、AI 智能名片、S2B2C 商城小程序在用户留存与品牌发展中的应用研究
  • AI写作(二)NLP:开启自然语言处理的奇妙之旅(2/10)
  • Vue7种组件之间通信方式
  • 在 Flutter 应用中调用后端接口的方法
  • Linux高阶——1109—线程函数线程属性线程分类
  • 【缓存策略】你知道 Write Around(缓存绕过写)这个缓存策略吗?
  • SQL Servers审核提高数据库安全性
  • 机器学习day1-数据集
  • Ubuntu23.10下解决C语言调用mysql.h问题
  • arcgis for js实现FeatureLayer图层弹窗展示所有field字段
  • Matlab自学笔记四十一:介绍日期时间型的显示格式:年‘y‘ 月‘M‘ 日‘d‘ 周‘e‘ 时‘h‘ 分‘m‘ 秒‘s‘
  • 科研绘图系列:R语言差异分析双侧柱状图(grouped barplot)
  • PostgreSQL 表文件损坏异常
  • vue的样式知识点
  • 【Windows】任务管理器无法解决的问题?试试`tasklist`和`taskkill`命令
  • IOS开发之Xcode版本选择
  • POI实现根据PPTX模板渲染PPT
  • 攻防世界38-FlatScience-CTFWeb
  • 【Linux】:进程信号(信号概念 信号处理 信号产生)
  • SpringBoot框架的企业资产管理自动化
  • Docker pull镜像故障分析
  • python遇到问题
  • Snort的配置与使用