前言

此文主要介绍hash的各种乱搞方法,hash入门请参照我之前这篇文章

不好意思hash真的可以为所欲为

在开头先放一下题表(其实就是我题解中的hash题目qwq)

查询子串hash值

必备的入门操作,因为OI中用到的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值,那么减去即可。(简单的容斥思想)

这是基本操作,现在来看一个这个的拓展问题。

题意

现在有一个字符串\(s\),每次询问它的一个子串删除其中一个字符后的hash值(删除的字符时给定的)

要求必须\(O(1)\)回答询问

Sol

删除操作?那不能像上面那样子简单粗暴的来搞了,但是其实本质上是一样的。

假设我们现在询问的区间为\([l,r]\),删除的字符为\(x\)(指位置,不是字符)

类比上面的做法,我们可以先\(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);
}

这题的原题是LOJ#2823. 「BalticOI 2014 Day 1」三个朋友 ,需要分类讨论一下,不过知道上面这个也就不难了

用hash求最长回文子串/回文子串数

最长回文子串!我知道!马拉车!可以\(O(n)\)!

可是如果你马拉车写挂了呢?或者像我一样不会马拉车

这时候就得靠hash来水分了

我们知道,回文子串是具有单调性的

如果字符串s[l...r]为回文子串,那么s[x...y](l<x,y<r)也一定是回文子串

单调性!我们是不是可以二分?

我们暂时只讨论长度为奇数的回文子串。(事实上,长度为偶数的回文子串与奇数的只是处理上的一些细节不同,仅此而已)

考虑枚举回文子串的中点,并二分回文子串的长度(不过一般来说,二分回文子串的长度的1/2可能会更好写一点),那么我们使用上文提到的\(O(1)\)查询子串hash值的方法,就可以\(O(1)\)判断二分得到的这个子串是不是回文子串了。

对于长度为偶数的回文子串,枚举中点左边/右边的字符即可

效率是\(O(nlogn)\)的,复杂度较马拉车算法比较逊色,不过如果马拉车算法打挂或者是时间复杂度允许的情况下,hash也是一个不错的选择。

然后还有一种方法,适合像我这种下标总是搞错的,可以直接处理出正串和反串的hash值,然后每次根据二分出来的长度计算整个字符串的起止,判断正串和反串的hash值是否相等即可。(这样就不用研究恶心的下标了...研究下标还得分奇偶讨论...)

字符串的很多特性是具有单调性的,二分求解是一个常见的思路,配合哈希进行判断操作一般可以做到在\(O(nlogn)\)效率内完成问题

例题:SP7586 NUMOFPAL - Number of Palindromes

练习:LOJ#2452. 「POI2010」反对称 Antisymmetry

例题代码

#include<bits/stdc++.h>
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);
}

用hash代替kmp算法

关于kmp算法,可以看pks大佬的blog,讲的真的很好!

但是我们这里不讲kmp算法,我们利用hash来代替kmp算法求解单模式串匹配问题。

但是kmp算法的next数组真的很妙!可以解决很多神奇的东西,强烈推荐去学学!

好了,步入正题。

单模式串匹配问题是什么?

给出两个字符串\(s1\)和\(s2\),其中\(s2\)为\(s1\)的子串,求\(s2\)在\(s1\)中出现多少次/出现的位置。

如果有认真看过该篇文章的第一子目的话,应该不难想到这题的hash做法。

具体做法是预处理出来两个串的hash值,因为求的是\(s2\)在\(s1\)中出现的次数,所以我们要匹配的长度被压缩到了\(s2\)的长度,所以我们只需要枚举\(s2\)在\(s1\)中的起点,看看后面一段长度为\(len\)的区间的hash值和\(s2\)的hash值一不一样就好。

时间复杂度是\(O(n+m)\)的!和kmp算法一样!

例题:LOJ #103. 子串查找 (本来想放洛谷的结果要输出next数组就没办法了23333)

练习:UVA10298 Power Strings

例题代码

#include <bits/stdc++.h>
using namespace std; #define N 1000010
#define ull unsigned long long
#define base 233 ull h[N], p[N], ha;
char s1[N], s2[N]; int main() {
scanf("%s%s", s1 + 1, s2 + 1);
int n = strlen(s1 + 1), m = strlen(s2 + 1);
for(int i = 1; i <= m; ++i) ha = ha * base + (ull)s2[i];
p[0] = 1;
for(int i = 1; i <= n; ++i) {
h[i] = h[i - 1] * base + (ull)s1[i];
p[i] = p[i - 1] * base;
}
int l = 1, r = m, ans = 0;
while(r <= n) {
if(h[r] - h[l - 1] * p[m] == ha) ++ans;
++l, ++r;
}
printf("%d\n", ans);
}

用hash代替其他一些字符串算法

因为博主并没有写过,所以并不打算深入讲(没写过不熟悉啊...)

这一子目会分析一下hash还能代替哪些算法以及使用hash算法代替的复杂度是多少

manacher算法

求最长回文串/回文串个数manacher算法是可以做到\(O(n)\)的

使用hash+二分可以做到\(O(nlogn)\),并且实现简单

kmp算法

进行单模式串匹配可以使用hash进行

复杂度\(O(n+m)\),kmp算法复杂度也是\(O(n+m)\)。但是kmp的next数组可以做到一些hash做不到的事情。

上面两个是前面两子目分析过的。

AC自动机

多模式串匹配:求文本串中各个模式串出现了多少次。

设文本串的长度为\(n\),模式串的总长度为\(len\),模式串的个数为\(m\)

hash出文本串中每个子串,并存入一个map中,复杂度是\(O(n^2logn)\)的(用map主要是便于查询)。然后hash出每个模式串,复杂度是\(O(len)\)的。

对每个模式串,查询对应的map中文本串的子串的个数即可。复杂度\(O(mlogn)\)

总复杂度是\(O(n^2logn+len+mlogn)\)

这个\(log\)可以去掉的(自行写个哈希表)。

所以并没有什么用...还是用AC自动机实在。

用AC自动机可以做到\(O(n+len)\)

后缀数组

求后缀数组中的SA数组。(如果不知道请自行百度)(给定的串为S)

最暴力的做法是直接对每个后缀进行排序,并逐字符匹配,这样会达到\(O(n^2logn)\)

那么有没有不这么无脑的做法?

有!有个hash+二分的神仙做法可以做到\(O(nlognlogn)\)

我们处理出整个串S的hash值。

在排序中对两个子串进行排序的过程中,采用二分找相同的前缀(比较用hash,可以\(O(1)\)),那么设我们最后二分到的值为r,则直接比较\(s[x+r+1]\)和\(s[y+r+1]\)的大小即可(设子串1的起点为\(x\),子串2的起点为\(y\))。这样每次比较的复杂度就是\(O(logn)\)了。

加上排序,总的复杂度为\(O(nlognlogn)\)

并且其实还能求出height数组的,但是我自己对height数组的理解也不大行,所以这里就不讨论这个。

而后缀数组的复杂度是\(O(nlogn)\)(使用倍增法)

后缀数组这部分主要参考自李煜东的《算法竞赛进阶指南》。

使用hash的几个要注意的地方

在复杂度允许的情况下,尽量采用多hash(不过一般双hash就够)

比赛时能不用自然溢出就不要(平时刷题如果用自然溢出被卡可以及时换掉,但是比赛时如果用自然溢出,OI赛制就GG了)

模数用大质数这个不用说了

并且进制数不要选太简单的,比如\(233\)和\(13131\)这样的,尽量大一点,比如\(13131\)和\(233333\)。太小容易被卡。

以及要合理应对各种卡hash方法的最好方法就是自己去卡一遍hash,详情请参考BZOJ hash killer系列。

hash进阶:使用字符串hash乱搞的姿势的更多相关文章

  1. 转载:字符串hash总结(hash是一门优雅的暴力!)

    转载自:远航休息栈 字符串Hash总结 Hash是什么意思呢?某度翻译告诉我们: hash 英[hæʃ] 美[hæʃ]n. 剁碎的食物; #号; 蔬菜肉丁;vt. 把…弄乱; 切碎; 反复推敲; 搞糟 ...

  2. cdoj1092-韩爷的梦 (字符串hash)【hash】

    http://acm.uestc.edu.cn/#/problem/show/1092 韩爷的梦 Time Limit: 200/100MS (Java/Others)     Memory Limi ...

  3. 转载:字符串HASH

    转载自:Slager_Z 字符串Hash总结 Hash是什么意思呢?某度翻译告诉我们: hash 英[hæʃ] 美[hæʃ]n. 剁碎的食物; #号; 蔬菜肉丁;vt. 把…弄乱; 切碎; 反复推敲; ...

  4. 转载 字符串hash

    转载自:http://www.cnblogs.com/jiu0821/p/4554352.html 求一个字符串的hash值: •现在我们希望找到一个hash函数,使得每一个字符串都能够映射到一个整数 ...

  5. poj 3461 字符串单串匹配--KMP或者字符串HASH

    http://poj.org/problem?id=3461 先来一发KMP算法: #include <cstdio> #include <cstring> #include ...

  6. 【BZOJ-3578】GTY的人类基因组计划2 set + map + Hash 乱搞

    3578: GTY的人类基因组计划2 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 367  Solved: 159[Submit][Status][ ...

  7. 【NOI2013模拟】坑带的树(仙人球的同构+圆方树乱搞+计数+HASH)

    [NOI2013模拟]坑带的树 题意: 求\(n\)个点,\(m\)条边的同构仙人球个数. \(n\le 1000\) 这是一道怎么看怎么不可做的题. 这种题,肯定是圆方树啦~ 好,那么首先转为广义圆 ...

  8. LA4671 K-neighbor substrings(FFT + 字符串Hash)

    题目 Source http://acm.hust.edu.cn/vjudge/problem/19225 Description The Hamming distance between two s ...

  9. 字符串hash入门

    简单介绍一下字符串hash 相信大家对于hash都不陌生 翻译过来就是搞砸,乱搞的意思嘛 hash算法广泛应用于计算机的各类领域,像什么md5,文件效验,磁力链接 等等都会用到hash算法 在信息学奥 ...

随机推荐

  1. scu 4444 Travel

    题意: 一个完全图,有n个点,其中m条边是权值为a的无向边,其它是权值为b的无向边,问从1到n的最短路. 思路: 首先判断1和n被哪种边连通. 如果是被a连通,那么就需要全部走b的边到达n,选择最小的 ...

  2. maven工程的common模块jar上传至仓库并被其它模块依赖

    .parent pom和common pom 都需要添加 <distributionManagement> <repository> <id>nexus</i ...

  3. 从网站上扒网页,保存为file文件格式

    保存下来的页面总是有部分特效缺失,可是文件包里已经有好几个js文件了. 例如想保存易迅的搜索页面,条件筛选栏的按钮全部失效了,按钮-更多.多选等 都没有反应,搜索结果的鼠标悬浮显示完整信息也没有了. ...

  4. linux常用命令:mkdir 命令

    linux mkdir 命令用来创建指定的名称的目录,要求创建目录的用户在当前目录中具有写权限,并且指定的目录名不能是当前目录中已有的目录. 1.命令格式: mkdir [选项] 目录... 2.命令 ...

  5. mybatis源码解析5---SqlSession解析

    由之前解析可知,mybatis启动的时候会加载XML配置文件解析生成全局配置对象Configuration对象,SqlSessionFactoryBuilder类会根据Configuration对象创 ...

  6. 转:【专题十一】实现一个基于FTP协议的程序——文件上传下载器

    引言: 在这个专题将为大家揭开下FTP这个协议的面纱,其实学习知识和生活中的例子都是很相通的,就拿这个专题来说,要了解FTP协议然后根据FTP协议实现一个文件下载器,就和和追MM是差不多的过程的,相信 ...

  7. node.js学习(一)

    一.assert assert.deepEqual(actual, expected[, message]) 测试 actual 参数与 expected 参数是否深度相等. 原始值使用相等运算符(= ...

  8. 利用vue写一个复选框的组件

    HTML <multicheck :source=tlist :busValue='objInfo.tt' @getTt="getTtInfo"></multic ...

  9. The Little Prince-12/01

    The Little Prince-12/01 The people have no imagination. They repeat whatever one says to them… On my ...

  10. SpringMVC MultiActionController 默认方法名解析器

    MultiActionController默认方法名解析器是指在请求的地址中加入指定方法名称 MultiActionController类具有一个属性methodNameResolver,方法名解析器 ...