字符串 hash 算法求解回文串
此文以一个题目为例,讲解求解回文串的 hash 字符串解法。
注: 除了 kmp 算法之外,该算法也可用来求解字符串子串问题,此处不论述该问题。
题目
给定一个字符串 S,以及 q 次询问。每次询问给出两个正整数 L,R,你需要回答 S[L~R]是否为回文串。
input
第一行给出字符串 S,|S|<=1e6. 保证字符串仅由小写字母构成。
第二行给出询问次数 q,q<=1e6.
接下来每行给出两个整数 L,R,1<=L,R<=|S|.
output
对于每个询问,若字符串 S 中[L,R]为回文串,输出 YES,否则输出 NO。
测试用例
输入:
abccba
5
1 6
2 5
3 4
1 3
1 1
输出:
YES
YES
YES
NO
YES
解答
一、暴力解法
设置两个指针,前后依次遍历匹配。该方法时间复杂度较高,为 O(n*q),直接上代码,不再赘述。
// 判断字符串s从i到j的子字符串是否为回文串
bool judege(char *s, int i, int j){
while (i < j)
{
if(s[i] != s[j])
break;
i++;
j--;
}
return i >= j;
}
二、字符串哈希解法
所谓字符串 hash,就是通过 hash 运算,将原本的字符串的子串计算为一个个可以计算的整数,最后求子串时可直接通过整数计算求得。此方法将计算子串的时间复杂度由 O(n)降为了 O(1)。最后通过将子串的正序 hash 值与逆序 hash 值相比较是否相等,即可评判该子串是否为回文串,大大节省了计算时间,整体时间复杂度下降为 O(n+q)。
下面详细介绍该算法:
生成 hash 序列
为了更加直观的讲解,我们以数字字符串s="-123456789"
为例,用十进制将该字符串生成 hash 序列。为了方便计算 s[0]不存储有效值。
先设置一个数组Hash[10]
来存放我们生成的 Hash 序列。Hash[0] = 0
, Hash[1] = Hash[0] * 10 + (s[i]-'0')
,递归可得Hash[i] = Hash[i-1]*10 + (s[i]-'0')
。
其中s[i] - '0'
是为了将对应的字符计算为其对应的整数,如'5' - '0'
计算得整数5
代码如下:
Hash[0] = 0;
for (int i = 1; i < 10; i++)
{
Hash[i] = Hash[i-1] * 10 + (s[i] - '0');
}
最后生成的 Hash[10]序列如下:
再次强调,Hash[0]
是功能位,方便计算,并没有存储字符串"123456789"
生成的 hash 值,有效 hash 值从 Hash[1]开始。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 12 | 123 | 1234 | 12345 | 123456 | 1234567 | 12345678 | 123456789 |
得到了如上的 hash 序列,我们怎么通过该 hash 序列求子串的值呢?
如我们需要求子串"345"
的 hash 值,我们只需要通过计算Hash[5] - Hash[2]*10^3
即可求得。
所以我们需要求得子串s[l:r]
的 hash 值,只需要计算Hash[r] - Hash[l-1]*10^(r-l+1)
即可。
计算一个子串的 hash 值只需要 O(1)的时间。
通过字符串 hash 验证回文串
我们已经知道了如何计算字符串的 hash 值,以及如何通过字符串 hash 序列来求解子串。那么如何通过字符串 hash 值来验证回文串呢。
上文讲过回文串验证是通过比较子串的正序 hash 值与逆序 hash 值是否相等来求得的。
如字符串"121"
,其正序哈希值为121
,逆序 hash 值也为121
,所以"121"
是回文串。而"123"
的正序 hash 值为123
,逆序 hash 值为321
,所以"123"
不是回文串。
所以除了正序Hash[10]
序列之外,我们还需要求出一个"-1234546789"
的逆序 hash 序列Hash_reverse[10]
,该求法与以上正序 hash 序列求法相同。
int slen = 9; //字符串长度为9
Hash_reverse[0] = 0;
for (int i = 1; i < 10; i++)
{
Hash_reverse[i] = Hash_reverse[i-1] * 10 + (s[slen-i+1] - '0');
}
生成的逆序 hash 序列如下:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
0 | 9 | 98 | 987 | 9876 | 98765 | 987654 | 9876543 | 98765432 | 987654321 |
现在我们得到了字符串"123456789"
的正序与逆序的hash 序列。
要验证子串s[l:r]
是否为回文串,只需求得s[l:r]
与s[r:l]
的 hash 值,相比较即可。
举个例子,我们要求字符串s
中的第 3 到 5 位(即"345"
)是否为回文串,通过Hash[5] - Hash[2]*10^3
即可求得"345"
的 hash 值345
, Hash_reverse[7]-Hash_reverse[4]*10^3
即可求得"345"
的逆 hash 值为543
。 345 != 543
,所以"345"
不是回文串。
求子串s[l:r]
的算法如下:
int hs = Hash[r] - Hash[l-1] * 10^(r-l+1); // s[l:r]的hash值
int hs_reverse = Hash_reverse[slen-l+1] - Hash_reverse[slen-r]*10^(r-l+1);//s[r:l]的hash值
if(hs == hs_reverse)
printf("s[l:r]是回文串\n");
else
printf("s[l:r]不是回文串\n");
至此,字符串hash求解回文串的算法已经介绍完毕。下面是这个题的hash解法的完整代码:
#include <bits/stdc++.h>
using namespace std;
// 通过字符串hash方法来多次求回文串,只需要O(n+q)时间复杂度,暴力解法为O(n*q) //
const unsigned long long int base = 2333; //base表示进制,用一个大质数作为进制减少hash碰撞
const int N = 1e6 + 3;
int Hash[N], Hash_reverse[N], power[N];
char s[N];
int main()
{
int q, l, r;
scanf("%s %d", s + 1, &q);
int slen = strlen(s + 1);
// 初始化hash和逆hash数组, 以及表示base进制位数的power数组
// 数组从1开始存储字符串和哈希值,Hash[0]为功能位,方便计算
Hash[0] = 0;
Hash_reverse[0] = 0;
power[0] = 1;
// power存放的是进制位数,方便计算。以十进制为例power[3]即为10^3.
// 此题我们选的进制是质数2333,power[3]即为2333^3
for (int i = 1; i < slen + 1; i++)
{
power[i] = power[i - 1] * base;
}
// 计算字符串的hash值,时间复杂度只需O(n)
for (int i = 1; i < slen + 1; i++)
{
Hash[i] = Hash[i - 1] * base + s[i] - 'a' + 1; // 加1是为了让hash值不为0,如果前面某位hash值为0,将会影响后面hash值的计算
Hash_reverse[i] = Hash_reverse[i - 1] * base + s[slen - i + 1] - 'a' + 1;
}
// 下面通过哈希值来判断回文串,每次判断只需要O(1)时间复杂度
for (int i = 0; i < q; i++)
{
scanf("%d %d", &l, &r);
if ((Hash[r] - Hash[l - 1] * power[r - l + 1]) == (Hash_reverse[slen - l + 1] - Hash_reverse[slen - r] * power[r - l + 1]))
printf("YES\n");
else
printf("NO\n");
}
return 0;
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。