回文如何才能不难

回文如何才能不难

直觉上:回文≈难题,LC上的回文难题, 但是总不能指望面试/周赛出中等题吧?

    1. 回文对
    1. 最短回文串
    1. 分割回文串
    1. 统计不同的回文子序列
    1. 超级回文数
    1. 段式回文

1. 线性时间内解决回文串问题——Manacher算法 {竞赛难度}

Manacher 算法是在线性时间!内求解最长回文子串的算法。在本题中,我们要求解回文串的个数,为什么也能使用 Manacher 算法呢?这里我们就需要理解一下 Manacher 的基本原理。

Manacher 算法的处理方式是在所有的相邻字符中间插入 # \# #,比如 a b a a a b a a abaaabaa abaaabaa 会被处理成 # a # b # a # a # a # b # a # a # \#a\#b\#a\#a\#a\#b\#a\#a\# #a#b#a#a#a#b#a#a#,这样可以保证所有找到的回文串都是奇数长度的(因为 n + n + 1 = 2 n + 1 n+n+1=2n+1 n+n+1=2n+1),以任意一个字符为回文中心,既可以包含原来的奇数长度的情况,也可以包含原来偶数长度的情况。假设原字符串为 S S S,经过这个处理之后的字符串为 s s s

我们用 f ( i ) f(i) f(i) 来表示以 s s s 的第 i i i 位为回文中心,可以拓展出的最大回文半径,那么 f ( i ) − 1 f(i) - 1 f(i)1 就是以 i i i为中心的最大回文串长度 (想一想为什么)。

Manacher 算法依旧需要枚举 s s s 的每一个位置并先假设它是回文中心,但是它会利用已经计算出来的状态来更新 f ( i ) f(i) f(i),而不是向「中心拓展」一样盲目地拓展。具体地说,假设我们已经计算好了 $[1, i - 1] $区间内所有点的 f f f(即我们知道 $[1, i - 1] $这些点作为回文中心时候的最大半径), 那么我们也就知道了 [ 1 , i − 1 ] [1, i - 1] [1,i1] 拓展出的回文达到最大半径时的回文右端点。例如 i = 4 i = 4 i=4的时候 f ( i ) = 5 f(i) = 5 f(i)=5,说明以第 4 个元素为回文中心,最大能拓展到的回文半径是 5,此时右端点为 4 + 5 − 1 = 8 4 + 5 - 1 = 8 4+51=8。所以当我们知道一个 i i i 对应的 f ( i ) f(i) f(i) 的时候,我们就可以很容易得到它的右端点为 i + f ( i ) − 1 i + f(i) - 1 i+f(i)1

Manacher 算法如何通过已经计算出的状态来更新 f ( i ) f(i) f(i) 呢?Manacher 算法要求我们维护「当前最大的回文的右端点 r m r_m rm」以及这个回文右端点对应的回文中心 i m i_m im 。我们需要顺序遍历 s s s,假设当前遍历的下标为 i i i。我们知道在求解 f ( i ) f(i) f(i) 之前我们应当已经得到了从 [ 1 , i − 1 ] [1, i - 1] [1,i1] 所有的 f f f,并且当前已经有了一个最大回文右端点 r m r_m rm 以及它对应的回文中心 i m i_m im

初始化 f ( i ) f(i) f(i)

  • 如果 i ≤ r m i \leq r_m irm,说明 i i i 被包含在当前最大回文子串内,假设 j j j 是 $i $关于这个最大回文的回文中心 i m i_m im的对称位置(即 j + i = 2 × i m j + i = 2 \times i_m j+i=2×im),我们可以得到 f ( i ) f(i) f(i) 至少等于 min ⁡ { f ( j ) , r m − i + 1 } \min\{f(j), r_m - i + 1\} min{f(j),rmi+1}。这里将 f ( j ) f(j) f(j) r m − i + 1 r_m - i + 1 rmi+1取小,是先要保证这个回文串在当前最大回文串内。(思考:为什么 f ( j ) f(j) f(j) 有可能大于 r m − i + 1 r_m - i + 1 rmi+1?)如果 i > r m i > r_m i>rm,那就先初始化 f ( i ) = 1 f(i) = 1 f(i)=1

中心拓展

  • 做完初始化之后,我们可以保证此时的 s [ i + f ( i ) − 1 ] = s [ i − f ( i ) + 1 ] s[i + f(i) - 1] = s[i - f(i) + 1] s[i+f(i)1]=s[if(i)+1],要继续拓展这个区间,我们就要继续判断 s [ i + f ( i ) ] s[i + f(i)] s[i+f(i)] s [ i − f ( i ) ] s[i - f(i)] s[if(i)]是否相等,如果相等将 f ( i ) f(i) f(i)自增;这样循环直到 s [ i + f ( i ) ] ≠ s [ i − f ( i ) ] s[i + f(i)] \neq s[i - f(i)] s[i+f(i)]=s[if(i)],以此类推。我们可以看出循环每次结束时都能保证$ s[i + f(i) - 1] = s[i - f(i) + 1]$,而循环继续(即可拓展的条件)一定是 s [ i + f ( i ) ] = s [ i − f ( i ) ] s[i + f(i)] = s[i - f(i)] s[i+f(i)]=s[if(i)]。 这个时候我们需要注意的是不能让下标越界,有一个很简单的办法,就是在开头加一个 $$$,并在结尾加一个 ! ! !,这样开头和结尾的两个字符一定不相等,循环就可以在这里终止。
    这样我们可以得到 s s s 所有点为中心的最大回文半径,也就能够得到 S S S 中所有可能的回文中心的的最大回文半径,把它们累加就可以得到答案。
class Solution {
public:
    int countSubstrings(string s) {
        int n = s.size();
        string t = "$#";
        for (const char &c: s) {
            t += c;
            t += '#';
        }
        n = t.size();
        t += '!';

        auto f = vector <int> (n);
        int iMax = 0, rMax = 0, ans = 0;
        for (int i = 1; i < n; ++i) {
            // 初始化 f[i]
            f[i] = (i <= rMax) ? min(rMax - i + 1, f[2 * iMax - i]) : 1;
            // 中心拓展
            while (t[i + f[i]] == t[i - f[i]]) ++f[i];
            // 动态维护 iMax 和 rMax
            if (i + f[i] - 1 > rMax) {
                iMax = i;
                rMax = i + f[i] - 1;
            }
            // 统计答案, 当前贡献为 (f[i] - 1) / 2 上取整
            ans += (f[i] / 2);
        }

        return ans;
    }
};

2. Rabin-Karp编码


214. 最短回文串

给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。

示例 1:

输入: "aacecaaa"
输出: "aaacecaaa"

示例 2:

输入: "abcd"
输出: "dcbabcd"

我们可以用Rabin-Karp编码判断一个子集是否为回文串, 设 A S C I I ( i ) ASCII(i) ASCII(i)表示字符的ascii码.

b a s e base base可以取比整个字符集大的素数, m o d mod mod可以取一个很大的幂加上一个素数,比如 1 0 5 + 7 10^5+7 105+7. f ( i ) 和 f ( i ) ^ f(i)和\hat{f(i)} f(i)f(i)^分别表示字符串编码和对应的回文串编码.
f ( i ) = f ( i − 1 ) ∗ b a s e % m o d + s [ i ] f ( i ) ^ = ( f ( i − 1 ) ^ + s [ i ] ∗ b a s e i ) % m o d f(i) = f(i-1)*base\% mod + s[i]\\ \hat{f(i)} = (\hat{f(i-1)} +s[i]*base^i)\%mod f(i)=f(i1)base%mod+s[i]f(i)^=(f(i1)^+s[i]basei)%mod
由于题目只要求在字符串前加字符,因此可以判断 s s s中最长回文前缀,

代码如下:

class Solution {
public:
    string shortestPalindrome(string s) {
        if(!s.size()) return "";
        //思路,回文数判断:Manacher算法
        //反过来思考:去掉最少几个字符它将称为回文数
        int left = 0, right = 0, mul = 1;
        int base = 157, mod = 1E5+7;
        int  loc = 1; 
        int n = s.size();
        for(int i = 0;i < n;i++)
        {
            left = (long long)left*base%mod + (int)s[i];
            right = (right+ (int)s[i]*mul)%mod;
            if(left == right) 
            {
                loc = i;
            }
            mul = (mul*base)%mod; 
        }
        string add = (loc==n-1)?"":s.substr(loc,n-loc);
        reverse(add.begin(),add.end());
        return (add+s);
    }
};

复杂度分析

  • 时间复杂度: O ( ∣ s ∣ ) O(|s|) O(s)
  • 空间复杂度: O ( 1 ) O(1) O(1)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值