目录:
1.进制hash
2.查询子串的Hash值
3.用hash求最长回文子串 / 回文子串数
1.进制hash
作用:
把字符赋予进制和模数,将每一个字符串映射为一个小于模数数字。
思路:
首先设一个进制数base,并设一个模数mod,
进制哈希就是把一个串转化为一个值,这个值是base进制的,
储存在哈希表中,注意一下在存入的时候取模一下即可。
解决hash冲突:
(1)用一个大质数当模数
(2)双模数哈希:
(通过设置两个不同的哈希方式,对于一个字符串,
当且仅当两个哈希值都相同时才判定相同。)
对一个hash值用两个不同的质数进行两次mod操作,
然后最后用一对数<hash1[n],hash2[n]>来表示一个字符串的哈希值,
这样的一对数的重复几率加上选择较大的质数,冲突率几乎为0。
(3)自然溢出法:
对于自然溢出方法,我们定义Base 而MOD对于自然溢出方法,
就是 unsigned long long 整数的自然溢出
这种方法是利用数据结构unsigned long long的范围自然溢出:
即当存储的数据大于unsigned long long的存储范围时,
会自动mod 2^64−1,就不用mod其他质数来保证唯一性了。
代码实现:
#define base 233317
#define inf 212370440130137957(LL)
ull mod = inf;
ull hasha(char s[]) {
ll ans = 0, len = strlen(s);
for (ll i = 0; i < len; i++) {
ans = (ans * base + (ull)s[i]) % mod;
//枚举该字符串的每一位,与base相乘,转化为base进制,
//加(ull)是为了防止爆栈搞出一个负数,(ull)是无符号的,
//但其实加了一个ull是可以不用mod的,加个mod更保险
//然而加了mod会很玄学,莫名比不加mod慢
}
return ans;
}
# 【模板】字符串哈希
## 题目描述
如题,给定N个字符串(第i个字符串长度为M_i,字符串内包含数字、大小写字母,大小写敏感),请求出 $N$ 个字符串中共有多少个不同的字符串。
** 友情提醒:如果真的想好好练习哈希的话,请自觉。**
## 输入格式
第一行包含一个整数 $N$,为字符串的个数。
接下来 $N$ 行每行包含一个字符串,为所提供的字符串。
## 输出格式
输出包含一行,包含一个整数,为不同的字符串个数。
## 样例 #1
### 样例输入 #1
5
abc
aaaa
abc
abcc
12345
### 样例输出 #1
4
#include<iostream>
#include<algorithm>
const int Maxn = 100010;
typedef unsigned long long ull ;
typedef long long ll;
using namespace std;
char s[Maxn];
ull key[Maxn]; //建立unsigned long long 的数组防止溢出
const ull base = 233317;
int sum=0;
ull Hash(string s) {
ull ans = 0; ll len = s.size();
for (ll i = 0; i < len; i++) { //自然溢出法
ans = (ans * base) + (ull)s[i];
}
return ans; //得到hash地址的解
}
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> s;
key[i]=Hash(s);
}
sort(key, key + n);
for (int i = 0; i < n; i++) {
if (key[i] != key[i + 1]) //比较得到不同值
sum++;
}
cout << sum << endl;
return 0;
}
2.查询子串的Hash值
设一个字符串的前缀hash值记为h[i],
我们hash时使用的进制数为base,那么显然 h[i] = h[i−1]∗base + s[i]
p[i]表示base的i次方,那么我们可以通过这种方式O(1)得到一个子串的hash值
(设这个子串为s[l]...s[r])
代码实现:
typedef unsigned long long ull;
ull get_hash(int l, int r) {
return h[r] - h[l - 1] * p[r - l + 1];
}
原因:
进行进制哈希的过程本质上就是把原先得到的哈希值在base进制上强行左移一位,
然后放进去当前的这个字符。
现在的目的是,取出l到r这段子串的hash值,也就是说,
h[l−1]这一段是没有用的,我们把在h[r]这一位上,
h[l−1]这堆字符串的hash值做的左移运算全部还原给h[l−1],
就可以知道h[l−1]在h[r]中的hash值,那么减去即可。
[Baltic OI 2014 Day1] Three Friends
题目描述
有一个字符串 S,对他进行操作:
1.将 S 复制为两份,存在字符串 T 中
2.在 T 的某一位置上插入一个字符,得到字符串 U
现在给定 U,求 S。
如果不能通过上述的步骤从 S 推到 U,输出 NOT POSSIBLE。
如果从 U 得到的 S 不是唯一的,输出 NOT UNIQUE。
否则,输出一个字符串 S。
思路:
现在有一个字符串s,每次询问它的一个子串删除其中一个字符后的hash值
(删除的字符时给定的)
类比上面的做法,我们可以先O(1)得到区间[l, x−1]和区间[x + 1, r]的hash值,
那么现在要做的事情就是把这两段拼起来了,由于使用的是进制hash,
强行将前面的区间强行左移r−x位(这么看可能会好理解一点r−(x + 1) + 1)就好。
typedef unsigned long long ull;
ull get_hash(int l, int r) {
return h[r] - h[l - 1] * p[r - l + 1];
}
ull get_s(int l, int r, int x) {
return get_hash(l, x - 1) * p[r - x] + get_hash(x + 1, r);
}
整体实现:
#include<iostream>
#include<map>
const int MAXN = 2000005;
typedef unsigned long long ull;
using namespace std;
long long n, sum = 0, ans, x;
ull Hash[MAXN], s[MAXN];
string u;
map<long long, bool> flag;
void init()
{
s[0] = 1;
for (long long i = 1; i < n; i++)
{
s[i] = s[i - 1] * 31;
//*31是为了减少hash冲突
}
}
void HASH(string u) {
for (long long i = 0; i < n; i++)
{
Hash[i + 1] = Hash[i] * 31 + (ull)(u[i] - 'A' + 1);
}
}
int main()
{
cin >> n;
cin >> u;
if (!(n % 2))
{
printf("NOT POSSIBLE");
return 0;
}
//因为S翻倍后要插入一个字符,也就是奇数,
//所以如果是偶数就输出NOT POSSIBLE,并结束
init();
HASH(u);
x = n >> 1;
for (long long i = 1; i <= x; i++)
{
if (Hash[x + 1] - Hash[i] * s[x + 1 - i] + Hash[i - 1] * s[x + 1 - i] == Hash[n] - Hash[x + 1] * s[x] && !flag[Hash[n] - Hash[x + 1] * s[x]])
{
sum++;
//找到了插入的字符也就自然有一个答案
ans = i;
flag[Hash[n] - Hash[x + 1] * s[x]] = 1;
//为了后面不再重复找到,flag便置为1,后面便进不了循环
}
}
//插入的字符在前面一半之间,则S(也就是答案)就在后面一半,
//就在前面枚举字符的位置,然后根据 hash值来判断是否相等;
if (Hash[x] == Hash[n] - Hash[x + 1] * s[x] && !flag[Hash[x]])
{
sum++;
ans = x + 1;
flag[Hash[x]] = 1;
}
for (long long i = x + 2; i <= n; i++)
{
if (Hash[n] - Hash[i] * s[n - i] + (Hash[i - 1] - Hash[x] * s[i - 1 - x]) * s[n - i] == Hash[x] && !flag[Hash[x]])
{
sum++;
ans = i;
flag[Hash[x]] = 1;
}
}
//在后面则相反
if (!sum)
{
cout << "NOT POSSIBLE" << endl;
}
//没有答案输出NOT POSSIBLE
else
{
if (sum > 1)
{
cout << "NOT UNIQUE" << endl;
}
//如果答案不唯一,输出NOT UNIQUE
else
{
if (ans <= x + 1)
{
for (long long i = x + 2; i <= n; i++)
{
cout<< u[i - 1];
}
}
//如果ans比U的一半少就输出后面一半
else
{
for (long long i = 0; i < x; i++)
{
cout<<u[i];
}
}
//否则相反
}
}
return 0;
}
3.用hash求最长回文子串 / 回文子串数
回文子串是具有单调性的——二分解决
#include<iostream>
#include<algorithm>
using namespace std;
typedef unsigned long long ull;
#define N 10100
#define base 13131
char s[N];
ull h1[N], p[N], h2[N], ans = 0;
int n;
ull gh1(int l, int r) { return h1[r] - h1[l - 1] * p[r - l + 1]; }
ull gh2(int l, int r) { return h2[l] - h2[r + 1] * p[r - l + 1]; }
ull query1(int x) { //奇
int l = 1, r = min(x, n - x);
while (l <= r) {
int mid = (l + r) >> 1;
if (gh1(x - mid, x + mid) == gh2(x - mid, x + mid)) l = mid + 1;
else r = mid - 1;
}
return r;
}
ull query2(int x) { //偶
int l = 1, r = min(x, n - x);
while (l <= r) {
int mid = (l + r) >> 1;
if (gh1(x - mid + 1, x + mid) == gh2(x - mid + 1, x + mid)) l = mid + 1;
else r = mid - 1;
}
return r;
}
int main() {
scanf("%s", s + 1); p[0] = 1;
n = strlen(s + 1);
for (int i = 1; i <= n; ++i) {
h1[i] = h1[i - 1] * base + s[i];
p[i] = p[i - 1] * base;
}
for (int i = n; i; i--) h2[i] = h2[i + 1] * base + s[i];
for (int i = 1; i < n; ++i) {
ans += query1(i) + query2(i);
}
printf("%llu\n", ans + n);
}