动规之字符串的应用【力扣】

预备知识

回文串就是正反读着都一样,就比如:“习习羊习习(我的一个算法高手朋友)

  • tips:本文部分概述并不精确。例如,s[0, j] 意思为 s 的 (0,j - 1)子串。//不包含s[j]

5. 最长回文子串

在这里插入图片描述

我们观察回文子串,假设(i,j)子串为回文,那么(i + 1,j - 1)子串回文(或者 j - i <= 2,ep:“a”, “aa”,“bab”)
这也表明,是否是回文串也可以从:他的缩略是否回文 且 左右边界相等 来推导是否回文,即: s[i] == s[j] && dp[i + 1][j - 1]

  • 边界情况:每单个字符均为长度为1 的子串

  • 注意,我们是由短子串推向长子串,这样才符合推导逻辑,所以遍历顺序应该为顺序

 public String longestPalindrome(String s) {
     if (s.length() < 2){
         return s;
     }
     int size = s.length();
     int maxlen = 1;//最长长度暂定为1(单字符)
     int begin = 0;//记录最长会问子串的起始下标
     boolean[][] dp = new boolean[size][size];
     //每一个只含单字符的子串均为回文子串
     for (int i = 0; i < size; ++i) {
         dp[i][i] = true;
     }
     //先枚举子串长度,从2开始
     for (int L = 2; L <= size; ++L) {
         //定义左边界
         for (int i = 0; i < size; ++i) {
             //表示右边界
             int j = L + i - 1;
             //一旦发生数组越界,break;
             if(j >= size){
                 break;
             }
             //状态转移过程
             if (s.charAt(i) != s.charAt(j)){
                 dp[i][j] = false;
             }else{
                 if(j - i < 3){  //加快判断,如果 j - i < 3,则一定回文 
                     dp[i][j] = true;
                 }else{
                     dp[i][j] = dp[i + 1][j - 1];//和dp[i + 1][j - 1] 保持一致
                 }
             }
             if (dp[i][j] && j - i + 1 > maxlen){    //如果是回文串且大于记录的最大长度,则进行更新
                 maxlen = j - i + 1;
                 begin = i;
             }
         }
     }
     return s.substring(begin, begin + maxlen);
 }

647. 回文子串

在这里插入图片描述

又是回文子串问题,有了刚刚的推导,相信你能够很自信的做出的这道题目,题目中只有一个字符串,我们可定义dp[i][j]表示,s中(i, j)的答案,即回文子串的数目。 吗?
如果这样推导,状态的转移并不好想,所以我们可以dp[i][j]表示为(i, j)是否为回文子串,如果是,我们让计数变量++。
是否为回文串的状态转移可就好想多了,思考也会更加简便。

    public int countSubstrings(String s) {
        int n = s.length();
        boolean[][] dp = new boolean[n][n];
        //用于判断(i, j)是否为回文字符串
        int res = 0;
        for (int i = n - 1; i >= 0; --i) {
            for (int j = i; j < n; ++j) {
                if (s.charAt(i) == s.charAt(j) && (i == j || i == j - 1 || dp[i + 1][j - 1])){
                    dp[i][j] = true;
                    ++res;
                }
            }
        }
        return res;
    }

139. 单词拆分

在这里插入图片描述

该问题明显为完全背包问题,我们可选择的物品就是字符串列表wordDict,要去填充填满这个字符串 s
我们定义dp[i] 为字符串(0, i )能否被字典所拼接,也可以被分解成,
是否存在某个j, 使得(0,j)子串可被拼接,且(j, i)子串也可以被拼接或者存在于字典中0 < j < i

  • 边界问题,dp[0],没有东西,当然能被拼接,所以dp[0] = true;
 public boolean wordBreak(String s, List<String> wordDict) {
     HashSet<String> set = new HashSet<>(wordDict);
     boolean[] dp = new boolean[s.length() + 1];
     dp[0] = true;
     for (int i = 0; i <= s.length(); ++i) {
         for (int j = 0; j < i; ++j) {
             if (dp[j] && set.contains(s.substring(j, i))){
                 dp[i] = true;
                 break;
             }
         }
     }
     return dp[s.length()];
 }

516. 最长回文子序列

在这里插入图片描述

  • 首先,让我们区分子串和子序列,子串是连续的,子序列不是,但都满足从前往后的顺序。

定义:dp[i][j]表示s[i…j]代表的字符串的最长回文子序列
为什么这样定义,因为回文子序列不可能说光从0开始,并且我们需要确定左右边界来确定,而且这样定义,我们也可以通过已知数据去推导下一个dp数组值。

s[i, j]的最长回文子序列,很明显和两个东西有关:
①s【i,j】的子串的最长回文子序列,②s[i] == s[j] ?

  • 如果s[i] == s[j],那么很明显s【i,j】的最长回文子序列一定是s【i + 1, j - 1】子串最长回文子序列 在加上这两个字符:dp[i][j] = dp[i + 1][j - 1] + 2;
  • s[i] != s[j] 呢?此时最两边的字符已经抱不上回文的希望了,只能找子串的最大的回文子序列了,哪个子串的最长回文子序列最长呢?那肯定是最长子串的要有更长的可能,所以此时dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
  • 边界问题的处理:由于每个单字符都是回文子序列,所以dp[i][i] = 1

其实也有另一种巧妙地思想,但是会结合公共子序列的问题。就是:
最长回文子序列即: 字符串 str 与其逆序字符串所构成的最长公共子序列

 public int longestPalindromeSubseq(String s) {
     if (s.length() <= 1){
         return s.length();
     }
     int[][] dp = new int[s.length()][s.length()];
     //dp[i][j] 表示从i到j的最长回文子序列长度
     //dp[i][i] = 1 ,每个单个字符都是回文子序列
     for (int i = 0; i < dp.length; ++i) {
         dp[i][i] = 1;
     }
     //如果边界相等,则等于dp[i + 1][j - 1] + 2
     //否则 找两边的最大值
     for (int i = dp.length - 1; i >= 0; --i) {
         //需要逆序,因为要从短的子序列到长的子序列
         for (int j = i + 1; j < dp.length; ++j) {
             if (s.charAt(i) == s.charAt(j)){
                 dp[i][j] = dp[i + 1][j - 1] + 2;
             }else{
                 dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
             }
         }
     }
     return dp[0][dp.length - 1];
 }

72.编辑距离

在这里插入图片描述

有了之前的经验,我相信你能轻松想到:
定义 dp[i][j] 表示 word[0,i] 转换成 word[0, j]的最少操作数

  • 我们在递推的过程中,要有一种已经推出其子串的思想,我们循序渐进的进一步转换罢了。

  • 如果 word1[i] == word2[j] 说明这两个字符根本不需要任何转换,在dp[i - 1][j - 1]中,我们已经得出用word1[0, i - 1],word2[0, j - 1]的所需操作数,word1[i] == word2[j]说明这一步我们不需要任何操作,所以 dp[i][j] = dp[i - 1][j - 1];

  • 那么如果 word1[i] != word2[j] 那么说明一定需要 增删改 的任一操作,我们选其中的最小操作数即可.那么很好想:

      1. 增:`dp[i][j] = dp[i][j - 1] + 1`		(多一个 插入word2[j] 的步骤)
      2. 删:`dp[i][j] = dp[i - 1][j] + 1`	    (多一个删除word1[i]的操作)
      3. 改:`dp[i][j] = dp[i - 1][j - 1] + 1` 
      (此处难理解,其实就是已知从word1[i - 1] 到word2[j - 1]的操作数,再加上一个操作:把word1[i]改成word2[j] )
    
  • 综合上述,即略写为:dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1]));

  • 边界问题处理:一个子串为0,要转换成另一个子串 or 该零子串被转换的结果,均为该子串的长度(插入 length 个字符转换而成 or 删除 length 个字符转换而成)

class Solution {
    public int minDistance(String s1, String s2) {
        int len1 = s1.length(), len2 = s2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];
        //dp[i][j]表示s1[0,i] 和 s2[0,j] 最少操作数
        //初始化,边界情况为另一个子串的长度
        for (int i = 0; i <= len1; i++) {
            dp[i][0] = i;
        }
        for (int i = 0; i <= len2; i++) {
            dp[0][i] = i;
        }
        for (int i = 1; i <= len1; ++i) {
            for (int j = 1; j <= len2; ++j) {
                if (s1.charAt(i - 1) == s2.charAt(j - 1)){
                    dp[i][j] = dp[i - 1][j - 1];
                }else {
                    dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1]));
                }
            }
        }
        return  dp[len1][len2];
    }
}

712. 两个字符串的最小ASCII删除和

在这里插入图片描述

先说一个知识点 ,s.codePointAt(int idx) 返回 s 的第 idx - 1 位字符的Unicode码。

  • 乍一看,跟上个题一毛一样,没啥说的,边界处理也一模一样,只不过步骤改成了Ascii码而已。直接摆上代码:
public int minimumDeleteSum(String s1, String s2) {
    int[][] dp = new int[s1.length() + 1][s2.length() + 1];
    //特殊情况处理,如果i = 0 或者 j = 0时,那么这个值就是另一个子串的所有ASCII码值
    for (int i = 1; i <= s1.length(); ++i) {
        dp[i][0] = dp[i - 1][0] + s1.codePointAt(i - 1);
    }
    for (int i = 1; i <= s2.length(); ++i) {
        dp[0][i] = dp[0][i - 1] + s2.codePointAt(i - 1);
    }

    for (int i = 1; i <= s1.length(); ++i) {
        for (int j = 1; j <= s2.length(); ++j) {
            if(s1.charAt(i - 1) == s2.charAt(j - 1)){   //相等,不加东西,继承上一个的和
                dp[i][j] = dp[i - 1][j - 1];
            }else{
                //此时三种情况,要i不要j,要j不要i,或者都不要,看哪个小
                dp[i][j] = Math.min(dp[i - 1][j] + s1.codePointAt(i - 1), dp[i][j - 1] + s2.codePointAt(j - 1));
            }
        }
    }
    return dp[s1.length()][s2.length()];
}

115. 不同的子序列

在这里插入图片描述

定义dp[i][j] 表示s的前i个子序列中,t的(0,j)子串出现的个数

  • 如果s[i] == t[j] ,那么我们可以将求dp[i][j] 分解成两个集合,①让s[i]跟它匹配,dp[i - 1][j - 1] ②不让s[i]跟他匹配,即 dp[i - 1][j],且②的子序列中,均不包含 s[i]…可得 dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];

  • 如果 s[i] != t[j] , 那么只能让 s 前边的子序列去匹配,即:dp[i][j] = dp[i - 1][j]

  • 边界问题:s 为空时,任意 t 均无法寻找到子序列; t 为空时,任意 s 均可找到一条 t 的子序列。

public int numDistinct(String s, String t) {
    if (s.length() == 0 || t.length() ==0){
        return 0;
    }
    int[][] dp = new int[s.length() + 1][t.length() + 1];
    //dp[i][j] 表示s的前i个子序列中,t的(0,j)子串出现的个数
    //边界问题:s为0,t长度任意,也找不到子序列。所以dp[0][i]均为0
    //s任意,t为0,则可在s中找到t的子序列,为1, 故dp[i][0] = 1
    for (int i = 0; i <= s.length(); ++i) {
        dp[i][0] = 1;
    }
    for (int i = 1; i <= s.length(); ++i) {
        for (int j = 1; j <= t.length(); ++j) {
            if(j > i){
                //如果j>i, 子序列无意义,直接跳过
                continue;
            }
            if (s.charAt(i - 1) == t.charAt(j - 1)){
                //如果相等,那么有两个情况,以i位结尾匹配,不以i位结尾匹配,
                //①以i位结尾匹配,因为相等,所以肯定=dp[i - 1][j - 1[
                //②不以i位结尾匹配,那肯定和上一个的状态有关,因为都是推来的,
                // 那么我们就要在i - 1中,找j的子序列的个数,即dp[i - 1][j]
                dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
            }else {
                //此时最后一位不相等,所以答案肯定是s(0,i - 1)中能匹配t(0,j)的个数,因为s的i位不是t的最后一位,指望不上。
                dp[i][j] = dp[i - 1][j];
            }
        }
    }
    return dp[s.length()][t.length()];
}

总结:

  • 其实我感觉有些题目需要先思考边界问题,会更有利于问题思路的思考,但本文每道题目都把边界问题的思考都放到了最后,个人认为边界问题是一个问题很关键的部分,尤其在动态规划,边界问题的初始化,对整个dp数组的赋值很有必要

  • 在构造dp数组这方面, 通常如果题目中只出现一个字符串,我们通常定义dp[i][j]表示 s 中(i, j) 子串的答案 如果题目出现了两个字符串,我们通常定义dp[i][j], 表示s1的(0, i)和 s2的(0, j) 两个子串所对应的答案。 这样定义也有助于我们思考并从前往后推导,这也点明了我们的解题入手点,毕竟正常思路就是如此。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xoliu1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值