2019暑期集训第四讲:字符串基础
字符串基础
1.字符串匹配
1.暴力算法
1 | for (i = 0; i < n - m + 1; i++) { |
2.哈希
将string映射为int 每一个字符串代表一个数字 时间复杂度变为O(n+m)
转化方法可以采用
1 | ll hashs(char s[])//上述公式相当于把一个字符串看成一个b进制数 |
b与M应当是互质的(不然取余的时候直接约了)
在输入随机的情况下,这个求出的数在[0,M)上的值概率相等,所以单次比较重复的概率为
$\frac{1}{M}$ n次比较中出错的概率就为$1-(1-(1/M))^n$
当M>>n时 这个值接近$\frac{n}{M}$
一般来说我们直接用ULL(unsigned long long类型)来储存Hash值,因为这个类型在$2^{64}$溢出时会自动对$2^{64}$取模
最长公共子串
给出几个由小写字母构成的单词,求它们最长的公共子串的长度。
任务
- 从文件中读入单词
- 计算最长公共子串的长度
- 输出结果到文件
输入
文件的第一行是整数 n,1<=n<=5,表示单词的数量。接下来n行每行一个单词,只由小写字母组成,单词的长度至少为1,最大为2000。
输出:
仅一行,一个整数,最长公共子串的长度。
样例输入:
1 | 3 |
样例输出:
1 | 2 |
二分答案+哈希 或者 用后缀数组
哈希:(PS:map的[]是O(logn)的,所以这个过不了1e5的数据,如果n=2直接用vector就可以过1e5了)
1 |
|
3.KMP
前缀数组
一个字符串的真前缀是其前缀但不等于该字符串自身。
对于一个字符串s,其前缀函数为$\pi$,其中$\pi$[i]指即是s[0~i]的前缀又是该子串后缀的最长真前缀
abcaabcab的前缀函数为[0,0,0,1,1,2,3,4, 2]
aabaaab的前缀函数为[0,1,0,1,2,2,3]
前缀数组最直观的求法 O($n^3$)
1 | vector<int> prefix_function(string s) { |
KMP算法
观察前面的前缀函数例子,发现前缀函数后一位有三种情况
1.增加1(因为是最长真前缀,所以一定和之前一位只相差1)
2.保持不变
3.减少
观察减小的例子 如果当前字符s[i]与s[$\pi$[i-1]+1]不同,就不能加一
这时候我们去判断“前缀函数的前缀函数”即$\pi$[$\pi$[i-1]-1]
如果这时匹配了,那就加一
具体证明此处不赘述
1 | vector<int> prefix_function(string s) { |
$POJ 1961$
如果一个字符串 $S$ 是由一个字符串 $T$ 重复 $K$ 次形成的,则称 $T$ 为 $S$ 的循环元。使 $K$ 最大的字符串 $T$ 称为 $S$的最小循环元,此时的 $K$ 被称为最大的循环次数。
现在给定一个长度为 $N$ 的字符串 $S$ ,对 $S$ 的每一个前缀 $S[1$~$i]$ ,如果它的最大循环次数大于 $1$ ,则输出该前缀的最小循环长度和最大循环次数。
Sample Input
1 | 3 |
Sample Output
1 | Test case #1 |
$S[1$~ $i]$ 具有长度为了 $len$ < $i$ 的循环元的充要条件为 $len$ 能整除 $i$ 并且 $S[len + 1$~ $i] = S[1$~ $i - len]$
证明:
必要性 有长度为 $len$ 的循环元,那么显然 $len$ 能整除 $i$ 并且 $S[len + 1$~ $i]$ = $S[1$~ $i - len]$ 成立
充分性 $len$ 能整除 $i$ 并且 $S[len + 1$~ $i]$ = $S[1$~ $i - len]$ 成立。首先 $S[len + 1$~ $i]$ 和 $S[1$~ $i - len]$ 的长度不小于$len$ 且是 $len$ 的倍数。$S[len + 1$~ $i]$ 和 $S[1$~ $i - len]$ 以 $len$ 为长度错位对齐,以$len$ 为间隔取对应字符有
$S[len + 1$~ $2*len] = S[1$~ $len]$ ,以此类推得证。
$Codeforces - Anthem of Berland$
题意:给定一个包含?的字符串A,还有一个字符串B,问B最多可能在A中出现几次。
input
1 | winlose???winl???w??win |
output
1 | 5 |
DP+KMP
$dp[i]$ 表示 $0$ ~ $i$ 的字符串最多能出现几次 $B$
$cnt[i]$ 表示最后一次出现的以 $i$ 结尾 $B$ $0$ ~ $i$中出现 $B$ 的次数($A[i-len$~$i] = B$ )
求 $cnt[i]$ 可以用KMP
首先,不考虑最后一个B $cnt[i]$ 肯定大于等于 $cnt[i-lenB]$ 如果$i-lenB$ 前的某几个字符与 后面的字符也能凑成一个 $B$ 那么 $cnt[i]$ 就比 $cnt[i-lenB]$ 要大一 这样的情况就是求一下前缀数组的情况
例子 $A:abc|abcab$ $B:abcab$
具体来说就是
1 | cnt[i] = dp[i - lenB]; |
$codeforces$ $471D$ $MUH$ $and$ $Cube$ $Walls$
给定两个数组表示形状,问第二个数组的形状能否在第一个数组中找到.若能找到,求出能找到几次
kmp,因为只需要形状相同,不需要考虑每个值的实际大小,只需要使得数组的前后差值相等,就能使得上边界构成的形状相同。
求出两个数组的相邻数差值构成的新数组,然后利用kmp求第二个差值数组在第一个差值数组中出现的次数,注意到若第二个数组就只有一个数,则在第一个数组任意位置都能构成相同的形状因此此时输出n即可
1 |
|
2.最小表示法
给定一个字符串 $S$ , 如果我们不断把它的最后一个字符放到开头,最终会得到 $n$ 个字符串,称这 $n$ 个字符串是循环同构的。这些字符串中字典序最小的一个,称为字符串 $S$ 的最小表示。
用$B[i]$表示从 $i$ 开始的循环同构字符串,即 $S[i$~$n]+S[1$~$i-1]$。
首先,我们把这个字符串复制一份放到后面,记这个新的字符串为 $SS$ .
在对 $SS$ 比较的过程中如果在 $i + k$, $j + k$ 处发现不相等,假设 $SS[i+k] > SS[j+k]$ ,那么我们除了得知 $B[i]$ 不是最小表示,同时$B[i+1]$~$B[i+k]$ 也都不是。指针后移即可。
3.Trie(读作Try)
$trie[i][j] = k$, 编号为 $i$ 的节点第 $j$ 个孩子是编号为 $k$ 的节点, $i$ 与 $k$ 是建树时新插入的节点的编号
若都为大写或都为小写, $j$ 可取 $0-25$
1 | void insert(char *s){ |
1 | bool find(char *s){ |
$The$ $XOR$ $Largest$ $Pair$
给定 $N$ 个整数 $A_1,A_2,…,A_N$ 中选出两个进行异或运算,得到结果最大多少?
解:考虑一个数转化为二进制后,要使它与另一个数异或后值最大,那就是在每一个二进制位上取反
我们将每一个所给的数转化为32位的二进制数(补前导 $0$)然后建立字典树(低位在下),对每个数,我们寻找其上述每位求反的数是否存在,如果不存在只能找与其相同的,那这一位上的值变为 $0$,这样可以找到与$Ai$异或后值最大的 $A_j$
$POJ 3764$
给定一棵 $N$ 个节点的树, 树上的每一条边都有一个权值。从树中选择两个点 $x$ 和 $y$ ,把从 $x$ 到 $y$ 的路径上的所有边权 $xor$ 起来,得到的结果最大是多少?
首先,我们可以求出每个点 $x$ 到根节点的路径上的所有边权 $xor$ 值 记为 $D[x]$
题目所要求的值就等于 $D[x]xorD[y]$ 问题就变成了从 $D[1]$ ~ $D[N]$ 这 $N$ 个数中选两个数, $xor$ 的结果最大,就是前一道题。
4.Manacher
求回文子串
p数组保存以当前字符为对称轴的回文半径
因为有可能回文串长为偶数,所以对称轴不是字符而是字符之间的空隙
于是我们在每两个字符间插入一个不会出现的字符($Ascill$为 $0$ 比如说)
bob -> #b#o#b#
但是这样会出现新的问题
求回文串的时候会比较当前回文串前一个字符与后一个字符,这样就会访问到$s[-1]$ 这个位置导致越界
所以我们在开头再加入一个不会出现的且与之前插入的不同的字符($Ascill$为 $1$ 比如说)
然后是求 $p[i]$
1 | p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1; |
其中 $id$ 为当前所探索到的最右边那个回文串的对称轴位置
$mx$ 为最右端位置
如果 $mx > i$, 则 $p[i] = min( p[2 * id - i] , mx - i )$
否则,$p[i] = 1$
当 $mx - i > p[j]$ 的时候,以S[j]为中心的回文子串包含在以S[id]为中心的回文子串中,由于 $i$ 和 $j$ 对称,以$S[i]$为中心的回文子串必然包含在以$S[id]$为中心的回文子串中,所以必有 $p[i] = p[j]$,其中 $j = 2id - 1$,因为 $j$ 到 $id$ 之间到距离等于 $id$ 到 $i$ 之间到距离,为 $i - id$,所以 $j = id - (i - id) = 2id - i$
当 $p[j] >= mx - i$
的时候,以$S[j]$为中心的回文子串不一定完全包含于以$S[id]$为中心的回文子串中,但是基于对称性可知,下图中两个绿框所包围的部分是相同的,也就是说以$S[i]$为中心的回文子串,其向右至少会扩张到$mx$的位置,也就是说
$p[i] = mx - i$。至于$mx$之后的部分是否对称,只能老老实实去匹配了
1 | string t = "$#"; |
题意:求这样一个回文串S,S = A + B 且 A, B都是回文串。问最长S
我们可以定义这么两个东西
1.:l[i]代表i位置所在回文串中的最右端的位置
2.:r[i]代表i位置所在回文串中的最左端的位置
一个从左到右遍历,一个从右到左,所以i所在的回文串不一定是同一个
我们可以算出来这个东西。
因此通过左右拼接就可以得到我们的双回文串了
1 | for(int i=0;i<len;i++) |
5.exkmp(Z函数)
假设我们有一个长度为 $N$ 的字符串 $s$,该字符串的 Z函数 为一个长度为 $n$ 的数组,其中第 $i$ 个元素为满足从位置 $i$ 开始且为 $s$ 前缀的字符串的最大长度。
即, $z[i]$ 为 $s$ 和从 $i$ 开始的 $s$ 的后缀的最大公共前缀长度。
假设数组第一位为0
样例:
$Z(aaaaa) = [0,1,2,3,4]$
$Z(aaabaab) = [0,2,1,0,2,1,0]$
$Z(abacaba) = [0,0,1,0,3,0,1]$
朴素算法(O($n^2$))
1 | vector<int> z_function_trivial(string s) { |
高效算法(线性)
$z$ 数组的定义为取得最大的 $z[i]$ 使得 $s[0;z[i]-1] = s[i;i+z[i]-1]$
假设当前我们已经知道$s[0;r-l] = s[l;r]$ (;表示~)
可知$s[i-l;r-l] = s[i;r]$
然后可得$s[i-l;i-l+z[i-l]-1] = s[i;i+z[i-l]-1]$
那么 $z[i]$ 就至少等于 $z[i-l]$
当然还要注意,r后面的情况我们还不知道
所以$z[i] = min(z[i-1], r-i+1)$
然后再进行匹配 这样相当于让 $z[i]$ 赋了一个初值
至于为什么这样子会把时间复杂度降为 $O(n)$ 有兴趣的话可以自行百度
1 | vector<int> z_function(string s) { |
在一个字符串s中找一个子串,这个子串要符合的条件有三点,第一可以说这个子串是s的前缀,第二也可以说这个子串是s的后缀,第三也可以说这个子串既不是s的前缀也不是s的后缀。
1 |
|
例题
POJ2185
给你一个字符矩阵,求出它的最小覆盖子矩阵,即使得这个子矩阵的无限复制扩张之后的矩阵,能包含原来的矩阵。 即二维的最小覆盖子串。
一看这题,容易想出一种很直观的做法:求出每一行的最小覆盖子串长度,取所有行的最小覆盖子串长度的最小公倍数为宽;对列也同样操作求出高。由于POJ测试数据水了,所以过了。
但是,让我们来看一下下面这种情况
Input
1 | 2 8 |
Output
1 | 12 |
第一行的最小覆盖子串长度为6,第二行为5 那么答案应该是60
但是第二行取6时 答案就为12
由于数据范围是R (1 <= R <= 10,000) C (1 <= C <= 75)那么直接暴力枚举重复子串的长度c,每列逐一检查是否与当前枚举的重复子串相对应。因此求宽度这个部分的时间复杂度为$O(R*C^2)$。
1 | int get_width() { |
确定高度,把一行看成一个整体,行与行之间作KMP
1 | int get_height() { |