由字符串全家桶入门到字符串全家桶直僵僵地镶嵌在门框里。
Hash 翻译为散列表或杂凑函数,音译为哈希,也称 Hash 表。
散列表一般由 Hash 函数和链表结构共同实现完成。
——我不知道来源。
哈希目的是把一些数据范围很大的数据(整数)或者描述保存比较复杂(字符串)利用 Hash 函数把信息映射到一个范围比较小容易维护的范围内(有点类似离散化),由于映射后的值域范围变小,有可能造成不同的原有不同信息映射为相同的值,造成冲突(映射中的多对一,一对一最理想但是有时候比较困难)。当然没有冲突是最理想的,但是关键在于 Hash 函数的选择,我们的目标是尽量减少冲突或没有冲突。
——我也不知道来源。
一个简单的例子,我们要查找一个集合内的所有元素,朴素的做法是一个一个遍历,而我们通过按照每个数的个位数字分类保存,再使用链表查找,效率会更高。

此时,按照个位数分类保存就是一个哈希函数 。但是我们会发现,这种方式会发现多个数的哈希值相同,即存在很多冲突。如果 ,就会发现冲突少了很多。
解决哈希冲突两种常见的方法是闭散列和开散列。
也叫开放地址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置(因为定义哈希表时大小肯定不能少于原始数据的个数),那么可以把 key 存放到冲突位置中的“下一个” 空位置中去。
——我还不知道来源。
寻找下一个空位置的方法一般有两种,即线性探测和二次探测。
从哈希函数确定的位置依次向后移动 个位置,直到找到空位置为止。
从哈希函数确定的位置依次向后移动 个位置,直到找到空位置为止。
当然这些不是重点,重点在后面。
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。这种方法类似图的邻接点存储,常用数组模拟。目前在实际应用中都是用这种方法。
——The Same…
其实说人话就是通过像上面 引入 部分的图一样,通过对于每个 Hash 冲突的 值建立链表。
如 ,哈希函数为 ,则哈希表可用下图表示:

哈希表通常使用结构体定义,要分别记录哈希值、每个哈希值对应的数的个数和链表中下一个的位置,与邻接表(链式前向星)基本一致。
1 | |
哈希的程序主要包含如下三个板块:哈希值计算、插入一个值、查找一个值。
哈希值的计算即为正常的取模运算。
也与邻接表加边操作基本一致,不同之处在于如果碰到哈希值相同,应该使 。
1 | |
遍历链表,当 时,直接返回元素个数即 ,否则返回 。
1 | |
已知方程如下:
其中: 是未知数, 是系数,且方程中的所有数均为正整数。
求这个方程的正整数解的个数 。
对于 的数据,。
考虑哈希。
将原式移项得:
通过枚举左式所有可能的情况,并将左式的结果记录在一个哈希表中,然后枚举右式的所有可能情况,查找哈希表中是否有这种情况的结果。
时间复杂度 。
1 | |
还有一道板子 P3370 【模板】字符串哈希,太板了就不写了。
未完待续……
未完待续……
KMP 算法是一种改进的字符串匹配算法,由 D.E.Knuth,J.H.Morris 和 V.R.Pratt 提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称 KMP 算法)。KMP 算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。
——Baidu Baike。
设模式串 长度为 ,主串 长度为 。
最暴力的方法显然是遍历 ,逐位匹配 的前缀,当前缀长度为 时,即为成功匹配。
复杂度显然是 。
考虑这样一组匹配:
1 | |
前四位均匹配成功,匹配第五位时发现失配。这时,我们直接将 模式串 向右移动三位,如下所示:
1 | |
此时模式串完全匹配成功。
这种算法一定是快速的,所以算法关键部分就是移动位数的计算。
Border 在 KMP 算法的定义是:字符串 的一个非 本身的子串 ,满足 既是 的前缀,又是 的后缀的 的最大长度。
设 为主串 的指针,从 开始遍历; 为此时前后缀的长度。
先手算模拟大致过程,以主串为 为例:
| 子串 | ||
|---|---|---|
| 1 | b | 0 |
| 2 | b b | 1 |
| 3 | bba | 0 |
| 4 | bbac | 0 |
| 5 | b bac b | 1 |
| 6 | bb ac bb | 2 |
| 7 | bb acb bb | 2 |
Border 数组即为所有 。同时,不难发现, 是后缀最后一位的下标(如果有), 是前缀最后一位的下标。
接下来就是 Border 数组应该如何计算的问题:
我们以 为例,此时子串末尾增加了一个 ,,即此时 。得出结论当 时,,同时 ;
当 时,子串末尾增加了一个 ,根据上文的算法, 与 匹配失败。但此时 不能直接设为 ,因为整个字串的后缀还可能匹配其前缀(也就是后缀的后缀可以匹配到对应前缀)。
有一个性质 如果一个模式串的后缀匹配了一个前缀,那么这个后缀的后缀一定在这个前缀中出现了,因此对于某个等于后缀的前缀,它就出现在了这个前缀中。 即 Border 的 Border 还是 Border。
通过这个性质,我们可以发现,在这种情况下,我们可以通过将 跳到 上,继续向后查找。
这部分的代码,本质上是通过 串自己和自己匹配实现的:
1 | |
匹配的部分与 Border 计算一样,只不过 Border 是匹配 的前缀和 后缀,而匹配则是比较 和 。
代码:
1 | |
模板题,上文两部分结合
1 | |
匹配时当前匹配位置每次增加 ,也就是说一共的增加量就到 ,跳 Border 的减少也只能减少这么多,所以就是 。
——zrz
是的它不能让你直接 AC 但能让你自动 WA。
AC 自动机其实可以理解成在字典树上运用 KMP 的思想进行字符串匹配。
KMP 是求单字符串的匹配,而 AC 自动机是求多字符串匹配一个字符串。
暴力的方法显然是有多少个字符串跑多少遍 KMP,但明显效率极低。而 AC 自动机,则是将所有的模式串构建成一颗 Trie 树。
比如模式串为 ,则可以构建如下图的一棵 Trie 树。

如果一个模式串的后缀是任何一个模式串的前缀,则将该后缀的 Fail 指针指向该前缀。
形式化的,即若 ,则 节点的字符串是 节点的字符串的一个后缀。
也就是说,Fail 指针是指 最长的/可以在树上找到的/当前字符串的后缀/的末尾/的下标。例如,在上图中,。
其实看起来与 KMP 中 Border 类似,只不过 AC 自动机是查找所有模式串中的 Border。
接下来看如何求 Fail。
首先可以确定的一点是,所有第二层的节点的 Fail 一定指向 root,因为没有长度比 还小的字符串。
另外,如果一个点的父亲的 Fail 指针(即 )指向的节点有与当前点相同的字符 ,则当前点的 直接指向 ,因为每次求出的 Fail 都是最长的,那最长的 Fail 加一个节点也是最长的。
而上文就表现出来一个问题,就是当求 Fail 时,需要知道当前节点父亲的 Fail,也就是说,我们需要 BFS 层次遍历来求 Fail。
我们可以建立一个 号节点,将其所有儿子指向 ,然后将 的 Fail 指向 。
1 | |
还是像 KMP 一样,如果可以匹配那就继续,如果不能匹配就跳 Fail。同时,我们可以把所有经过节点的 设为 ,表示已经经过该节点。
1 | |
模板题,上文两部分结合
1 | |
用于在 的时间复杂度内寻找字符串 中最长回文子串的长度。
从 枚举一个中间点 ,然后以此枚举最大可行的回文串长度,最后计算答案。
这样做法的时间复杂度显然是 的,并不够优秀。
不难发现,在上文的定义中,回文串长度的奇偶性可能会对算法造成一定影响, 为了统一和方便计算,需要在字符串两两字符之间插入不属于 字符集的字符。易证此时回文子串长度(包含插入的间隔字符)长度一定为奇数。下文的回文串均默认长度为奇数。
定义一个回文串的半径 ,左右端点下标分别为 。
参照朴素算法的思路,对于每一个位置 ,我们仍需要计算极大的 ,例如, 的 序列如下:
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| ~ | # | a | # | b | # | b | # | a | # | |
| 1 | 1 | 2 | 1 | 2 | 5 | 2 | 1 | 2 | 1 |
如果已经计算出 ,其中最大的 即为答案。
现在问题转化为如何高效地计算 。注意到回文串具有一个极其优美的性质:若我们已知 是一个回文串的中心, 也是一个回文串的中心,由于回文的对称,我们可以知道 也是一个合法的回文串(如图)。通过这一操作可以减少重复检查。

现在假设下一步将计算 ,而 已经被计算完成,同时维护已找到的最靠右的回文子串的边界 (即具有最大 值的回文串,称此回文串 是极大的)。容易想到可以通过下面的方式实现:

注意到每次计算时 是单调不降的,且每个字符扩展比较的成本是常数。可以证明整个扫描过程中右边界最多只会被更新 次,因此总体比较次数是线性的。
1 | |
由字符串全家桶入门到字符串全家桶直僵僵地镶嵌在门框里。
Hash 翻译为散列表或杂凑函数,音译为哈希,也称 Hash 表。
散列表一般由 Hash 函数和链表结构共同实现完成。
——我不知道来源。
哈希目的是把一些数据范围很大的数据(整数)或者描述保存比较复杂(字符串)利用 Hash 函数把信息映射到一个范围比较小容易维护的范围内(有点类似离散化),由于映射后的值域范围变小,有可能造成不同的原有不同信息映射为相同的值,造成冲突(映射中的多对一,一对一最理想但是有时候比较困难)。当然没有冲突是最理想的,但是关键在于 Hash 函数的选择,我们的目标是尽量减少冲突或没有冲突。
——我也不知道来源。
一个简单的例子,我们要查找一个集合内的所有元素,朴素的做法是一个一个遍历,而我们通过按照每个数的个位数字分类保存,再使用链表查找,效率会更高。

此时,按照个位数分类保存就是一个哈希函数 。但是我们会发现,这种方式会发现多个数的哈希值相同,即存在很多冲突。如果 ,就会发现冲突少了很多。
解决哈希冲突两种常见的方法是闭散列和开散列。
也叫开放地址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置(因为定义哈希表时大小肯定不能少于原始数据的个数),那么可以把 key 存放到冲突位置中的“下一个” 空位置中去。
——我还不知道来源。
寻找下一个空位置的方法一般有两种,即线性探测和二次探测。
从哈希函数确定的位置依次向后移动 个位置,直到找到空位置为止。
从哈希函数确定的位置依次向后移动 个位置,直到找到空位置为止。
当然这些不是重点,重点在后面。
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。这种方法类似图的邻接点存储,常用数组模拟。目前在实际应用中都是用这种方法。
——The Same…
其实说人话就是通过像上面 引入 部分的图一样,通过对于每个 Hash 冲突的 值建立链表。
如 ,哈希函数为 ,则哈希表可用下图表示:

哈希表通常使用结构体定义,要分别记录哈希值、每个哈希值对应的数的个数和链表中下一个的位置,与邻接表(链式前向星)基本一致。
1 | |
哈希的程序主要包含如下三个板块:哈希值计算、插入一个值、查找一个值。
哈希值的计算即为正常的取模运算。
也与邻接表加边操作基本一致,不同之处在于如果碰到哈希值相同,应该使 。
1 | |
遍历链表,当 时,直接返回元素个数即 ,否则返回 。
1 | |
已知方程如下:
其中: 是未知数, 是系数,且方程中的所有数均为正整数。
求这个方程的正整数解的个数 。
对于 的数据,。
考虑哈希。
将原式移项得:
通过枚举左式所有可能的情况,并将左式的结果记录在一个哈希表中,然后枚举右式的所有可能情况,查找哈希表中是否有这种情况的结果。
时间复杂度 。
1 | |
还有一道板子 P3370 【模板】字符串哈希,太板了就不写了。
未完待续……
未完待续……
KMP 算法是一种改进的字符串匹配算法,由 D.E.Knuth,J.H.Morris 和 V.R.Pratt 提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称 KMP 算法)。KMP 算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。
——Baidu Baike。
设模式串 长度为 ,主串 长度为 。
最暴力的方法显然是遍历 ,逐位匹配 的前缀,当前缀长度为 时,即为成功匹配。
复杂度显然是 。
考虑这样一组匹配:
1 | |
前四位均匹配成功,匹配第五位时发现失配。这时,我们直接将 模式串 向右移动三位,如下所示:
1 | |
此时模式串完全匹配成功。
这种算法一定是快速的,所以算法关键部分就是移动位数的计算。
Border 在 KMP 算法的定义是:字符串 的一个非 本身的子串 ,满足 既是 的前缀,又是 的后缀的 的最大长度。
设 为主串 的指针,从 开始遍历; 为此时前后缀的长度。
先手算模拟大致过程,以主串为 为例:
| 子串 | ||
|---|---|---|
| 1 | b | 0 |
| 2 | b b | 1 |
| 3 | bba | 0 |
| 4 | bbac | 0 |
| 5 | b bac b | 1 |
| 6 | bb ac bb | 2 |
| 7 | bb acb bb | 2 |
Border 数组即为所有 。同时,不难发现, 是后缀最后一位的下标(如果有), 是前缀最后一位的下标。
接下来就是 Border 数组应该如何计算的问题:
我们以 为例,此时子串末尾增加了一个 ,,即此时 。得出结论当 时,,同时 ;
当 时,子串末尾增加了一个 ,根据上文的算法, 与 匹配失败。但此时 不能直接设为 ,因为整个字串的后缀还可能匹配其前缀(也就是后缀的后缀可以匹配到对应前缀)。
有一个性质 如果一个模式串的后缀匹配了一个前缀,那么这个后缀的后缀一定在这个前缀中出现了,因此对于某个等于后缀的前缀,它就出现在了这个前缀中。 即 Border 的 Border 还是 Border。
通过这个性质,我们可以发现,在这种情况下,我们可以通过将 跳到 上,继续向后查找。
这部分的代码,本质上是通过 串自己和自己匹配实现的:
1 | |
匹配的部分与 Border 计算一样,只不过 Border 是匹配 的前缀和 后缀,而匹配则是比较 和 。
代码:
1 | |
模板题,上文两部分结合
1 | |
匹配时当前匹配位置每次增加 ,也就是说一共的增加量就到 ,跳 Border 的减少也只能减少这么多,所以就是 。
——zrz
是的它不能让你直接 AC 但能让你自动 WA。
AC 自动机其实可以理解成在字典树上运用 KMP 的思想进行字符串匹配。
KMP 是求单字符串的匹配,而 AC 自动机是求多字符串匹配一个字符串。
暴力的方法显然是有多少个字符串跑多少遍 KMP,但明显效率极低。而 AC 自动机,则是将所有的模式串构建成一颗 Trie 树。
比如模式串为 ,则可以构建如下图的一棵 Trie 树。

如果一个模式串的后缀是任何一个模式串的前缀,则将该后缀的 Fail 指针指向该前缀。
形式化的,即若 ,则 节点的字符串是 节点的字符串的一个后缀。
也就是说,Fail 指针是指 最长的/可以在树上找到的/当前字符串的后缀/的末尾/的下标。例如,在上图中,。
其实看起来与 KMP 中 Border 类似,只不过 AC 自动机是查找所有模式串中的 Border。
接下来看如何求 Fail。
首先可以确定的一点是,所有第二层的节点的 Fail 一定指向 root,因为没有长度比 还小的字符串。
另外,如果一个点的父亲的 Fail 指针(即 )指向的节点有与当前点相同的字符 ,则当前点的 直接指向 ,因为每次求出的 Fail 都是最长的,那最长的 Fail 加一个节点也是最长的。
而上文就表现出来一个问题,就是当求 Fail 时,需要知道当前节点父亲的 Fail,也就是说,我们需要 BFS 层次遍历来求 Fail。
我们可以建立一个 号节点,将其所有儿子指向 ,然后将 的 Fail 指向 。
1 | |
还是像 KMP 一样,如果可以匹配那就继续,如果不能匹配就跳 Fail。同时,我们可以把所有经过节点的 设为 ,表示已经经过该节点。
1 | |
模板题,上文两部分结合
1 | |
用于在 的时间复杂度内寻找字符串 中最长回文子串的长度。
从 枚举一个中间点 ,然后以此枚举最大可行的回文串长度,最后计算答案。
这样做法的时间复杂度显然是 的,并不够优秀。
不难发现,在上文的定义中,回文串长度的奇偶性可能会对算法造成一定影响, 为了统一和方便计算,需要在字符串两两字符之间插入不属于 字符集的字符。易证此时回文子串长度(包含插入的间隔字符)长度一定为奇数。下文的回文串均默认长度为奇数。
定义一个回文串的半径 ,左右端点下标分别为 。
参照朴素算法的思路,对于每一个位置 ,我们仍需要计算极大的 ,例如, 的 序列如下:
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| ~ | # | a | # | b | # | b | # | a | # | |
| 1 | 1 | 2 | 1 | 2 | 5 | 2 | 1 | 2 | 1 |
如果已经计算出 ,其中最大的 即为答案。
现在问题转化为如何高效地计算 。注意到回文串具有一个极其优美的性质:若我们已知 是一个回文串的中心, 也是一个回文串的中心,由于回文的对称,我们可以知道 也是一个合法的回文串(如图)。通过这一操作可以减少重复检查。

现在假设下一步将计算 ,而 已经被计算完成,同时维护已找到的最靠右的回文子串的边界 (即具有最大 值的回文串,称此回文串 是极大的)。容易想到可以通过下面的方式实现:

注意到每次计算时 是单调不降的,且每个字符扩展比较的成本是常数。可以证明整个扫描过程中右边界最多只会被更新 次,因此总体比较次数是线性的。
1 | |