浅谈最长回文子串求法——字符串哈希
引入
众所周知,\(manacher\)算法可以在\(O(n)\)的时间复杂度内求出一个字符串的最长回文子串长度,但是\(manacher\)算法对于初学者来说并不是很好理解(至少我是这么认为的),因此我们考虑有没有什么更好理解的方式求出一个字符串的最长回文子串。
关于字符串哈希
字符串哈希是一种非常神奇的东西,我们可以\(O(n)\)地预处理出一字符串的哈希值(一般使用自然溢出法),然后我们发现一个字符串的哈希值相当于更换了其进制位的前缀和形式,因此我们其实在预处理完之后可以直接通过修改进制位的减法\(O(1)\)求出该字符串的一段子串的哈希值。
与回文串的关系
我们考虑一个字符串是回文串在哈希下的充要条件,我们根据回文串的定义可以知道一个字符串为回文串当且仅当其正序与反序完全相同而对于字符串哈希来说,想要判断两串是否相同只需判断它们的哈希值是否相等,所以判断一个字符串是否为回文串只需要判断其正向哈希与反向哈希值是否相等,而一个字符串的正向哈希与反向哈希都可以\(O(n)\)预处理,然后对于它的每一个子串都可以\(O(1)\)提取出它的正向与反向哈希,即可以\(O(1)\)判断其所有子串是否为回文串了。
进入正题
我们首先考虑回文串其实有两种形式——奇数回文串与偶数回文串。
奇数回文串比较好处理,因为每一个奇数回文串都存在一个回文中心,但是偶数回文串则没有,因此我们可以考虑进行转化,强制给偶数回文串增加一个回文中心,简单的说就是将一个不存在于字符串字符集中的字符插入至偶数回文串中心,使其有回文中心,方便判断,为了方便,我们可以将整个字符串的字符间都插入一个字符(一般为'#'),这样就能将偶数回文串与奇数回文串以相同的方式处理了。(其实这里与\(manacher\)算法相同)
然后我们考虑直接枚举回文串的回文中心,统计每一个字符作为回文中心的最长回文串长度,但是这样暴力拓展复杂度很显然是\(O(n^2)\)的,因此我们考虑一下如何优化。
我们发现,我们需要求的是最长回文串的长度,因此我们在从一个回文中心向外拓展时不需要从最里面开始考虑,直接从已经求出的最长长度开始拓展,因为我们的正反字符串哈希比较可以\(O(1)\)判断已经处理的任何字符串是否是回文串,因此开始拓展时若判断失败可以直接跳过,而回文串的半径\(r\)最多拓展到字符串边缘,因此我们的复杂度其实是\(O(n)\)的。
至此,我们已经完成了使用字符串哈希\(O(1)\)求字符串最长回文串的所有分析。
模板题:P3805 【模板】manacher - 洛谷 | 计算机科学教育新生态
模板代码
#include<iostream>
#include<cstring>
using namespace std ;
namespace IO
{
const size_t SIZE = 1 << 16 ;
namespace Read
{
#define isdigit(ch) (ch >= '0' && ch <= '9')
char buf[SIZE] , *_now = buf , *_end = buf ;
char getchar()
{
if(_now == _end)
{
_now = _end = buf ;
_end += fread(buf , 1 , SIZE , stdin) ;
if(_now == _end)
return EOF ;
}
return *(_now++) ;
}
template<typename T>
void read(T& w)
{
w = 0 ;
short f = 1 ;
char ch = getchar() ;
while(!isdigit(ch))
{
if(ch == '-')
f = -1 ;
ch = getchar() ;
}
while(isdigit(ch))
w = (w << 1) + (w << 3) + (ch ^ 48) , ch = getchar() ;
w *= f ;
}
#define sb(ch) (ch == ' ' || ch == '\n')
void read(char* s)
{
char ch = getchar() ;
while(sb(ch))
ch = getchar() ;
int len = 0 ;
while(!sb(ch) && ch != EOF)
s[len++] = ch , ch = getchar() ;
}
void read(string& s)
{
char ch = getchar() ;
while(sb(ch))
ch = getchar() ;
while(!sb(ch) && ch != EOF)
s.push_back(ch) , ch = getchar() ;
}
#undef sb
class qistream
{
public:
template<typename T>
qistream& operator>>(T& a)
{
read(a) ;
return *this ;
}
qistream& operator>>(char* s)
{
read(s) ;
return *this ;
}
} qcin ;
}
namespace Write
{
char buf[SIZE] , *p = buf ;
void flush()
{
fwrite(buf , 1 , p - buf , stdout) ;
p = buf ;
}
void putchar(char ch)
{
if(p == buf + SIZE)
flush() ;
*p = ch ;
++p ;
}
class Flush{public:~Flush(){flush() ;};}_;
template<typename T>
void write(T x)
{
char st[50] ;
int len = 0 ;
if(x < 0)
putchar('-') , x = -x ;
do
{
st[++len] = x % 10 + '0' ;
x /= 10 ;
} while(x) ;
while(len)
putchar(st[len--]) ;
}
void write(char c)
{
putchar(c) ;
}
void write(const char* s)
{
int siz = strlen(s) ;
for(int i = 0 ; i < siz ; ++i)
putchar(s[i]) ;
}
void write(char* s)
{
int siz = strlen(s) ;
for(int i = 0 ; i < siz ; ++i)
putchar(s[i]) ;
}
void write(string& s)
{
int siz = s.size() ;
for(int i = 0 ; i < siz ; ++i)
putchar(s[i]) ;
}
class qostream
{
public:
template<typename T>
qostream& operator<<(T x)
{
write(x) ;
return *this ;
}
qostream& operator<<(const char* s)
{
write(s) ;
return *this ;
}
} qcout ;
}
using Read::qcin ;
using Write::qcout ;
}
using namespace IO ;
char in[11000005] = {} ;
char str[22000005] = {} ;
int len = 0 ;
#define ull unsigned
ull hashz[22000005] = {} , hashf[22000005] = {} , p[22000005] = {} ;
ull gethashz(int i , int j)
{
return hashz[j] - hashz[i - 1] * p[j - i + 1] ;
}
ull gethashf(int i , int j)
{
return hashf[i] - hashf[j + 1] * p[j - i + 1] ;
}
bool check(int i , int j)
{
return gethashz(i , j) == gethashf(i , j) ;
}
int main()
{
qcin>>in ;
len = strlen(in) ;
for(int i = 0 ; i <= len * 2 ; ++i)
{
if(i % 2)
str[i] = in[i / 2] ;
else
str[i] = '#' ;
}
len = strlen(str) ;
hashz[0] = str[0] , hashf[len + 1] = 0 , p[0] = 1 ;
for(int i = 1 ; i < len ; ++i)
hashz[i] = hashz[i - 1] * 131 + str[i] , p[i] = p[i - 1] * 131 ;
for(int i = len - 1 ; i >= 0 ; --i)
hashf[i] = hashf[i + 1] * 131 + str[i] ;
int maxr = 0 ;
for(int i = 0 ; i < len ; ++i)
{
while(i - maxr >= 0 && i + maxr < len && check(i - maxr , i + maxr))
++maxr ;
}
qcout<<maxr - 1<<'\n' ;
return 0 ;
}
写在最后
当然字符串哈希的常数要大于\(manacher\)的,而且空间上不注意的话容易炸,(虽然我看到网上都是二分的\(O(n \log n)\)的字符串哈希做法),当然,字符串哈希比较好理解,而且更适合于不同问题的解决(题外话)
如果你不会字符串哈希,请看这篇文章。

浙公网安备 33010602011771号