3.26-DP

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

思路:

dp[i][j]表示从起点到(i,j)的路径和最小

状态转移方程:dp[i+1][j+1]=min(dp[i+1][j],dp[i][j+1])+grid[i][j]

初始值:vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX))

dp[0][1] = 0

为什么要在最左边和最上面插入一排0?如果直接从grid[0][0]出发更新,那么在比较min(dp[0][1] , dp[1][0])时会出现min(INT_MAX , INT_MAX),无法更新。和01背包容量V的最左边插入一排0效果一样。

这样循环可以从(0,0)开始写:

for (int i = 0; i < m; i++) {
             for (int j = 0; j < n; j++) {
                dp[i + 1][j + 1] = min(dp[i][j + 1],dp[i + 1][j]) + grid[i][j];
             }
          }

答案为 dp[m][n],

写法一

class Solution {
  public:
      int minPathSum(vector<vector<int>>& grid) {
        int m = grid.size() , n = grid[0].size();
          vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
            dp[0][1] = 0;
          for (int i = 0; i < m; i++) {
             for (int j = 0; j < n; j++) {
                dp[i + 1][j + 1] = min(dp[i][j + 1],dp[i + 1][j]) + grid[i][j];
             }
          }
          return dp[m][n];
      }
  };

复杂度分析

  • 时间复杂度:O(mn),其中 mn 分别为 grid 的行数和列数。
  • 空间复杂度:O(mn)。

M:647. 回文子串

1.确定dp数组(dp table)以及下标的含义

如果大家做了很多这种子序列相关的题目,在定义dp数组的时候 很自然就会想题目求什么,我们就如何定义dp数组。绝大多数题目确实是这样,不过本题如果我们定义,dp[i] 为 下标i结尾的字符串有 dp[i]个回文串的话,我们会发现很难找到递归关系。

dp[i] dp[i-1]dp[i + 1] 看上去都没啥关系。

所以我们要看回文串的性质。 如图:

image-20250326145350219

我们在判断字符串S是否是回文,那么如果我们知道 s[1],s[2],s[3] 这个子串是回文的,那么只需要比较 s[0]和s[4]这两个元素是否相同,如果相同的话,这个字符串s 就是回文串。

那么此时我们是不是能找到一种递归关系,也就是判断一个子字符串(字符串下标范围[i,j])是否回文依赖于,子字符串(下标范围[i + 1, j - 1])) 是否是回文。

所以为了明确这种递归关系,我们的dp数组是要定义成一位二维dp数组。

布尔类型的dp[i][j]表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。

2.确定递推公式

  1. 当s[i]与s[j]不相等,dp[i][j]一定是false。

  2. 当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况

  • 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
  • 情况二:下标i 与 j相差为1,例如aa,也是回文子串
  • 情况三:下标:i 与 j 相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串,就看aba是不是回文就可以了,那么aba的区间就是i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。

以上三种情况分析完了,那么递归公式如下:

if(s[i] == s[j]) {
 	if(j - i <= 1){ //情况1,2
    res ++;
    dp[i][j] = true;
  }else if(dp[i + 1][j - 1]){//情况3
    res ++;
    dp[i][j] == true;
  }
}

res为返回结果,统计回文子串的数量。

3.dp数组初始化为false,因此s[i]!=s[j]的情况不用考虑

4.遍历顺序

首先从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]是否为true,在对dp[i][j]进行赋值true的。

dp[i + 1][j - 1] dp[i][j]的左下角,如图:

647.回文子串

如果这矩阵是从上到下,从左到右遍历,那么会用到没有计算过的dp[i + 1][j - 1],也就是根据不确定是不是回文的区间[i+1,j-1],来判断了[i,j]是不是回文,那结果一定是不对的。

所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的

有的代码实现是优先遍历列,然后遍历行,其实也是一个道理,都是为了保证dp[i + 1][j - 1]都是经过计算的。

5.举例推导dp数组

举例,输入:"aaa",dp[i][j]状态如下:

647.回文子串1

图中有6个true,所以就是有6个回文子串。

注意因为dp[i][j]的定义,所以j一定是大于等于i的,那么在填充dp[i][j]的时候一定是只填充右上半部分

代码:

class Solution {
  public:
      int countSubstrings(string s) {
          int n = s.size();
        vector<vector<int>> dp(n , vector<int>(n , 0));
        int res = 0;

        for (int i = n - 1; i >= 0; i--) {//注意遍历顺序从下到上,j从i开始加保证j >= i
            for (int j = i; j < n; j++) {
              if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) {
                res ++;
                dp[i][j] = true;
             }
            }
        }
        return res;
      }
  };

5. 最长回文子串 - 力扣(LeetCode)

思路:

有了上题的铺垫,这题在每次更新完dp[i][j]后记录下全局最长回文子串长度maxlenth和对应的左指针l,最后截取返回即可。

class Solution {
  public:
      string longestPalindrome(string s) {
          int n = s.size();
          vector<vector<int>> dp(n , vector<int>(n , 0));
          int maxlength = 0 , l , r;
          for (int i = n - 1; i >= 0 ; i--) {
                for (int j = i; j < n; j++) {
                   if(s[i]== s[j] && (j - i <= 1 || dp[i + 1][j - 1])){
                    dp[i][j] = true;                 
                   }
                   if(dp[i][j] && j - i + 1 > maxlength){
                    maxlength = j - i + 1;
                    l = i;
                    r = j;
                   }
                }
          }
          return s.substr(l , maxlength);
      }
  };
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n^2)

法二:双指针

动态规划的空间复杂度是偏高的。

首先确定回文串,就是找中心然后想两边扩散看是不是对称的就可以了。

在遍历中心点的时候,要注意中心点有两种情况

一个元素可以作为中心点,两个元素也可以作为中心点。

class Solution {
  public:
      int l = 0 , r = 0 , maxlength = 0;
      string longestPalindrome(string s) {
          int res = 0;
          int n = s.size();
          auto extend = [&](int i , int j)->void{
              while(i >= 0 && j < n && s[i] == s[j]){
                if(j - i + 1 > maxlength){
                  l = i;
                  r = j;
                  maxlength = j - i + 1;
                }
                i --;
                j ++;
              }
          };
          for (int i = 0; i < n; i++) {
             extend(i , i);  //以i , i 为中心扩散
             extend(i , i + 1);  //以i , i + 1 为中心扩散              
          }
          return s.substr(l , maxlength);
      }
  };
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)

M:516.最长回文子序列

思路:

回文子串,回文子序列都是动态规划经典题目。回文子串是要连续的,回文子序列可不是连续的!

回文子串系列:

  • 647.回文子串 返回的是回文子串的数目,每次更新dp[i][j]时res++
  • 5.最长回文子串 返回的是最长子串,每次更新时需要记下左端点l和长度maxlength
  • 516.最长回文子序列 返回的是长度

思路其实是差不多的,但本题要比求回文子串简单一点,因为情况少了一点。

动规五部曲分析如下:

1.确定dp数组(dp table)以及下标的含义

dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]

2.确定递推公式

在判断回文子串的题目中,关键逻辑就是看s[i]与s[j]是否相同

如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2;

如图: 516.最长回文子序列

如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子序列的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。

因为dp[i][j]由状态dp[i + 1][j - 1]转移而来:

  • 加入s[j]的回文子序列长度为dp[i + 1][j]

  • 加入s[i]的回文子序列长度为dp[i][j - 1]

那么dp[i][j]一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);

516.最长回文子序列1

代码如下:

if (s[i] == s[j]) {
    dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}

3.dp数组如何初始化

首先要考虑当i 和j 相同的情况,从递推公式:dp[i][j] = dp[i + 1][j - 1] + 2; 可以看出 递推公式是计算不到 i 和 j 相同时候的情况。

所以需要手动初始化一下,当i与j相同,那么dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1。

其他情况dp[i][j]初始为0就行,这样递推公式:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); 中dp[i][j]才不会被初始值覆盖。

vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for (int i = 0; i < s.size(); i++) dp[i][i] = 1;

4.确定遍历顺序

从递归公式中,可以看出,dp[i][j] 依赖于 dp[i + 1][j - 1]dp[i + 1][j] dp[i][j - 1],如图:

img

所以遍历i的时候一定要从下到上遍历,这样才能保证下一行的数据是经过计算的

j的话,可以正常从左向右遍历。

代码如下:

for (int i = n - 1; i >= 0; i--) {
    for (int j = i + 1; j < n; j++) {
        if (s[i] == s[j]) {
            dp[i][j] = dp[i + 1][j - 1] + 2;
        } else {
            dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
        }
    }
}

5.举例推导dp数组

输入s:"cbbd" 为例,dp数组状态如图:

516.最长回文子序列3

红色框即:dp[0][s.size() - 1]; 为最终结果。

如果是回文子串求最大长度,那么要定义全局变量maxlen,在过程中取最大值,因为dp数组是布尔类型,保存的是ij之间是否回文,而不是长度。如果定义为长度,状态转移及其复杂。

class Solution {
  public:
      int longestPalindromeSubseq(string s) {
          int n = s.length();
          vector<vector<int>> dp(n , vector<int>(n , 0));
          for (int i = 0; i < n; i++)  dp[i][i] = 1;
             
          for (int i = n - 1; i >=  0; i --) {
              for (int j = i + 1; j < n; j++) {
                 if(s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1] + 2;
                else {
                      dp[i][j] = max(dp[i + 1][j] , dp[i][j - 1]);                  
                 }
              }
          }
          return dp[0][n - 1];
      }
  };

经典线性 DP

§4.1 最长公共子序列(LCS)

一般定义 f[i][j] 表示对 (s[:i],t[:j]) 的求解结果。


1143. 最长公共子序列 - 力扣(LeetCode)

灵神的思路:

image-20250326215903142

image-20250326220039026

image-20250326220651364

image-20250326220732940

最终可以得到递推关系式:

\(d f s(i, j)=\left\{\begin{array}{ll} d f s(i-1, j-1)+1 & s[i]=t[j] \\ \max (d f s(i-1, j), d f s(i, j-1)) & s[i] \neq t[j] \end{array}\right.\)

1.确定dp数组以及下标的含义

dp[i][j]:长度为[0, i - 1]的字符串text1 与 长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]

有同学会问:为什么要定义长度为[0, i - 1]的字符串text1,定义为长度为[0, i]的字符串text1不香么?

这样定义是为了后面代码实现方便,如果非要定义为长度为[0, i]的字符串text1也可以,我在 动态规划:718. 最长重复子数组 (opens new window)中的「拓展」里 详细讲解了区别所在,其实就是简化了dp数组第一行和第一列的初始化逻辑

2.确定递推公式

主要就是两大情况: 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]);

代码如下:

if (text1[i - 1] == text2[j - 1]) {
    dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}

3.dp数组如何初始化

先看看dp[i][0]应该是多少呢?

test1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0;同理dp[0][j]也是0。

其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。

代码:

vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));

4.确定遍历顺序

从递推公式,可以看出,有三个方向可以推出dp[i][j],如图:

1143.最长公共子序列

那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。

代码如下:

class Solution {
  public:
      int longestCommonSubsequence(string text1, string text2) {
          int m = text1.size() , n = text2.size();
          vector<vector<int>> dp(m + 1 , vector<int>(n + 1 , 0));

          for (int i = 1; i <= m; i++) {
              for (int j = 1; j <= n; j++) {
                //记住定义:dp[i][j]:长度为[0, i - 1]的字符串text1 与 长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j],因此比较的是text[i - 1]与text2[j - 1]
                 if(text1[i - 1] == text2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
                 else{
                  dp[i][j] = max(dp[i][j - 1] , dp[i - 1][j]);
                 } 
              }
          }
          return dp[m][n];
      }
  };

72. 编辑距离 - 力扣(LeetCode)

思路:

image-20250326223016636

m = word1.size() , n = word2.size()

考虑word1[i] 与 word2[j] 是否相等的情况

  1. s[i] == t[j]时
    • dp[i][j] = dp[i - 1][j - 1]
  2. s[i] != t[j]
    • dp[i][j] = min(dp[i , j - 1] , dp[i - 1][j] , dp[i - 1][j - 1])

编辑距离是用动规来解决的经典题目,这道题目看上去好像很复杂,但用动规可以很巧妙的算出最少编辑距离。

1. 确定dp数组(dp table)以及下标的含义

dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]

2. 确定递推公式

在确定递推公式的时候,首先要考虑清楚编辑的几种操作,整理如下:

if (word1[i - 1] == word2[j - 1])
   // 那么说明不用任何编辑,不操作
if (word1[i - 1] != word2[j - 1])
    增
    删
    换

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数组如下图所示意的:

            a                         a     d
   +-----+-----+             +-----+-----+-----+
   |  0  |  1  |             |  0  |  1  |  2  |
   +-----+-----+   ===>      +-----+-----+-----+
 a |  1  |  0  |           a |  1  |  0  |  1  |
   +-----+-----+             +-----+-----+-----+
 d |  2  |  1  |
   +-----+-----+
  • 操作三:替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增删加元素。

可以回顾一下,if (word1[i - 1] == word2[j - 1])的时候我们的操作 是 dp[i][j] = dp[i - 1][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;

三个或三个以上的数取max、min,可用大括号括起来{},这个版本接受一个初始化器列表

递归公式代码如下:

if (word1[i - 1] == word2[j - 1]) {
    dp[i][j] = dp[i - 1][j - 1];
}
else {
    dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
}

3. dp数组如何初始化

dp[i][0] :以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0]

那么dp[i][0]就应该是i,对word1里的元素全部做删除操作,即:dp[i][0] = i;

同理dp[0][j] = j;

所以C++代码如下:

for (int i = 0; i <= word1.size(); i++) dp[i][0] = i;
for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;

4. 确定遍历顺序

可以看出dp[i][j]是依赖左方,上方和左上方元素的,如图:

72.编辑距离

所以在dp矩阵中一定是从左到右从上到下去遍历。

代码如下:

for (int i = 1; i <= word1.size(); i++) {
    for (int j = 1; j <= word2.size(); j++) {
        if (word1[i - 1] == word2[j - 1]) {
            dp[i][j] = dp[i - 1][j - 1];
        }
        else {
            dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
        }
    }
}

5. 举例推导dp数组

代码如下:

class Solution {
  public:
      int minDistance(string word1, string word2) {
          int m = word1.size() , n = word2.size();
          vector<vector<int>> dp(m + 1 , vector<int>(n + 1 , INT_MAX));

          for (int i = 0; i <= m; i++) {
             dp[i][0] = i;
          }

          for (int j = 0; j <= n; j++) {
             dp[0][j] = j;
          }

          for (int i = 1; i <= m; i++) {
             for (int j = 1; j <= n; j++) {
               //dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j],因此比较的是word1[i - 1]与word2[j - 1]
                if(word1[i - 1] == word2[j - 1])  dp[i][j] = dp[i - 1][j - 1];

                else{
                  dp[i][j] = min({dp[i][j - 1] , dp[i - 1][j] , dp[i - 1][j - 1]}) + 1;
                }
             }
          }
          return dp[m][n];
      }
  };

至此,hot100一刷完结~

posted @ 2025-03-26 23:05  七龙猪  阅读(1)  评论(0)    收藏  举报
-->