注:LP = Luogu Problem
,MP = MarsOJ Problem
,AP = Acwing Problem
。
子串一定连续,子序列不一定连续。
常用 C 库函数(仅适用于 C 风格字符串 char
数组):strlen
, strcmp
, strcat
, sscanf
, sprintf
。
strcat
把第二个字符串拼到第一个的结尾。
sscanf()
:第一个参数为读取内容,后续参数与正常 scanf
相同。
char s[] = "apple 5 13"; char t[10]; int a, b; sscanf("%c %d %d", t, &a, &b);
常用 C++ 库函数(仅适用于 std::string
):+
, find(ch, start)
, substr(start, len)
, append(s)
, replace(pos, n, s)
, erase(pos, n)
, insert(pos, s)
, c_str()
。
find(ch, start)
查找从 start
开始的字符 ch
的位置。
substr(start, len)
注意第二个参数是子串长度不是子串末尾。第二个参数不填则默认为取到字符串结尾。
c_str()
应用场景:把 std::string
转成 C 风格字符串,再使用 printf
进行输出。
string s, t; int a, b; printf("%s=%d&%s+%d", s.c_str(), a, t.c_str(), b);
模式匹配问题(KMP):判断字符串 s2 是否为字符串 s1 的子串。要搞清楚 border
和 nxt
数组。
border
:字符串最大公共前后缀。最大公共前后缀不能是字符串本身(真前缀、真后缀)。
nxt[i]
:前缀 i 的 border
长度。怎么快速地求 nxt
数组呢?递推!
当 s[i+1] == s[nxt[i]+1]
,nxt[i+1] = nxt[i]+1
。
当 s[i+1] != s[nxt[i]+1]
,找前缀 i 的所有公共前后缀:
int j = nxt[i]; while (j!=0 && s[i+1] != s[j+1]) j=nxt[i]; if (s[i+1] == s[j+1]) nxt[i+1] = j+1;
KMP 复杂度为 O(n+m)。KMP 是个很抽象的算法。必须要画图,抽象,再充分理解。
KMP 应用 AP4678:求字符串的最小周期(最后一个周期不一定循环完)。答案为 n-nxt[n]
。画图理解。
例题 MP3308:给定字符串 S,T,问 T 最少由几个 S 的前缀拼接而成。(AT_abc257_g)
贪心解(正解一):每次匹配 S,T 的最长公共前缀,匹配成功,就从 T 中删去该前缀,直到不能删为止。删完,删的次数即为最优解;删不完,-1。
DP 解:dp[i]
表示把 T_1-T_i 可以分割成最少的多少个 S 的前缀。dp_i=\min(dp_j+1),其中 j<i 且 T 的第 j+1 到第 i 位是 i 的前缀。
DP 优化(正解二):反证法易得 DP 单调不降。发现 DP 过程中就是要维护以 S_i 结尾,和 T 前缀相同的最大长度。假设 KMP 枚举到 s_i 时,匹配长度为 len,则 dp_i \leftarrow dp_{i-len}。
字符串哈希Hash:和比较有关的字符串题目,第一时间想字符串哈希!
在 O(n) 预处理后,O(1) 判断两个子串是否相等。(映射值相同,两字符串大概率相同)
把字符串看作 k 进制数(k\geq 128),利用数学方法取出当中的一段值,转成十进制,就是那一段的哈希值。
怎么快速算出中间一段对应的 k 进制数的十进制?
计算所有前缀对应的哈希值 hash[i]
,递推式 has[i] = (has[i-1] * k + s[i]) % mod;
区间 [l,r] 对应的字符串的哈希值(前缀和思想)(has[r]-has[l-1]*qpow[k,r-l+1] % mod + mod) % mod
如果这个数太大怎么办?对一个大质数取余(998244353,10^9+7,10^9+9)
冲突怎么办?如果 n\leq 10^5,无需考虑;如果 n\geq 10^6,做双哈希:一个模 998244353,一个模 10^9+7,如果两个字符串的两个哈希值对应相等,就判定为相等,大大降低判断错误的概率,无需担心呢。
Hash应用:最长公共子串。给定 m 个总长不超过 n 的字符串,查找所有字符串的最长公共子串。
答案具有单调性(存在长度为 k 的,必定存在长度为 k-1 的公共子串),二分子串长度 len。判断答案时,O(n) 枚举每一个字符串长度为 len 的子串,并求出哈希值。看这些哈希值的交集是否为空集。
Trie树(字典树):前缀数据结构,用于存储字符串集合。节点没有权值,树边有权值(权值是字符串中的字符)用一棵树形结构维护字符串集合。字符串 = 根节点到叶子节点的路径。就是 26 叉树。
Trie 应用 MP3350:已知 m 条信息,n 条暗号,问对于每条暗号 j,有多少条信息 i 与其有相同前缀,且前缀长度为两个字符串长度的最小值。解法:创建关于暗号的 Trie 树,在字典树上维护 cnt[u]
表示经过节点 u 的字符串个数,拿着每一条暗号在上面跑,走到一个节点就把 cnt[u]
加入答案。
Manacher 算法:求最长回文子串。动态规划思想。
显然,每一个回文串都会有一个“回文中心”。显然可以枚举回文中心,二分求解“最大回文半径”,哈希判断左半段与右半段是否相等即可。回文中心可以是一个字符,也可以是两个字符中间的空隙。O(n\log n)。
由于回文中心可能是空隙,还得奇偶处理,很麻烦,所以给字符串两两字符中间以及串首尾出插入一个未出现过的字符,如 #
。设 d_i 表示以新串 i 位置为回文中心的回文串的最大半径,即处理前回文串长度。答案 \max d_i。
考虑递推/DP求 d_i。
char s[N], ch[N]; int m, n; void manachar() { int mxr = 0, id; for (int i = 1; i <= n; i ++) { if (mxr >= i) p[i] = min(mxr - i, p[2*id-i]); else p[i] = 0; while (ch[i+p[i]+1]==ch[i-p[i]-1]) p[i]++; if(p[i]+i>mxr)id=i,mxr=p[i]+i; } } void init() { m = strlen(s + 1); n = 2*m+1; ch[0]='('; for(int i=1;i<=m;i++) { ch[2*i-1]='#'; ch[2*i]=s[i]; } ch[n]='#'; ch[n+1]=')'; }