哈希
字符串哈希
哈希是一种高效判断字符串相等的算法,准确来说是子串,因为预处理需要 \(O(n)\),自然是多次使用才划算
常见的哈希式是 \(\sum a_ibase^i (mod\ mod)\)
即选取一个 \(base\),将字符串当做 \(base\) 进制数,对一个大模数取模,可以认为冲突是可以忽略的
注意当精度要求很高时,可以尝试扩大 \(base\) 以及模数的范围,尤其是模数,甚至可以使用双哈希
当然,一般情况下开个 \(unsigned\) \(long\) \(long\) 是没有问题的~
注意 \(hash\) 的过程默认串头是高位,那么如果要截取某一段的哈希值直接前缀和相减是不行的,应该遵循对位相减,即把前半部分乘上 \(po[len]\),使得最高位与最高位对齐
P3667 [USACO17OPEN]Bovine Genomics G
发现问题具有单调性,那么直接二分答案,扫描每一个长度为 \(mid\) 的区间,然后哈希 \(O(1)\) 判断相等
P7469 [NOI Online 2021 提高组] 积木小赛
枚举每个左端点,然后右端点每次右移的时候贪心地选取一个最近的放进序列里,由于决策是单调的,双指针扫描即可
哈希用于子串去重
发现比较区间,那么可以考虑哈希
运用哈希可以算出 \(a\) 加上任何 \(x\) 的值
考虑一个 \(x\) 对 \(a\) 加上后在 \(b\) 中的匹配,那么可以考虑与 \(x-1\) 的对比,相当于在子序列中加入 \(n+x\),减少 \(x-1\),那么可以用线段树动态维护这件事
好吧,似乎通配符匹配类问题常常使用 \(dp\)
然而这个 \(dp\) 是有讲究的,发现通配符数量小于 \(10\)?
那么按照通配符一块一块设计状态就好的
设 \(f[i][j]\) 表示前 \(i\) 的通配符位置之前的串与文本串匹配到 \(j\) 位置可不可行
用哈希判断能否转移即可
虽然这道题的主角不是哈希,但是思路非常新颖
因为要判断连续多行能否沿着一个轴对折,那么可以先将每一行处理出一个哈希值,然后对哈希值跑 \(manacher\) 即可
比如这道题在进行 \(dp\) 转移时可以利用哈希实现任意串的相等判断
注意哈希是对顺序敏感的,所以前缀后缀哈希都得处理
坑人的数据结构题
首先树的形态是固定的,那么如果到时候想抽取树上一条链的时候想到维护每个点到根节点的哈希值,用的时候对位相减即可
由于操作序列是动态的,想到用线段树动态维护序列的哈希值,然后在序列上进行二分,得出来的序列去树上匹配
注意剪枝,树上如果开 \(map\) 过不去,直接二分也过不去,因为序列上二分出来长度已知,那么放到树上深度已知,在同深度的节点里二分
注意这题根节点不是1!!!
当然,哈希也不局限于字符串,其往往作为一种状态的刻画,可以进行 \(dp\) 或快速比较等
一种非常巧妙的刻画数量相等的方式是存储每个元素与第一个元素的差值
那么一个区间合法就相当于所有元素差值均为零
而等于零就等价于 \(w(r)=w(l)\),因此很方便地可以在哈希表中寻找
考虑怎样存储这个哈希,由于有不出现的元素,但是可以发现对于同一个右端点来说,一个元素出现了就不会再消失,因此只有 \(8\) 种可能的状态,只记录这些即可
AT4163 [ARC099D] Eating Symbols Hard
这个序列可以用哈希和指针刻画出来
维护一个位数指针 \(p\),加减对应加减 \(p\),乘除对应移动 \(p\) 即乘除 \(base\)
哈希内容逐渐向抽象化进化
创新哈希
当然,哈希并不是只能用作比较简单的相等比较,对于一些特定情形,对于题目给出的重定义相等,巧妙地设计哈希可以很方便地实现相等比较
- 不知道哪儿的题
本质不同定义为长度不同或按字典序最小重标号后的字符串不同
\(update\):现在知道是娜儿的题然而上不去了……
可以发现当前这个字符串是什么不重要,重要的是每种字符在区间内的分布情况
那么可以设计 \(\sum (i-last_{a_i})* p^i\) 作为哈希值,可以发现契合题目要求
可以发现这道题的本质定义和顺序有关,然而原本哈希值的定义是与顺序有关的
可以设计这样的哈希值 \(\sum p^{a_i}\),这样与顺序无关且容易线段树维护
这道题也是类似的,设计为 \(\sum ibase^{rank}\),用平衡树动态维护 \(rank\) 并合并即可
随机+异或的哈希模式非常常见
其应用场景是如果想要刻画出一个集合的所有元素,那么对于每个元素随机出一个大数,集合的权值就是所有元素值得亦或和
可以发现这样做的哈希可以实现很方便地实现集合加减等运算
康托展开
对于全排列,有一种特殊的哈希方式——康托展开
康托展开可以 \(nlogn\) 求出其在全排列中字典序的排名,可以 \(n^2\) 还原
先放式子:\(rk=\sum_{i=1}^n sum_{i}(n-i)!+1\),其中 \(sum_i\) 表示 \(i\) 后面比 \(a_i\) 小的数有几个
那么倒着扫一遍用树状数组维护即可
分析一下本质就可以推导出逆展开的规则
相当于是以阶乘为进制的一种数位规则
那么每次除 \((n-i)!\),相当于算出有几个数比它小,那么扫一遍未选的数看看第 \(w\) 小即可
之后将值模 \((n-i)!\) 即可
或许写棵平衡树可以做到 \(nlogn\)?
放个代码:
void change(int a[],int val){
memset(vis,0,sizeof vis);
for(int i=1;i<=n;i++){
int w=val/frac[n-i];
val%=frac[n-i];
for(int j=1;j<=n;j++){
if(!vis[j]){
if(!w){
a[i]=j;
vis[j]=true;
break;
}
w--;
}
}
}
}
比如这道题就是纯天然的一种恰到好处的应用