回文串问题一网打尽

Posted 一个山里的少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了回文串问题一网打尽相关的知识,希望对你有一定的参考价值。

目录

                                                              最长回文子串

                                                               验证回文串

                                                              验证回文串II

                                                              回文字串


对应letecode链接:

力扣

 题目描述:

给你一个字符串 s,找到 s 中最长的回文子串。

示例 1:

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:

输入:s = "cbbd"
输出:"bb"
示例 3:

输入:s = "a"
输出:"a"
示例 4:

输入:s = "ac"
输出:"a"

提示:

1 <= s.length <= 1000
s 仅由数字和英文字母(大写和/或小写)组成

 💖思考方式:1.中心扩散法

根据回文子串的定义:从左往右看和从右往左看都是一样的这让我们很容易就想到了中心扩散法。中心扩散法的基本思想是:遍历每一个下标,以这个下标为中心,利用「回文串」中心对称的特点,往两边扩散,看最多能扩散多远。

 注意:

细节:回文串在长度为奇数和偶数的时候,「回文中心」的形态不一样:

奇数回文串的「中心」是一个具体的字符,例如:回文串 "aba" 的中心是字符 "b";
偶数回文串的「中心」是位于中间的两个字符的「空隙」,例如:回文串 "abba" 的中心是两个 "b",也可以看成两个 "b" 中间的空隙。

我们看一下一个字符串可能的回文子串的中心在哪里?

因此我们可以设计一个方法,兼容以上两种情况:

如果传入重合的下标,进行中心扩散,此时得到的回文子串的长度是奇数;
如果传入相邻的下标,进行中心扩散,此时得到的回文子串的长度是偶数。

string longestPalindrome(string s) 
             int n=s.size();
             if(n==0||n==1)return s;//一个字符直接返回
             for(int i=1;i<n;i++)
                  help(s,i,i,n);//单个字母为中心
                  help(s,i-1,i,n);//两个字母为中心
             
             return s.substr(ans[0],ans[1]-ans[0]);//返回最长回文字串的长度
    

向两边扩散:

 

对应代码:

void help(string&s,int start,int end,int n)
                //向两边扩散
                while(start>=0&&end<=n-1)
                    if(s[start]==s[end])
                        end++;
                        start--;
                    
                    else
                        break;
                    
                
          if(end-(start+1)>(ans[1]-ans[0]))
              ans[0]=start+1;
              ans[1]=end;
          

    

可能会有老铁会说为什么是start+1了?别急博主马上解释:

 此时当扩散结束之后最长的回文字串是bb而我们start的位置刚好是最长回文字串起始位置的前一个位置所以我们需要将start+1,而最长回文字串的长度刚好是end-start+1.

代码汇总:

class Solution 
public:
              int ans[2]=0;
    string longestPalindrome(string s) 
             int n=s.size();
             if(n==0||n==1)return s;//一个字符直接访问
             for(int i=1;i<n;i++)
                  help(s,i,i,n);//一个字母为中心向两边扩散
                  help(s,i-1,i,n);//两个字母为中心向两步扩散
             
             return s.substr(ans[0],ans[1]-ans[0]);
    
    void help(string&s,int start,int end,int n)
                //向两边扩散
                while(start>=0&&end<=n-1)
                    if(s[start]==s[end])
                        end++;
                        start--;
                    
                    else
                        break;
                    
                
          if(end-(start+1)>(ans[1]-ans[0]))
              ans[0]=start+1;
              ans[1]=end;
          

    
;

方法2动态规划:

思路:定义dp[i][j]表示子串i~j是否是回文子串,循环s的子串,看是否满足s[i],s[j]相等,如果相等,则dp[i][j]是否为回文串取决于dp[i+1][j-1]是否也是回文子串,在循环的过程中不断更新最大回文子串的长度,注意子串的长度是0或1也算回文子串
复杂度:时间复杂度O(n^2),两层循环。空间复杂度O(n^2),即动态规划dp数组的空间。

 

由于单个字符它一定是回文字符串也就是i等于j的时候,由于dp[i][j]代表的是[i,j]是否有回文字串所以i是要小于j的所以只需要填对角线下方的空格所以i的范围是[0,len-2],j的范围是[1,len-1]

对应代码:

class Solution 
public:
    string longestPalindrome(string s) 
            int len=s.size();
            if(len<=1)return s;
          bool dp[1001][1001]=false;
          for(int i=0;i<len;i++)
              dp[i][i]=true;//单个字母一定是true;
          
          int start=0;
          int maxlen=1;
          for(int j=1;j<len;j++)
              for(int i=0;i<len-1&&i<j;i++)
                  if(s[i]!=s[j])
                      dp[i][j]=false;//不是回文串
                  
                  else

                      if(j-i<3)//这种情况必然为true;
                          dp[i][j]=true;
                      

                      else
                          dp[i][j]=dp[i+1][j-1];//状态转移方程
                      

                  

                  if(dp[i][j]&&j-i+1>maxlen)//获取最长的回文字串
                    maxlen=j-i+1;
                    start=i;
                

              
                
          
          return s.substr(start,maxlen);
    
;

下面博主来解释一个为什么j-i<3他必然是true;

2.验证回文串

对应letecode链接:力扣

题目描述:

给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。

说明:本题中,我们将空字符串定义为有效的回文串。

示例 1:

输入: "A man, a plan, a canal: Panama"
输出: true
解释:"amanaplanacanalpanama" 是回文串
示例 2:

输入: "race a car"
输出: false
解释:"raceacar" 不是回文串

提示:

1 <= s.length <= 2 * 105
字符串 s 由 ASCII 字符组成

解题思路:

我们可以定义一个string 把所有的字母全都放进去并且全部将其变成小写,在将其翻转,在和遍历得到的字符串比较是否相等

 对应代码:

class Solution 
public:
    bool isPalindrome(string s) 
     string t;
     for(auto tmp:s)
     
         if(isalnum(tmp))//判断是否是字母
         
             t+=tolower(tmp);
         
     
     string w(t.rbegin(),t.rend());
     return w==t;
    
; 

方法二:双指针

们直接在原字符串 s 上使用双指针。在移动任意一个指针时,需要不断地向另一指针的方向移动,直到遇到一个字母或数字字符,或者两指针重合为止。也就是说,我们每次将指针移到下一个字母字符或数字字符,再判断这两个指针指向的字符是否相同。

对应代码:

class Solution 
public:
    bool isPalindrome(string s) 
           int left=0;
           int right=s.size()-1;
           while(left<right)
               while(left<right&&!isalnum(s[left])) left++;//从左往右找字符
               while(left<right&&!isalnum(s[right]))right--;//从右往左找字母
               if(left<right)//如果没有错开就判断
                   if(tolower(s[left])!=tolower(s[right]))return false;//全部转换成小写
               
               left++;
               right--;
           
           return  true;
    
;

验证回文串II

对应letecode链接:

力扣

题目描述:

给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。

示例 1:

输入: s = "aba"
输出: true
示例 2:

输入: s = "abca"
输出: true
解释: 你可以删除c字符。
示例 3:

输入: s = "abc"
输出: false

提示:

1 <= s.length <= 105
s 由小写英文字母组成

解题思路:

首先判断原串是否是回文串,如果是,就返回 \\texttruetrue;如果不是,则枚举每一个位置作为被删除的位置,再判断剩下的字符串是否是回文串。这种做法的渐进时间复杂度是 O(n^2)O(n 
2
 ) 的,会超出时间限制。

我们换一种想法。首先考虑如果不允许删除字符,如何判断一个字符串是否是回文串。常见的做法是使用双指针。定义左右指针,初始时分别指向字符串的第一个字符和最后一个字符,每次判断左右指针指向的字符是否相同,如果不相同,则不是回文串;如果相同,则将左右指针都往中间移动一位,直到左右指针相遇,则字符串是回文串。

在允许最多删除一个字符的情况下,同样可以使用双指针,通过贪心实现。初始化两个指针 low 和 high 分别指向字符串的第一个字符和最后一个字符。每次判断两个指针指向的字符是否相同,如果相同,则更新指针,将 low 加 1,high 减 1,然后判断更新后的指针范围内的子串是否是回文字符串。如果两个指针指向的字符不同,则两个字符中必须有一个被删除,此时我们就分成两种情况:即删除左指针对应的字符,留下子串 s[low+1:high],或者删除右指针对应的字符,留下子串 s[low:high−1]。当这两个子串中至少有一个是回文串时,就说明原始字符串删除一个字符之后就以成为回文串

 对应代码:

class Solution 
public:
    bool validPalindrome(string s) 
            int low=0;
            int high=s.size()-1;
            while(low<high)
                if(s[low]==s[high])
                    low++;
                    high--;
                
                else
            return ispalindrome(s,low+1,high)||ispalindrome(s,low,high-1);//给他一次机会看是否能是回文
                
            
            return true;
    
    bool ispalindrome(string&s,int left,int right)
              while(left<right)
                  if(s[left]!=s[right])//不是回文返回false;
                      return false;
                  
                  left++;
                  right--;
              
              return true;
    
;

 2.考虑最多可以删除k个:

实际上这个作为面试题还算比较水的,可能面试官以这题为基础要求你最多删k个的情况下,判断是否能修改字符串为回文串。因此我决定写一下如何处理k个的情况。实际上,这种题目字符串题目很容易归类为动态规划问题。我们可以把这题转述为:将字符串s处理为字符串t,在不能进行替换的情况下最少需要进行多少次操作。然后判断操作次数是否在允许范围内

这题就转换为从字符串s转换为字符串t所需要的最小次数:

dp[i][j], 代表s[:i-1] == t[:j-1]所需要的编辑次数
dp[i][j],代表s[:i−1]==t[:j−1]所需要的编辑次数

 那么这题就变成了一个很经典的动态规划问题

dp[i][j]= dp[i−1][j−1]
            min(dp[i][j],dp[i−1][j]+1],dp[i][j−1]+1)
if(s[i−1]==t[j−1])
与上者并不是if,else关系

其中当s[i - 1] == t[j - 1]时,我们只需要看s[:i-2]s[:i−2]和t[:j-2]t[:j−2]的匹配情况

 class Solution 
public:
    bool k_validPalindrome(string s, int k)
        int n = s.size();
        vector<vector<int>> dp(2, vector<int>(n + 1, 0x3f3f3f3f)); // 只需要用前后两行就好,0x3f3f3f3f拟定的无穷大,足够大也不容易溢出

        for (int i = 0; i <= n; ++ i) dp[0][i] = i; // 初始化,显然将t[:i-1]修改为空串需要i次操作
        
        for (int i = 1; i <= n; ++ i)
            dp[i & 1][0] = i; // 类似上述的初始化,只是在这里才赋值
            int from = max(1, i - k), to = min(n, i + k); // k是可以修改的次数,我们可以往前或者往后修改
            for (int j = from; j <= to; ++ j)
                if (s[i - 1] == s[n - j]) dp[i & 1][j] = dp[(i - 1) & 1][j - 1]; // 参看图中i = c, j = c
                dp[i & 1][j] = min(dp[i & 1][j], dp[(i - 1) & 1][j] + 1, dp[i & 1][j - 1] + 1); // 从i - 1处和j - 1处转移
            
        
        return dp[n & 1][n] <= 2 * k; // 对原字符串的一次修改,反映在两个字符串上是两次。为了让s, t相同删除k代表可以对每个操作k次,共2*k
    
    bool validPalindrome(string s) 
        return k_validPalindrome(s, 1);
    
;

回文字串

对应letcode链接:

https://leetcode-cn.com/problems/palindromic-substrings/

题目描述:

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"
示例 2:

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

提示:

1 <= s.length <= 1000
s 由小写英文字母组成

接题思路:

利用回文串的性质找对称中心由于上面已经讲过了在这里就不重复了

 对应代码:

class Solution 
public:
           int ans=0;//记录结果    
    int countSubstrings(string s) 
        int len=s.size();
       for(int i=0;i<s.length();i++)
       
           Countpalindrome(i,i,len,s);//长度为奇数
           Countpalindrome(i,i+1,len,s);//长度为偶数
       
       return ans;
    
    void Countpalindrome(int start,int end,int len,string &s)
    
        while(start>=0&&end<len&&s[start]==s[end])
        
            ans++;
            start--;
            end++;
        
         
    

;

🙌🙌🙌🙌
结语:对于个人来讲,在leetcode上进行探索以及单人闯关是一件有趣的时间,一个程序员,如果不喜欢编程,那么可能就失去了这份职业的乐趣。刷到我的文章的人,我希望你们可以驻足一小会,忙里偷闲的阅读一下我的文章,可能文章的内容对你来说很简单,(^▽^)不过文章中的每一个字都是我认真专注的见证!希望您看完之后,若是能帮到您,劳烦请您简单动动手指鼓励我,我必回报更大的付出~

以上是关于回文串问题一网打尽的主要内容,如果未能解决你的问题,请参考以下文章

算法竞赛入门经典 例题 3-4 回文串

最长回文字串暴力

CSDN|每日一练最长回文串

CSDN|每日一练最长回文串

添加字符判断是否为回文串

《算法竞赛入门经典》3.3最长回文子串