各种回文子串相关问题及算法

文章较长,慎入,但是可以收获一大波方法和思路

基础:判断一个数是否为回文子串

最简单的方法,将字符串翻转,如果与源字符串相等就是回文子串。

完整代码

char s[25] ;

int main() {
	while (cin >> s) {
		char s_[25];
		for (ll i = 0; i < strlen(s); i++) s_[strlen(s) - i - 1] = s[i];
		s_[strlen(s)] = '\0';
		if (strcmp(s, s_) == 0) cout << "YES" << endl;
		else cout << "NO" << endl;
	}
}

(题型一)经典最长回文子串问题

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

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

方法一:最长公共子串

常 见 错 误 \mathbf\color{red}{常见错误}
有些人会忍不住提出一个快速的解决方案,不幸的是,这个解决方案有缺陷(但是可以很容易地纠正):

反转 S S S,使之变成 S ′ S' S 。找到 S S S S ′ S' S 之间最长的公共子串,这也必然是最长的回文子串。

这似乎是可行的,让我们看看下面的一些例子。

例如, S = “caba” S = \textrm{“caba”} S=“caba”, S ′ = “abac” S' = \textrm{“abac”} S=“abac”
S S S 以及 S ′ S' S 之间的最长公共子串为 “aba” \textrm{“aba”} “aba”,恰恰是答案。

让我们尝试一下这个例子: S = “abacdfgdcaba” , S ′ = “abacdgfdcaba” S = \textrm{“abacdfgdcaba”}, S' = \textrm{“abacdgfdcaba”} S=“abacdfgdcaba”,S=“abacdgfdcaba”
S S S以及 S ′ S' S之间的最长公共子串为 “abacd” \textrm{“abacd”} “abacd”。显然,这不是回文。


算 法 思 路 \mathbf\color{red}{算法思路}

我们可以看到,当 S S S 的其他部分中存在非回文子串的反向副本时,最长公共子串法就会失败。为了纠正这一点,每当我们找到最长的公共子串的候选项时,都需要检查子串的索引是否与反向子串的原始索引相同。如果相同,那么我们尝试更新目前为止找到的最长回文子串;如果不是,我们就跳过这个候选项并继续寻找下一个候选。


复 杂 度 分 析 \mathbf\color{red}{复杂度分析}

这给我们提供了一个复杂度为 O ( n 2 ) O(n^2) O(n2)的动态规划解法,它将占用 O ( n 2 ) O(n^2) O(n2)的空间(可以改进为使用 O ( n ) O(n) O(n) 的空间)。剩下的就是最长公共子串的问题了(该栏目下也会有公共子串的多种解法与题型)

方法二:暴力法

很明显,暴力法将选出所有子字符串可能的开始和结束位置,并检验它是不是回文。

复 杂 度 分 析 \mathbf\color{red}{复杂度分析}

  • 时间复杂度: O ( n 3 ) O(n^3) O(n3),假设 n n n 是输入字符串的长度,则 ( n 2 ) = n ( n − 1 ) 2 \binom{n}{2} = \frac{n(n-1)}{2} (2n)=2n(n1)为此类子字符串(不包括字符本身是回文的一般解法)的总数。因为验证每个子字符串需要 O ( n ) O(n) O(n) 的时间,所以运行时间复杂度是 O ( n 3 ) O(n^3) O(n3)
  • 空间复杂度: O ( 1 ) O(1) O(1)

方法三:动态规划

为了改进暴力法,我们首先观察如何避免在验证回文时进行不必要的重复计算。考虑 “ababa” \textrm{“ababa”} “ababa”这个示例。如果我们已经知道 “bab” \textrm{“bab”} “bab”是回文,那么很明显, “ababa” \textrm{“ababa”} “ababa” 一定是回文,因为它的左首字母和右尾字母是相同的。
在这里插入图片描述
这产生了一个直观的动态规划解法,我们首先初始化一字母和二字母的回文,然后找到所有三字母回文,并依此类推…
复 杂 度 分 析 \mathbf\color{red}{复杂度分析}

时间复杂度: O ( n 2 ) O(n^2) O(n2)

空间复杂度: O ( n 2 ) O(n^2) O(n2)

方法四:中心扩展算法

事实上,只需使用恒定的空间,我们就可以在 O ( n 2 ) O(n^2) O(n2) 的时间内解决这个问题。

我们观察到回文中心的两侧互为镜像。因此,回文可以从它的中心展开,并且只有 2 n − 1 2n - 1 2n1个这样的中心。

你可能会问,为什么会是 2 n − 1 2n - 1 2n1 个,而不是 n n n 个中心?原因在于所含字母数为偶数的回文的中心可以处于两字母之间(例如 “abba” \textrm{“abba”} “abba” 的中心在两个 ‘b’ \textrm{‘b’} ‘b’之间)。

复 杂 度 分 析 \mathbf\color{red}{复杂度分析}

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2),由于围绕中心来扩展回文会耗去 O ( n ) O(n) O(n) 的时间,所以总的复杂度为 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( 1 ) O(1) O(1)
int main() {
	string s;
	while (cin >> s) {
		int len = s.size(), res = 0;
		for (int i = 0; i < 2 * len - 1; i++) {
			int cnt = 0, l, r;
			if (i % 2 == 0) {//偶数,中心是一个点
				cnt = 1;
				l = (i / 2) - 1, r = (i / 2) + 1;
			}
			else //奇数,中心是空的
				l = i / 2, r = i / 2 + 1;
			
			while (l >= 0 && r < len&&s[l--] == s[r++])
				cnt += 2;
			if (cnt > res)res = cnt;
		}
		cout << res << endl;
	}
}

(题型二)修改最少元素使得原字符变为回文串

标准题型:回文词是一种对称的字符串。任意给定一个字符串,通过插入若干字符,都可以变成回文词。此题的任务是,求出将给定字符串变成回文词所需要插入的最少字符数。
原题解

方法一:奇思妙想

首先,我们要摸清回文串的特性,回文就是正着读反着读一样,一种非常对称不会逼死强迫症的字符串;这就是我们的突破口。。。你难道以为是逼死强迫症么?哈哈,太天真了,突破口其实是因为回文正着读反着读都相同的特性。。。这样我们就可以再建一个字符数组存储倒序的字符串

我们先分析下样例:ab3bd,

它的倒序是:db3ba

这样我们就可以把问题转化成了求最长公共自序列的问题,为啥可以这么转化呢?

它可以这么理解,正序与倒序“公共”的部分就是我们回文的部分,如果把正序与倒序公共的部分减去你就会惊奇的发现剩余的字符就是你所要添加的字符,也就是所求的正解

ad da把不回文的加起来就是我们梦寐以求的东西:回文串(卧槽?太没追求了)

把ad,da加起来成回文串就是adb3bda,所以这个问题就可以转化成了求最长公共自序列的问题,将字符串的长度减去它本身的“回文的”(最长公共自序列)字符便是正解

找到解题思路后我们就可以开始写了,最长公共自序列问题是个经典的dp问题,
最容易想到的方法就是开个二维数组dp【i】【j】,i,j分别代表两种状态;那么我们的动态转移方程应该就是

if (s[i] == s_[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
#include<iostream>
#include<cmath>
#include<iomanip>
#include<string.h>
#include<vector>
#include<map>
#include<queue>
#include<algorithm>
using namespace std;

#define MAX 1005
#define inf 1e9
#define ll long long
#define p pair<ll, ll>

char s[MAX], s_[MAX], t;
ll cnt = 0, dp[MAX][MAX];

int main() {
	cin >> s;
	cnt = strlen(s);
	for (ll i = 0; i < cnt; i++) s_[cnt - i - 1] = s[i];
	memset(dp, 0, sizeof(dp));
	for (ll i = 1; i <= cnt; i++) {
		for (ll j = 1; j <= cnt; j++) {
			ll x = i - 1, y = j - 1;
			if (s[x] == s_[y]) dp[i][j] = dp[i - 1][j - 1] + 1;
			else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
		}
	}
	cout << cnt - dp[cnt][cnt] << endl;
}

可添加滚动数组优化防止内存爆掉。

方法二:正宗DP

在这里插入图片描述
需要稍作解释, s [ i ] = = s [ j ] s[i]==s[j] s[i]==s[j]的时候,显然对着一对字符而言,我们不需要添加任何额外的字符,因此需要的字符数是子字符串需要的字符数。不相等的时候,我们有两种选择,在 i i i位置的右边加上一个字符,或者在 j j j位置的左边加上一个字符,选比较小的那个。

(题型三)求第k个回文数

POJ 2402 Palindrome Numbers


/*
题意:
    求第n个回文数
思路:
    不难发现规律,1位和2位的回文数有9个,3位和4位的回文数有90个。。。。
首先求出第k位的基数 10^((k-1)/2),再计算出第n个回文数是几位数,
对半后,对前半段进行填数,后半段的只要将前半段反向输出即可
*/
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define LL __int64
const int inf = 0x3f3f3f3f;
const int N = 2010;
LL f[30]={0,1,1};
LL n;
void solve()
{
    int i,j;
    int k = 1;//k表示回文串数字的位数
    while(n > f[k]*9)
    {
        n -= 9*f[k++];
    }//n要减去小于k位的所有回文数
    int mid = (k+1)>>1;
    LL ans = 1;
    for(i  = 1, j = 0; i <= mid; i ++)//第一位到中间那一位
    {
        if(i == 1) j = 1;//i为1,因为首位不能为0,所以要从1开始
        else j = 0;//j表示第i位要填的数字
        while(n > f[k])
        {
            n -= f[k];//减去基数
            j ++;
        }
        k -= 2;//除了第i位和i相对称的那一位,中间还有几位
        if(i == 1) ans = j;
        else ans = ans*10 + j;
    }
    printf("%I64d",ans);
    if(k&1) ans /= 10;//位数为奇数,最中间的数只输出一次
    while(ans)
    {
        printf("%I64d",ans%10);
        ans /= 10;
    }
    printf("\n");
}
int main()
{
    int i;
    for(i = 3; i < 30; i ++)
    {
        if(i&1) f[i] = f[i-1]*10;
        else f[i] = f[i-1];
    }
    while(scanf("%I64d",&n),n)
    {
        solve();
    }
    return 0;
}

(题型四)双倍回文

P4287 [SHOI2011]双倍回文

(题型五)整形回文运算+进制转换

题目链接

题目大意

若一个数(首位不为零)从左向右读与从右向左读都一样,我们就将其称之为回文数。

例如:给定一个十进制数 56,将 56 加 65(即把 56 从右向左读),得到 121 是一个回文数。

又如:对于十进制数 87:

STEP1:87+78=165
STEP2:165+561=726
STEP3:726+627=1353
STEP4:1353+3531=4884

在这里的一步是指进行了一次 N 进制的加法,上例最少用了 4步得到回文数 4884。

写一个程序,给定一个 N( 2 ≤ N ≤ 10 2 \le N \le 10 2N10 N = 16 N=16 N=16)进制数 M M M(100 位之内),求最少经过几步可以得到回文数。如果在 30 步以内(包含 30 步)不可能得到回文数,则输出 Impossible!

解题思路

由于 100 100 100位的数经过30步左右的这类运算,还是很可能会爆掉int或者longlong的,所以我使用了 v e c t o r vector vector来存储数值并进行运算。

  1. 进制转换,将所有输入数字从低位到高位存入vector,比如10进制的87,存入后 v [ 0 ] = 7 , v [ 1 ] = 8 v[0]=7,v[1]=8 v[0]=7,v[1]=8,后续如果要进位我们只需要pushback即可。
  2. for循环30次,每次使用渗透率(stl)大法将vector翻转,比较翻转前后是否完全相同,如果相同就得到结果,否则与原数组相加,注意进位。

完整代码

#include<iostream>
#include<cmath>
#include<iomanip>
#include<string.h>
#include<vector>
#include<map>
#include<queue>
#include<algorithm>
using namespace std;

#define MAX 25
#define inf 1e9
#define ll long long
#define p pair<ll, ll>

ll n, m;
vector<ll> a, b, num;
int main() {
	cin >> n; char str[105]; cin >> str;
	for (ll i = 0; i < strlen(str); i++) {
		char s = str[i];
		if (s >= '0'&&s <= '9') num.push_back(s - '0');
		else if (s >= 'A'&& s <= 'F')num.push_back(s - 'A' + 10);
	}
	reverse(num.begin(), num.end()); a.assign(num.begin(), num.end());
	ll i = 0, j = 0;
	for (i = 0; i < 30; i++) {
		b.assign(a.begin(), a.end());
		reverse(b.begin(), b.end());
		while (b[0] == 0) b.erase(b.begin());//舍去前导0
		for (j = 0; j < a.size(); j++) if (a[j] != b[j])break;
		if (j == a.size()) break;
		ll res = 0, mod = 0;//mod:来自上一位的进位
		for (ll i = 0; i < a.size(); i++) {
			res = (a[i] + b[i] + mod) % n, mod = (a[i] + b[i] + mod) / n;
			a[i] = res;
		}
		while (mod > 0) {//进位比较多,位数不够
			res = mod % n, mod = mod / n;
			a.push_back(res);
		}
	}

	if (i == 30) {
		b.assign(a.begin(), a.end());
		reverse(b.begin(), b.end());
		for (j = 0; j < a.size(); j++) if (a[j] != b[j])break;
		if (j == a.size()) cout << "STEP=30" << endl;
		else cout << "Impossible!" << endl;
	}
	else cout << "STEP=" << i << endl;
}

(题型六)素数+回文数组合问题

最后给一种简单题型下下饭。求11到n之间(包括n),既是素数又是回文数的整数有多少个。
题目链接,这类题只需要简单的变通即可,先筛素数,再判断回文数。

char s[25];
bool isPrime[1005];
vector<ll> primes;
int main() {
	ll n, cnt = 0; cin >> n;
	memset(isPrime, 1, sizeof(isPrime));
	for (ll i = 2; i <= n; i++){//埃及筛法
		if (isPrime[i] && i >= 11) primes.push_back(i);
		for (ll j = i; j < 1000; j += i) isPrime[j] = false;
	}
	for (unsigned i = 0; i < primes.size(); i++) {
		if (primes[i] < 100 && primes[i] % 10 == primes[i] / 10)cnt++;//二位数
		else if (primes[i] < 1000 && primes[i] % 10 == primes[i] / 100)cnt++;//三位数
	}
	cout << cnt << endl;
}

(题型七)最小回文数

输入格式
1行,一个正整数N。N的数值小于10^100,并且N没有前导0。

输出格式
你的程序应该输出一行,最小的回文数P(P>N)。
取前半个字符串

思路

如果 位数是奇数

则包含中间数

生成回文串

判断若符合条件直接输出

否则在原来的半串中从后往前搜索到一个可以+1的位(9不能+1)

进行加一后生成回文并输出

特判:全部为9

输出1000……0001

注意事项:

  1. 如果全是9,需要特判
  2. 如果中间几位是9,虽然这几位不能加1,但是要把他们改为0,比如大于1991的最小回文串是2002
  3. 串的长度是奇数时要注意最中间那一位
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cstdlib>
using namespace std;
string st;
int ans[100000];
int pd,l;
void print()
{
    for(int i=l+1>>1;i>=1;i--)if(ans[i]>9)ans[i-1]++,ans[i]%=10;
    for(int i=(l+1>>1)+1;i<=l;i++)ans[i]=ans[l-i+1];
    if(ans[0])ans[l]=ans[0],cout<<ans[0];
    for(int i=1;i<=l;i++)cout<<ans[i];
}
int main()
{
    cin>>st;
    l=st.size();
    for(int i=1;i<=l+1>>1;i++)
        ans[i]=st[i-1]-48;
    for(int i=(l+1>>1)+1;i<=l;i++)
        ans[i]=ans[l-i+1];
    pd=0;
    for(int i=1;i<=l;i++)
        if(ans[i]+48!=st[i-1])
        {
            pd=ans[i]+48>st[i-1];
            break;
        }   
    if(!pd)ans[l+1>>1]++;
    print();
}
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值