Day45-动态规划,leetcode115,583,72

  1. 不同的子序列
  • 给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。
  • 测试用例保证结果在 32 位有符号整数范围内。

  • 思路
  • 字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串
  • 1.确定dp数组定义及下标的含义:dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]
  • 2.确定递推公式:分两种情况,(1)s[i - 1] 与 t[j - 1]相等,可以选择用s[i - 1]来匹配,也可以选择不用s[i - 1]来匹配,即 dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];(2)s[i - 1] 与 t[j - 1] 不相等,只能考虑不用s[i - 1]匹配,dp[i][j] = dp[i - 1][j];
  • 3.dp数组如何初始化:dp[i][0] ,以i-1为结尾的s可以随便删除元素,出现空字符串的个数。那么dp[i][0]=1;dp[0][j]:空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数。那么dp[0][j]=0,s如论如何也变成不了t。dp[0][0]=1,空字符串s,可以删除0个元素,变成空字符串t。
  • 4.确定遍历顺序:从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][j]都是根据左上方和正上方推出来的。所以遍历的时候一定是从上到下,从左到右,这样保证dp[i][j]可以根据之前计算出来的数值进行计算。
  • 5.举例推导dp数组,打印dp数组:
/**
 * 1. dp数组定义
    dp[i][j] 表示:s 的前 i 个字符中,t 的前 j 个字符作为子序列出现的个数。
 * 2. 初始化
    空字符串 t 在任何 s 的前缀中都只出现一次(即都可以通过删除所有字符得到空串),所以 dp[i][0] = 1。
    其他 dp[0][j] 默认是 0,因为空字符串 s 无法变成非空字符串 t。
 * 3. 状态转移
    如果 s[i-1] === t[j-1],可以选择用 s[i-1] 匹配 t[j-1](即 dp[i-1][j-1]),也可以不用 s[i-1](即 dp[i-1][j]),两种情况之和。
    如果 s[i-1] !== t[j-1],只能不用 s[i-1],即 dp[i-1][j]。
 * 4. 返回结果
    dp[s.length][t.length] 就是 s 中 t 作为子序列出现的总个数。

 * 用二维动态规划,统计 s 的每个前缀中 t 的每个前缀作为子序列出现的次数,最终返回总数。
 */
const numDistinct = (s, t) => {
    let dp = Array.from(Array(s.length + 1), () => Array(t.length +1).fill(0));

    for(let i = 0; i <=s.length; i++) {
        dp[i][0] = 1;
    }
    
    for(let i = 1; i <= s.length; i++) {
        for(let j = 1; j<= t.length; j++) {
            if(s[i-1] === t[j-1]) {
                dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
            } else {
                dp[i][j] = dp[i-1][j]
            }
        }
    }

    return dp[s.length][t.length];
};


  1. 两个字符串的删除操作
  • 给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
  • 每步 可以删除任意一个字符串中的一个字符。

  • 思路
  • 1.确定dp数组定义及下标的含义:dp[i][j]:以i-1为结尾的字符串word1,和以j-1为结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。
  • 2.确定递推公式:
  • 当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1];
  • 当word1[i - 1] 与 word2[j - 1]不相同的时候,有三种情况:
  • 情况一:删word1[i - 1],最少操作次数为dp[i - 1][j] + 1
  • 情况二:删word2[j - 1],最少操作次数为dp[i][j - 1] + 1
  • 情况三:同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2
  • 三种情况取最小值,递推公式:dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1})
  • 3.dp数组如何初始化:dp[i][0]:word2为空字符串,以i-1为结尾的字符串word1要删除多少个元素,才能和word2相同呢,dp[i][0] = i。同理dp[0][j] = j。
  • 4.确定遍历顺序:从上到下,从左到右
  • 5.举例推导dp数组,打印dp数组:
/**
 * 1. dp数组定义
    dp[i][j] 表示:将 word1 的前 i 个字符和 word2 的前 j 个字符变成相同所需的最少删除次数。
 * 2. 初始化
    如果 word2 是空串,word1 需要删掉所有字符才能相同,所以 dp[i][0] = i。
    如果 word1 是空串,word2 需要删掉所有字符才能相同,所以 dp[0][j] = j。
 * 3. 状态转移
    如果当前字符相同,不需要删除,直接继承左上角的值。
    如果不同,有三种选择,取最小值:
        删除 word1[i-1],即 dp[i-1][j] + 1
        删除 word2[j-1],即 dp[i][j-1] + 1
        同时删除两者,dp[i-1][j-1] + 2
 * 4. 返回结果
    dp[word1.length][word2.length] 就是将两个字符串变成相同所需的最少删除次数。

 * 用动态规划,逐步计算每个前缀变成相同的最少删除次数,最终返回全局最优解。
 */
var minDistance = (word1, word2) => {
  let dp = Array.from(new Array(word1.length + 1), () =>
    Array(word2.length + 1).fill(0)
  );
  for (let i = 1; i <= word1.length; i++) {
    dp[i][0] = i;
  }
  for (let j = 1; j <= word2.length; j++) {
    dp[0][j] = j;
  }
  for (let i = 1; i <= word1.length; i++) {
    for (let j = 1; j <= word2.length; j++) {
      if (word1[i - 1] === word2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1];
      } else {
        dp[i][j] = Math.min(
          dp[i - 1][j] + 1,   // 删除 word1[i-1]
          dp[i][j - 1] + 1,  // 删除 word2[j-1]
          dp[i - 1][j - 1] + 2  // 同时删除 word1[i-1] 和 word2[j-1]
        );
      }
    }
  }
  return dp[word1.length][word2.length];
};


  1. 编辑距离
  • 给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
  • 你可以对一个单词进行如下三种操作:插入一个字符、删除一个字符、替换一个字符

  • 思路
  • 1.确定dp数组定义及下标的含义:dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
  • 2.确定递推公式:
  • if (word1[i - 1] == word2[j - 1]) 那么说明不用任何编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][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;
  • 操作三:替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增删加元素。即 dp[i][j] = dp[i - 1][j - 1] + 1;
  • 以上三种情况取最小值,即: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],对word1里的元素全部做删除操作,即:dp[i][0] = i;同理dp[0][j] = j;
  • 4.确定遍历顺序:从左到右,从上到下
  • 5.举例推导dp数组,打印dp数组:
/**
 * 1. dp数组定义
    dp[i][j] 表示:将 word1 的前 i 个字符和 word2 的前 j 个字符变成相同所需的最少操作数。
 * 2. 初始化
    dp[i][0]:把 word1 的前 i 个字符变成空串,需要删除 i 次。
    dp[0][j]:把空串变成 word2 的前 j 个字符,需要插入 j 次。
 * 3. 状态转移
    如果当前字符相同,不需要操作,直接继承左上角的值。
    如果不同,有三种操作,取最小值:
        删除 word1[i-1]:dp[i-1][j] + 1
        插入 word2[j-1]:dp[i][j-1] + 1
        替换 word1[i-1]:dp[i-1][j-1] + 1
 * 4. 返回结果
    dp[word1.length][word2.length] 就是将 word1 转换成 word2 的最少操作数。

用动态规划,逐步计算每个前缀之间的最少编辑距离,最终返回全局最优解。
 */
const minDistance = (word1, word2) => {
    let dp = Array.from(Array(word1.length + 1), () => Array(word2.length+1).fill(0));

    for(let i = 1; i <= word1.length; i++) {
        dp[i][0] = i; 
    }

    for(let j = 1; j <= word2.length; j++) {
        dp[0][j] = j;
    }

    for(let i = 1; i <= word1.length; i++) {
        for(let j = 1; j <= word2.length; j++) {
            if(word1[i-1] === word2[j-1]) {
                dp[i][j] = dp[i-1][j-1];
            } else {
                dp[i][j] = Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + 1);
            }
        }
    }
    
    return dp[word1.length][word2.length];
};



参考&感谢各路大神

posted @ 2025-07-11 09:07  安静的嘶吼  阅读(6)  评论(0)    收藏  举报