字符串hash+二分处理回文子串问题
1. 背景
如果使用DP求解字符串最长回文子串问题,假设字符串长度为\(N\),那么时间复杂度为\(O(N^2)\)。当字符串很长时(如长达100万字节),那么平方复杂度肯定行不通。
如果使用字符串hash+二分的方法,那么可以在\(O(NlgN)\)的时间复杂度内求解。
2. 字符串hash
字符串hash指输入一个字符串,在常数时间内得到一个正整数。理想哈希函数有两个要求:无碰撞性、随机性。
这里采用算法笔记中的哈希函数。
假设有一个长度为\(N\)的整型数组\(H\),其中\(H[i]\)表示字符串前i个字符组成的子串的hash值。
\(H[0]=str[0]\)
\(H[i]=(H[i-1]*p+str[i]) \%mod\)
\(其中p为进制数,mod为模。一般p取一个10^7级别的素数,mod取一个10^9级别的素数\)
在求出H数组后,就可以求出子串的hash值:
假设求第i位到第j位的子串的hash值,则有\(H[i..j]=((H[j]-H[i-1]*p^{j-i+1} ) \% mod+mod ) \% mod\)
3. 二分法查找
对给定输入的字符串,可以每两个字符之间插入一个特殊符号(如'#'),得到新的字符串str。这样做可以避免后续讨论回文子串长度是奇数还是偶数。
定义两个变量lb与rb,分别表示回文子串半径的可取的范围的上下界。定义变量k,表示当前判断的回文子串的半径。
初始时,lb=0,rb=min{i,len-i-1},即要找回文子串的半径就可以从这个范围中找。
很容易想到,若由一个半径得到的子串不是回文的,那么可知继续增大半径是没有用的,那么就需要减小半径继续搜索。同理,若字串是回文的,就增大半径来搜索。这就启发我们用二分的方法,挑选下界和上界的中间值来作为下一次的半径。对应的行为:子串是回文,lb=k;否则rb=k-1。下一次的半径k=(lb+rb)/2+1。
重复上述过程,直到lb>=rb为止。然后判断中心点i对应的字符c。若c为用于分隔的特殊字符,那么说明回文子串长度为偶数,子串长度为\(2*((lb+1)/2)\)
(注意,是整数除法,不得化简式子);否则回文子串长度为奇数,子串长度为\(2*(lb/2)+1\)
例题:acwing139--回文子串的最大长度
这里用了O2优化
#include<cstdio>
#include<cstring>
#include<string>
#include<iostream>
#include<unordered_map>
#include<vector>
#include<algorithm>
#pragma GCC optimize(2)
using namespace std;
typedef long long ll;
const int MAXLEN = 1000005;
const int MAXNUM = 35;
int strnum=0; //字符串的数量
//正序和逆序的字符串
string s1, s2;
//结果
int result[MAXNUM] = { 0 };
//进制数
ll p = 10000019;
//mod
ll mod = 1000000007;
//H数组,H[i]表示字符串前i个字符的子串的hash值
ll H1[MAXLEN] = { 0 }, H2[MAXLEN] = { 0 };
//powp[i]表示p的i次方
ll powp[MAXLEN] = { 0 };
//计算字符串的H数组
void calH(ll* H,string& str) {
H[0] = str[0];
for (int i = 1; i < str.size(); i++) {
H[i] = (H[i - 1] * p + str[i]) % mod;
}
}
//打表,预先求出p的幂次
void getpowp() {
powp[0] = 1;
for (int i = 1; i < MAXLEN; i++) {
powp[i] = (powp[i - 1] * p)%mod;
}
}
//计算子串的哈希值
ll calsubH(ll* H,int i,int j){
//对i为0的情况特殊处理
if (i == 0)
return H[j];
else
return ((H[j] - H[i - 1] * powp[j - i + 1]) % mod + mod) % mod;
}
//比较两个子串是否相等
bool cmp(int i1,int j1,int i2,int j2) {
return calsubH(H1, i1, j1) == calsubH(H2, i2, j2);
}
//对字符串进行预处理,每两个字符之间插入一个'#'
void initstring(string& tmp,int& len) {
len = 2 * len - 1;
char* ch = new char[len + 1];
for (int i = 0; i < len; i++) {
if (i % 2 == 0) ch[i] = tmp[i / 2];
else ch[i] = '#';
}
ch[len] = '\0';
tmp = string(ch);
}
//计算以第i个字符为中心时,回文子串的最大长度
//字符串s1,字符串s2为其反序
int calbyi(int i,int len) {
//搜索区间的最小值、最大值
int lb = 0, rb = min(i, len - i-1);
//字串的半径(不包括中心点),注意必须初始化应对i=0的情况
int k=0;
//使用二分法求解
while (rb > lb) {
//注意半径要大于等于1,否则会在第一个分支中死循环
k = (lb + rb) / 2+1;
//正、反序字串中要比较的子串的开始位置
int b1 = i - k, b2 = len - k - (i + 1);
//判断子串哈希值是否相等
if (cmp(b1,b1+k-1,b2,b2+k-1)) {
//以i为中心,k为半径的子串是回文子串,可以向两边拓展(用二分的方法)
//所以修改区间的最小值,表示半径最少有k个字符
lb = k;
}
else {
//以i为中心,k为半径的子串不是回文子串,所以向中心收缩半径
//修改区间的最大值,表示半径要小于k个字符
rb = k - 1;
}
}
//求除掉'#'后的回文子串的长度,注意这里半径不能用k,因为上面退出循环时可能k的值大于正确半径
//要根据中心字符的类型来判断
//中心为'#',回文子串为偶数长度
if (s1[i] == '#') {
return ((lb + 1) / 2) * 2;
}
//中心为英文字符,回文串为奇数长度
else{
return 2*(lb/2) + 1;
}
}
//计算字符串s1的最长回文字串长度
int cal(int len) {
int maxlen=1; //回文串的最大长度
//遍历所有点,将其作为中心点求解
for (int i = 0; i < len; i++) {
maxlen = max(calbyi(i,len), maxlen);
}
return maxlen;
}
int main(void) {
ios::sync_with_stdio(false);
getpowp(); //打表
//给s1,s2预留空间来优化
s1.reserve(MAXLEN);
s2.reserve(MAXLEN);
//对所有字符串求解其最长回文子串长度
string inputstr;
int strnum = 0;
int len = 0;
while(true) {
getline(cin, s1);
if (s1 == "END") break;
//原始字符串和反序字符串
len = s1.size();
initstring(s1,len); //给字符之间插入'#'
s2 = s1;
reverse(s2.begin(), s2.end());
//计算两个字符串的H数组
calH(H1, s1);
calH(H2, s2);
//求解
result[++strnum]=cal(len);
}
//输出结果
for (int i = 1; i <= strnum; i++) {
cout << "Case " << i << ": " << result[i] << endl;
}
}