目录
一、最长回文子字符串(返回长度或者返回子字符串是一样的)
1.1 暴力破解
1.1.1 判断一个字符串是否是回文串
1.2 中心扩展法
1.3 动态规划
二、回文串相关的一些扩展题
2.1 最多删除一个字符,是否能让字符串成为回文串
给你一个字符串 s,找到 s 中最长的回文子串。
示例 :
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
解法包括:暴力破解、中心扩展法、动态规划、最长公共子串、Manacher's Algorithm 马拉车算法
为了增加可读性,让文章显得不那么冗余。这里只给出暴力解法、中心扩展法、动态规划。
列举所有的子串,编写一个函数判断一个字符串是否为回文串,保存最长的回文串。这种方法时间复杂度太高!
只需要同时取字符串的首尾字符,判断是否相等。
若不相等则返回false;
若相等则左下标+1,右下标-1,直到左下标大于等于右下标,返回true。
bool check(string& s){ int len = s.size(); for(int i = 0; i < len/2; i++){ if(s[i] != s[len-1-i]) return false; } return true; }
则暴力解法代码:
class Solution { public: bool check(string& s) { int len = s.size(); for (int i = 0; i < len / 2; i++) { if (s[i] != s[len - 1 - i]) return false; } return true; } string longestPalindrome(string s) { int len = s.size(); if (len <= 1) return s; string res; int maxlen = 1;//最长回文子字符串长度 res = s.substr(0, maxlen); for (int i = 0; i < len; i++) { for (int j = 2; j+i <= len; j++) {//j表示的是截取的子串长度 所以i+j 应该小于等于len;一个字符肯定是回文 所以从2个开始截取 string tmp = s.substr(i, j); if (check(tmp) && j > maxlen) {//tmp.size() res = s.substr(i, j); maxlen = j; } } } return res; } };
上面这样写是一定会超时的,考虑进行优化,也就是每次向后截取的长度至少比上一次最大长度大。
class Solution { public: bool check(string& s) { int len = s.size(); for (int i = 0; i < len / 2; i++) { if (s[i] != s[len - 1 - i]) return false; } return true; } string longestPalindrome(string s) { int len = s.size(); if (len <= 1) return s; string res; int maxlen = 1;//最长回文子字符串长度 res = s.substr(0, maxlen); for (int i = 0; i < len; i++) { for (int j = maxlen + 1; j+i <= len; j++) {//j表示的是截取的子串长度 所以i+j 应该小于等于len;优化每次截取的长度 string tmp = s.substr(i, j); if (check(tmp) && j > maxlen) {//tmp.size() res = s.substr(i, j); maxlen = j; } } } return res; } };
当然,结果仍然会超时的!
中心扩散法怎么去找回文串?
从每一个位置出发,向两边扩散即可。遇到不是回文的时候结束。
举个例子,str=acdbbdaa 我们需要寻找从第一个 b(位置为 3)出发最长回文串为多少。怎么寻找?
首先往左寻找与当期位置相同的字符,直到遇到不相等为止。
然后往右寻找与当期位置相同的字符,直到遇到不相等为止。
最后左右双向扩散,直到左和右不相等。如下图所示:
当然,求解的时候可以再优化。具体见代码注释:
class Solution { public: string longestPalindrome(string s) { int len = s.size(); if(len <= 1) return s; string res; int l = 0, mid = 0, r = 0;//最开始都指向第一个字符 while(mid < len){//因为是中心扩散 所以用mid遍历整个string l = mid; r = mid;//每换一个中心点 都需要更新当前的左右坐标 才能找到当前中心的最长回文串 while(r < len && s[mid] == s[r+1]){//向右扩展 如果紧邻的两字符相等 则可以直接将中心点右移(因为是从左向右遍历) mid++; r++; } while(l > 0 && r < len-1 && s[l-1] == s[r+1]){ //向左扩展,因为l到mid已经满足回文了 这时候需要判断s[l-1]和s[r+1] 如果s[l-1] == s[r+1] 也就是说当前中心点可以向左右同时扩展 l--; r++; } //当前中心点不能再扩展时 记下该中心节点扩展的最长回文子串 if(r - l + 1 > res.size()){ res = s.substr(l, r - l + 1); } mid++;//右移中心节点 } return res; } };
这是最快的一种方法了。
中心扩散的方法,其实做了很多重复计算。动态规划就是为了减少重复计算的问题。动态规划听起来很高大上。其实说白了就是空间换时间,将计算结果暂存起来,避免重复计算。
动态规划三部曲:边界条件、递推关系、dp与所求的关系。
用一个 数组dp[l][r] 表示字符串从l到 r这段是否为回文。
边界条件:l=r 时,此时 dp[l][r]=true,因为一个字符肯定是回文。
递推关系:要判断 dp[l][r] 是否为回文,只需要先判断字符串从l+1到r-1是否为回文串,即dp[l+1][r-1]是否为true。再判断字符串在(l)和(r)两个位置是否为相同的字符即可。
则递推关系:dp[l][r] = dp[l+1][r-1]^(s[l]==s[r])
dp与所求的关系:每当字符串长度更新,就记下最长子字符串。
具体代码如下:
class Solution { public: string longestPalindrome(string s) { int len = s.size(); if(len <= 1) return s; string res; int maxStart = 0; //最长回文串的起点 int maxLen = 1; //最长回文串的长度 vector<vector<int>> dp(len,vector<int>(len, 0)); //由于递推关系是dp[l][r] = dp[l+1][r-1]^(s[l]==s[r]) //也就是从小到大递推 则循环最外层是r for(int r = 1; r < len; r++){ for(int l = 0; l < r; l++){ if(s[l] == s[r] && (r-l <= 2 || dp[l+1][r-1])){//注意这里的与判断后面部分是满足之一即可 所以要括号 dp[l][r] = 1; if(r - l + 1 > maxLen){ maxStart = l; maxLen = r - l + 1; } } } } return res = s.substr(maxStart, maxLen); } };
回文字符串就是正读和反读都一样的字符串,如“viv”、“nexen”、“12321”、“qqq”、“翻身把身翻” 等。
给定一个非空字符串 str,在最多可以删除一个字符的情况下请编程判定其能否成为回文字符串;如果可以则输出首次删除一个字符所能得到的回文字符串,如果不行则输出字符串 "false" 。
输入描述:
一个非空字符串
输出描述:
一个回文字符串,或者 "false" 字符串(如果无法构造出回文字符串的话)输入例子1:
abda输出例子1:
ada例子说明1:
删除字符串"abda"中的一个字符 ‘b’ 后,得到 "ada"是一个回文字符串;删除一个字符 ‘d’ 后,得到 "aba"也是一个回文字符串;所以最终输出为 "ada"。
这道题是vivo的一道真题,但是我发现它原oj出来的答案和题目对不上。所以大概明白题目意思就行。
思路:
使用判断一个字符串是不是回文串的方式,首尾同时对比 遇到不等的 总得删掉一个 分别判断删除其中一个后 剩下的字符串是否是回文串。
#include<bits/stdc++.h> using namespace std; bool check(string& s) { int len = s.size(); for (int i = 0; i < len / 2; i++) { if (s[i] != s[len - i - 1]) return false; } return true; } int main() { //针对这个字符串 首尾同时对比 遇到不等的 总得删掉一个 分别判断删除其中一个后 剩下的字符串是否是回文串 string input; cin >> input; int len = input.size(); string res = input; for (int i = 0; i < len / 2; i++) { if (input[i] != input[len - 1 - i]) { res = "false"; string delete_right = input.substr(i, len - 1 - i - i); string delete_left = input.substr(i + 1, len - 1 - i - i); if (check(delete_left)) { input.erase(input.begin() + i); res = input; /*if (i == 0) res = input.substr(i + 1, len - 1 - i); else res = input.substr(0, i) + input.substr(i + 1, len - 1 - i);*/ break; } else if (check(delete_right)) { input.erase(input.begin() + len - 1 - i); res = input; /* if (i == 0) res = input.substr(0, len - 1 - i); else res = input.substr(0, len - 1 - i) + input.substr(len - i, i);*/ break; } else break; } } cout << res << endl; }