回文字符串

回文basic

  1. 从前往后看等于从后往前看
  2. 长度可为奇数,也可以是偶数
  3. 单个字符可以是回文,长度为1
  4. 可以考虑的方法:暴力破解、dp、中心扩散、Manacher、KMP、双指针
  5. 子串:原字符串在前后删除一些字符(可以可不删)得到的字符串
  6. 子序列:原字符串删除掉一些字符(任意位置,也可以不删)后得到的字符串

验证是否是一个回文

#include<iostream>
#include<string>
using namespace std;
//有效的回文
bool isPalindrome1(string s) {
    string sgood;
    for (char ch : s) {
        if (isalnum(ch)) { //只考虑英文字母大小写和数字,忽略其它ascii字符
            sgood += tolower(ch);
        }
    }
    string sgood_rev(sgood.rbegin(), sgood.rend()); 
    return sgood == sgood_rev;
}
//也可以用双指针
bool isPalindrome2(string s) {
    int i = 0; int j = s.size() - 1;
    while (i <= j) {
        while (!isalnum(s[i])) i++;
        while (!isalnum(s[j])) j--;
        if (s[i] != s[j]) {
            return false;
        }
        else {
            i++;
            j--;
        }

    }
    return true;

}

int main() {
    string s = "asdf&&%*fbdsa";
    cout << isPalindrome1(s) << endl;
    cout << isPalindrome2(s) << endl;
	return 0;
}

统计字符串中有效的回文子串数量

  • 字符串中有效的回文子串数量,假设输入为已经处理过的字符串,即只有小写字母
    • 中心扩散,可以遍历每一个可能是回文中心的位置,然后向外扩散,直到两边字符不相等就停止扩散
    • 但是要注意子串长度,可能是奇数也可能是偶数,需要分别考虑
#include<iostream>
#include<string>
#include<vector>
#include<limits.h>
using namespace std;

//字符串中有效的回文子串数量 输入为已经处理过的字符串
//遍历每一个可能是回文中心的位置,然后向外扩散,直到两边字符不相等就停止扩散
int countSubstrings(string s) {
    int n = s.size(), ans = 0;
    //奇偶位置一次性处理
    /*
    for (int i = 0; i < 2 * n - 1; ++i) {
        int l = i / 2, r = i / 2 + i % 2;
        while (l >= 0 && r < n && s[l] == s[r]) {
            --l;
            ++r;
            ++ans;
        }
    }
    */
    //奇偶位置处理
    //子串奇数长 中心为s[i]
    
    for (int i = 0; i < n; i++) {
        int l = i, r = i;
        while (l >= 0 && r < n && s[l] == s[r]) {
            --l;
            ++r;
            ++ans;
        }
    }
    //子串偶数数长 中心为s[i],s[i+1]
    for (int i = 0; i < n; i++) {
        int l = i, r = i+1;
        while (l >= 0 && r < n && s[l] == s[r]) {
            --l;
            ++r;
            ++ans;
        }
    }
    return ans;
}

int main() {
    cout << countSubstrings("asajfdsfigghhjhhgg") << endl;
}

最短回文字符串

解释:在一个字符串前添加一段最短字符串使得原字符串变成一个回文字符串

coding...

最长回文子序列

  • 最长回文子序列,给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度;这里子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
    • dp[i][j]表示字符串下标为[i,j]内的最长子字符串
    • 初始化:0<=i<=j<n 时,dp[i][j]才有值,否则为0;任何单个字符都是回文,所以 0<=i<n 时,dp[i][i]=1
    • i<j时,考虑s[i]是否和s[j]相等的两种情况
      • s[i]==s[j]:在得到[i+1,j-1]之间的条件下,又在两边各加了一个字符,所以长度+2

      dp[i][j]=dp[i+1][j−1]+2

      • s[i]!=s[j]😒[i]和s[j]不可能同时作为一个回文字符串的首尾,所以:

      dp[i][j] = max(dp[i+1][j], dp[i][j-1])

    • 状态转移都是从长度较短转移到较长,即中心扩散,所以注意循环顺序

      i从n-1遍历到0
      j从i+1,遍历到n-1

    • dp[0][n-1]即为最长的回文子字符串
#include<iostream>
#include<string>
#include<vector>
#include<limits.h>
using namespace std;

int longestPalindromeSubseq(string s) {
    int n = s.length();
    vector<vector<int>> dp(n, vector<int>(n));
    for (int i = n - 1; i >= 0; i--) {
        dp[i][i] = 1;
        char c1 = s[i];
        for (int j = i + 1; j < n; j++) {
            char c2 = s[j];
            if (c1 == c2) {
                dp[i][j] = dp[i + 1][j - 1] + 2;
            }
            else {
                dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[0][n - 1];
}

int main() {
    cout << longestPalindromeSubseq("asajfdsfigghhjhhgg") << endl;
    return 0;
}

最长回文子串

动态规划

  1. 确定dp含义:dp[i][j]为字符串[i,j]是不是一个连续回文字子符串
  2. 确定递推公式:
    - 如果 ij 那么dp[i][j] =true;
    - 如果 j-i = 1 那么dp[i][j] =(s[i] == s[j]);
    - 其它 dp[i][j] = (dp[i+1][j-1] && s[i]
    s[j])
  3. 确定遍历顺序。
```
int maxLen = 1;
for(int i=0; i < s.size(); i++){
    for(int j=0;ij<=i;j++){
        //递推公式
        if(dp[i][j] && i-j+1>0) maxLen = max(maxLen,i-j+1);
    }
}
```
  1. 总体代码
#include<iostream>
#include<string>
#include<vector>
#include<limits.h>
using namespace std;

int max_conc_plaindrome_dp(string s) {
	//dp[i][j]表示[i,j]子字符串是不是一个回文字符串
	vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
	int n = s.size();
	int maxLen = 1;
	for (int i = 0; i < n; i++) {
		for (int j = 0; j <= i; j++) {
			if (i == j) dp[j][i] = true; //奇数长度字子符串
			else if (i - j == 1) dp[j][i] = (s[i] == s[j]);//偶数长度字子符串
			else dp[j][i] = (s[i] == s[j] && dp[j + 1][i - 1]);//只有中间也是回文串,且两边两字符相等,才能是一个新的回文串 p.s.中间串的值上一轮已经确定过了
			if (dp[j][i] && i - j + 1 > maxLen) maxLen = i - j + 1;

		}
	}
	return maxLen;
}

int main() {
	
	string s;
	while (cin >> s) {
		cout << max_conc_plaindrome_dp(s) << endl;
	}

	return 0;
}

中心扩散

注意:奇偶分别考虑

#include<iostream>
#include<string>
#include<vector>
#include<limits.h>
using namespace std;

int zxks_getmaxLen(string& s, int l, int r) {
	//看看能扩散的最大程度
	while (l >= 0 && r < s.size() && s[l] == s[r]) {
		l--;
		r++;
	}
	return r - l - 1;//因为上次相等之后就把长度加了2,所以需要减掉2
}

int max_conc_plaindrome_zxks(string s) {
	//中心扩散,回文长度分奇偶情况
	if (s.size() == 0) return 0;
	if (s.size() == 1) return 1;
	int maxLen = 1;
	for (int i = 0; i < s.size(); i++) {
		int m1 = zxks_getmaxLen(s, i, i);
		maxLen = max(m1, maxLen);
	}
	for (int i = 0; i < s.size()-1; i++) {
		int m2 = zxks_getmaxLen(s, i, i + 1);
		maxLen = max(m2, maxLen);
	}

	return maxLen;
}
int main() {
	string s;
	while (cin >> s) {
		cout << max_conc_plaindrome_zxks(s) << endl;
	}

	return 0;
}

Manacher

参考:牛客网题解

  • manacher算法
  • 可以肯定的是子字符串有奇数串和偶数串两种情况,有什么办法能让他们一起考虑呢?
  • 加入一些字符,如abaaccd - > $#a#b#a#a#c#c#d^ , 为什么这样加?
  • 可以不用考虑边界
  • 动态规划,dp[i]的含义是,以s[i]为中心的最大回文字符串的 半径长度, i=2 dp[i]=2 i=4 dp[4]=3
  • 如果之前有个回文串的长度是 len, 那么变换完之后,对应回文串的半径长为 len+1 ,也就是说新串的回文串均为奇数
  • 动态规划:
    • dp[i]含义如上,以s[i]为中心的最大回文字符串的 半径长度
    • 初始化:全0
    • 转移方程:
      中心i从1遍历到s.size(),维护一个最靠右的有边界的字符串,右边界(不取到)设为mr; 2*mid-i
      如果 i < mr, dp[i] = min(mr-i,dp[2*mid-i])
      如果 i >= mr, dp[i] = 1 超过了上一个字符串的边界
      
      扩张半径+维护边界,但不需要判断字符串边界,因为通配符的$^原因
      while(s[i-dp[i]] == s[i+dp[i]]) dp[i]++;
      if(i+dp[i]>mr){
      	mr = i+dp[i]l;
      	mid = i;
      }
      
    • 顺序遍历
#include<iostream>
#include<string>
#include<vector>
#include<limits.h>
using namespace std;


string init(string& s) {
    string res = "";
    res += "$#";
    for (int i = 0; i < s.size(); i++) res += s[i], res += '#';
    res += '^';
    return res;
    // 这个是在开始和结束加上通配符, 然后我们中间每个分割的地方加上#
}

void manacher(vector<int>& p, string& s) {
    int mr = 0, mid;
    // mr代表以mid为中心的最长回文的有边界
    // mid为i和j的中心点, i以mid为对称点翻折到j的位置
    for (int i = 1; i < s.size(); i++) {
        if (i < mr)
            p[i] = min(p[mid * 2 - i], mr - i);
        // 2 * mid - i为i关于mid的对称点
        else
            p[i] = 1;
        // 超过边界总共就不是回文了
        while (s[i - p[i]] == s[i + p[i]]) p[i]++;
        // 不需要判断边界, 因为我们有通配符
        if (i + p[i] > mr) {
            mr = i + p[i];
            mid = i;
        }
        // 我们每走一步i, 都要和mx比较, 我们希望mx尽可能的远
    }
}

signed main() {
    string s;
    cin >> s;
    s = init(s);
    vector<int> p(s.size());
    manacher(p, s);
    // 初始化字符串和求取出来我们的每一个位置的最长长度
    int maxx = INT_MIN;
    for (auto& it : p) maxx = max(maxx, it);
    cout << maxx - 1 << "\n";
    return 0;
}

posted @ 2022-08-01 12:43  PiaYie  阅读(172)  评论(0编辑  收藏  举报