leetcode 131. 分割回文串I,II

本文详细介绍了两种解决字符串回文串分割问题的方法:递归和动态规划,并提供了对应的C++实现。在动态规划解决方案中,通过预处理计算回文子串,提高了效率。此外,还讨论了如何找到最小的分割次数,提出了状态转移方程,并给出了相关代码。文章深入探讨了回文串的判断和分割策略,对于理解动态规划和回文串处理具有指导意义。
摘要由CSDN通过智能技术生成

题目I

分割回文串
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。

输入: “aab”
输出:
[
[“aa”,“b”],
[“a”,“a”,“b”]
]

分析

方法1

使用朴素的递归,对于一个长度为N的字符串,其可以切割的地方有N-1处,因此这个时间复杂度为 o ( 2 n − 1 ) o(2^{n-1}) o(2n1)

class Solution {
public:
    vector<vector<string>> res; 
    vector<vector<string>> partition(string s) {
        vector<string> tmp;//用于存储中间结果
        dfs(0, s, tmp);
        return res;
    }
    bool isValid(string s){
        int i=0;
        int j=s.size()-1;
        while(i<j){
            if(s[i]!=s[j]) return false;
            i++;
            j--;
        }
        return true;
    }
    void dfs(int start, string s, vector<string> tmp){
        if(start==s.size()){
            res.push_back(tmp);
            return;
        }
        //选择决策
        for(int i=start;i<s.size();i++){
            string sub = s.substr(start, i-start+1);
            if(isValid(sub)){
                tmp.push_back(sub);
                dfs(i+1, s, tmp);
                tmp.pop_back();
            }
        }
    }
};
方法2 动态规划

验证回文串那里,每一次都得使用“两边夹”的方式验证子串是否是回文子串。于是“用空间换时间”,利用「力扣」第 5 题:最长回文子串 的思路,利用动态规划把结果先算出来,这样就可以以 O(1)的时间复杂度直接得到一个子串是否是回文。

class Solution {
public:
    vector<vector<string>> res; 
    vector<vector<string>> partition(string s) {
        //使用朴素的递归,对于一个长度为N的字符串,其可以切割的地方有N-1处,因此这个时间复杂度为$o(2^(n-1))$
        //使用动态规划先对字符串进行预处理,dp[i][j]表示s[i, j]是否是回文串
        vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
        
        for(int j=0;j<s.size();j++){
            for(int i=0;i<=j;i++){
                if(s[i]==s[j]){
                    if((j-i)<=2) dp[i][j] = true;
                    else if(dp[i+1][j-1]) dp[i][j] = true;
                }
            }
        }
        vector<string> tmp;//用于存储中间结果
        dfs(0, s, tmp, dp);
        return res;
    }
    void dfs(int start, string s, vector<string> tmp, vector<vector<bool>> dp){
        if(start==s.size()){
            res.push_back(tmp);
            return;
        }
        //选择决策
        for(int i=start;i<s.size();i++){
            string sub = s.substr(start, i-start+1);
            if(dp[start][i]){
                tmp.push_back(sub);
                dfs(i+1, s, tmp, dp);
                tmp.pop_back();
            }
        }
    }
};

注意上面的循环:

for(int j=0;j<s.size();j++){
  for(int i=0;i<=j;i++){
      if(s[i]==s[j]){
          if((j-i)<=2) dp[i][j] = true;
          else if(dp[i+1][j-1]) dp[i][j] = true;
      }
  }
}

在这里插入图片描述
为什么s[i][j]依赖于s[i+1][j-1], 其实将右指针放在外循环,本身就是中心扩展的方式,且s[i+1][j-1]先被求出来。
对于动态规划部分也可以写成如下的方式:

//外层循环表示的是当前字串的长度, 且长度从小到大
for(int l=0;l<s.size();l++){
    //内层循环i表示的是子字符串的起点
    for(int i=0;i+l<s.size();i++){
        //计算出终点j
        int j = i + l;
        if(s[i]==s[j]){
            if((j-i)<=2) dp[i][j] = true;
            else if(dp[i+1][j-1]) dp[i][j] = true;
        }
    }
}

题目II

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回符合要求的最少分割次数。
示例:
输入: “aab”
输出: 1
解释: 进行一次分割就可将 s 分割成 [“aa”,“b”] 这样两个回文子串。

分析

求出最小的分割次数。定义第二个dp数组f[i],f[i]表示s[0,i]的最小分割数目。

  • 如果 s[0:i] 本身就是一个回文串,那么不用分割,即 f[i] = 0
  • 如果 s[0:i] 不是一个回文串则需要去遍历;接下来枚举可能分割的位置:即如果 s[0:i] 不是一个回文串,就尝试分割,枚举分割的边界 j。如果 s[j + 1, i] 不是回文串,尝试下一个分割边界。如果 s[j + 1, i] 是回文串,则 f[i] 就是在 f[j] 的基础上多一个分割。于是枚举 j 所有可能的位置,取所有 f[j] 中最小的再加 1 ,就是 f[i]。
    得到状态转移方程如下:

    f[i] = min([f[j] + 1 for j in range(i) if s[j + 1, i] 是回文])

由上面的分析可知,上面的问题还牵扯到子问题判断s[j + 1, i]是否回文串,因此为了避免在求解f[i]的时候还要再单独去判断s[j + 1, i]是否是回文串,那么就需要把回文串的信息也存储下来。

class Solution {
public:
    void partition(string s, vector<vector<bool> >& dp) {
        //外层循环表示的是当前字串的长度, 且长度从小到大
        for(int l=0;l<s.size();l++){
            //内层循环i表示的是子字符串的起点
            for(int i=0;i+l<s.size();i++){
                //计算出终点j
                int j = i + l;
                if(s[i]==s[j]){
                    if((j-i)<=2) dp[i][j] = true;
                    else if(dp[i+1][j-1]) dp[i][j] = true;
                }
            }
        }
    }
    int minCut(string s) {
        vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
        partition(s, dp);
        vector<int> f(s.size(), INT_MAX);
        //注意下面的写法是错误的,要做相应的修改, 本题作为一个动态规划问题,状态转移方程比较好写,但是对于特殊情况的处理就比较麻烦
        //主要考虑以0作为开始,j作为结束的字符串,如果s[0, j]是一个回文的话,那么就将f[j]赋值为0
        // for(int i=0;i<s.size();i++){
        //     //内层循环i表示的是子字符串的起点
        //     for(int j=0;j<i;j++){
        //         if(dp[j][i]){
        //             f[i] = min(f[i], f[j]+1);
        //         }
        //     }
        // }
        
        //当i等于0的时候,f[i]==0
        f[0] = 0;
        for(int i=1;i<s.size();i++){
            if(dp[0][i]){
                f[i] = 0;
                continue;
            }
            //内层循环i表示的是子字符串的起点
            for(int j=0;j<i;j++){
                if(dp[j+1][i]){
                    f[i] = min(f[i], f[j]+1);
                }
            }
        }
        return f[s.size()-1];

    }
};



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值