思路

我们先观察 \(\texttt{aauuhhhua}\) 这个字符串,如果把后三个字符 \(\texttt{hua}\) 各自就地重复一次(在后面插入一个相同字符),那么整个字符串就是 \(\texttt{aauuhhhhuuaa}\),变成一个回文串了。

更一般地,一个长度为 \(3k\) 的串 \(T\) 满足题目要求,当且仅当\(T\) 的后 \(k\) 个字符各自重复一次之后,得到的长度为 \(4k\) 的串是回文串。

判断回文串可以用字符串哈希实现,但是怎么实现每个字符在它的后面重复一次的操作呢?换言之,某个前缀哈希值代表的是前缀串所有字符重复两次的哈希值,怎么实现?

举个例子:

\[s = \texttt{aabbcc} \]

以下固定一个基数 \(p\)\(s_i\) 表示 \(s\) 的第 i 个元素的权值。

原串 \(s\) 的前缀哈希定义为:

\[h_0 = 0,\qquad h_i = (p \cdot h_{i-1} + s_i)\\ \]

展开得:

\[\left\{ \begin{aligned} h_1 &= s_1 \\ h_2 &= p \times h_1 + s_2 \\ h_3 &= p \times h_2 + s_3 \\ h_4 &= p \times h_3 + s_4 \\ \vdots \end{aligned} \right. \]

现在,我定义一个二倍哈希数组 \(dh\)。我要一步当两步走,\(dh_1 = h_2\)\(dh_2 = h_4\),将 \(h_1\)\(h_3\) 的值代入 \(h_2\)\(h_4\) 的递推式中得到:

\[\left\{ \begin{aligned} dh_1 &= p \times s_1 + s_2 \\ &= (p+1) \times s_1\\ dh_2 &= p \times (p \times dh_1 + s_3) + s_4 \\ &= p^2 \times dh_1 + p \times s_3 + s_4\\ &= p^2 \times dh_1 + (p+1) \times s_3\\ \vdots \end{aligned} \right. \]

于是我们就得到了 \(dh\) 的递推式的一般形式:

\[dh_i = p^2 \times dh_{i-1} + (p+1) \times s_i \]

还有最后一个问题,那就是应该怎么计算区间 \([l,r]\) 的二倍哈希值?

推导如下:

\[\begin{alignat}{2} & & dh_r & = p^{2(r-1)} \times s_1 + p^{2(r-2)} \times s_2 + p^{2(r-3)} \times s_3 + \dots + p^{2(r-l+1)} \times s_{l-1} + \dots + p^{2(1)} \times s_{r-1} + s_r \tag{1} \\ & \because \quad & dh_{l-1} & = p^{2(l-2)} \times s_1 + p^{2(l-3)} \times s_2 + p^{2(l-4)} \times s_3 +\dots + p^{2(1)} \times s_{l-2} + s_{l-1} \nonumber \\ & \therefore \quad & p^{2(r-l+1)} \times dh_{l-1} & = p^{2(r-1)} \times s_1 + p^{2(r-2)} \times s_2 + p^{2(r-3)} \times s_3 + \dots + p^{2(r-l+1)} \times s_{l-1} \tag{2} \\ & & (1)-(2) & = p^{2(r-l)} \times s_l + p^{2(r-l-1)} \times s_{l+1} + \dots + p^{2(1)} \times s_{r-1} + s_r \nonumber \end{alignat} \]

我们需要用上面的方法,取出子串 \(T\) 的后 \(k\) 个字符,将它们加倍后反转,再与前 \(2k\) 个字符比较。

加倍哈希的区间查询公式对任意区间都成立。如果我们预处理的是从右往左构建的加倍哈希,就可以直接截取后缀区间的加倍值,而不用做额外的反转操作。

具体地,设 \(dh_i\)\(s[i \dots n]\)(即后缀)反转后每个字符加倍的前缀哈希,从右往左递推:

\[dh_i = dh_{i+1} \times p^2 + (p+1) \times s_i \]

此时,区间 \([c,r]\) 的反向加倍哈希就是 \(dh_c - dh_{r+1} \times p^{2(r-c+1)}\)

至此思路完结。

代码细节

  • 二倍哈希是倒序更新的;
  • 注意二分的左右边界以及判断区间的左右端点;
  • 常用的哈希底数和模数 \((31,10^9+7)\) 能过;

复杂度分析

二分搭配哈希求回文串的变式,总体时间复杂度 \(O(n \log n)\)

代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
#define div() cout << "-----------------------\n";
#define debug(n) cout << #n << " = " << n << "\n";
#define deb(n) cout << #n << " = " << n << "    ";
int n;string s;int a[1000005];int ans;
int h[1000005];int fh[1000005];int base[1000005];
const int p = 31,mod = 1e9+7;
inline void work(int center){
	int l = 0,r = min(center/2,n-center+1),mid = 0;int len = 0;
	while(l <= r){
		mid = (l+r) >> 1;
		int lb = center-mid-mid;
		int rb = center+mid-1;
		int h1 = (h[center-1]-h[lb-1]*base[center-lb]%mod+mod)%mod;
		int h2 = (fh[center]-fh[rb+1]*base[(rb-center+1)*2]%mod+mod)%mod;
		if(h1 == h2){
			len = mid;
			l = mid+1;
		}
		else{r = mid-1;}
	}
	ans += len;
}
signed main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    cin >> s;n = s.size();
    for(int i = 1;i <= n;i++){a[i] = s[i-1]-'a'+1;}
    base[0] = 1;
    for(int i = 1;i <= 2*n;i++){base[i] = base[i-1]*p%mod;}
    for(int i = 1;i <= n;i++){h[i] = (h[i-1]*p+a[i])%mod;}
    for(int i = n;i >= 1;i--){fh[i] = (fh[i+1]*p*p+(p+1)*a[i])%mod;}
    for(int i = 1;i <= n;i++){work(i);}
    cout << ans;
    return 0;
}