(五)字符串算法
kmp算法
\(kmp\) 是在 \(O(n+m)\) 的时间复杂度内求解模式串匹配问题的算法。
大致思路如下:定义 \(nxt[i]\) 表示 \(s[0\sim i]\) 子串使得前 \(k\) 个字符恰等于后 \(k\) 个字符的最大的 \(k\) ,其中 \(k\neq i+1\) 。发现若在某一位失配过后,我们可以令模式串等于该位置的 \(nxt\) 数组值即可。同理 \(nxt\) 数组也可以在线性时间内求出。
void kmp()
{
int la = strlen(a + 1), lb = strlen(b + 1);
for (int i = 2, j = 0; i <= lb; i++) {
while (j > 0 && b[i] != b[j + 1]) j = nxt[j];
if (b[i] == b[j + 1]) j++;
nxt[i] = j;
}
for (int i = 1, j = 0; i <= la; i++) {
while (j > 0 && a[i] != b[j + 1]) j = nxt[j];
if (a[i] == b[j + 1]) j++;
if (j == lb) {
printf("%d\n", i - lb + 1);
j = nxt[j];
}
}
for (int i = 1; i <= lb; i++)
printf("%d ", nxt[i]);
}
扩展kmp(z 函数)
\(exkmp\) 算法:
定义一个字符串的 \(z\) 函数数组为:它与它的每个后缀的 \(LCP\) 长度。
可以做到在线性时间内递推求得 \(z\) 函数数组。
(推导下标从 \(0\) 开始)
设当前要求的为 \(z[x]\),设 \(k\) 为使得 \(\forall i\in[1, x),i+z[i]-1\) 最大的 \(k\),那么以下代码实现就不难得出了。
void get_z(std::string s)
{
int sz = s.size(), k = 0;
for (int i = 1; i < sz; i++)
{
if (k + z[k] > i) z[i] = std::min(z[i - k], k + z[k] - i);
while (i + z[i] < sz && s[i + z[i]] == s[z[i]]) z[i]++;
if (k + z[k] < i + z[i]) k = i;
}
z[0] = sz;
}
字符串哈希
将任意长度的字符串映射为一个非负整数,冲突概率几乎为零。
\(Hash\) 过程:首先将字符串化为一个非负整数(常见的方法有用 \(ACSII\) 码代替所有字符)。将这个非负整数看做一个 \(P\) 进制数,并对 \(M\) 取模。这里的 \(P\) 与 \(M\) 都是我们自己选取的一个合适的数值,一般可以取 \(P=13331, M=2^{64}\) 。用 \(\text{unsigned long long}\) 存储可以避免低效的取模运算。
若我们已知字符串 \(S\) 的哈希值为 \(H(S)\),字符串 \(S+T\) 哈希值为 \(H(S+T)\),那么字符串 \(T\) 的哈希值就是 \(H(T)=H(S+T)-H(S)*p^{len(T)}\pmod M\)。
于是我们可以 \(O(N)\) 预处理所有前缀的哈希值,\(O(1)\) 查询任意子串的哈希值。
AC 自动机
输出包含 mm 行,依次为删除每个元素之前,逆序对的个数。
\(AC\) 自动机通过结合 \(trie\) 和 \(kmp\) 匹配算法来进行多模式匹配。
一个节点 \(u\) 定义为从根节点到其的字符串,也称其为状态。
失配指针的构建:\(AC\) 自动机的 \(Fail\) 指针定义为当前状态 \(u\) 的存在于 \(trie\) 树中的最长后缀。
欲求当前状态 \(u\) 的 \(Fail\) 值,设 \(u\) 的父节点为 \(p\) 。若 \(tr[Fail[p]][ch]\) 存在,那么令 \(Fail[u]=tr[Fail[p]][ch]\),相当于在其父状态上添一个字符。
否则就令 \(tr[u][ch]\) 指向 \(tr[Fail[u]][ch]\) 。
最后再套一个 \(BFS\) 就好了。
询问时,考虑若一个子串能匹配成功,那么它的 \(Fail\) 一定也能匹配成功,于是我们不断的跳 \(Fail\) 就可以统计不同字符串的出现次数了。
求最大出现次数,一种暴力的方法就是类似于上题直接跳 \(fail\) 数组,但是由于不打标记,时间复杂度最坏情况下并不能通过(好像数据很水,这样写也能过)。
考虑如下结论,所有 \(fail\) 指针构成了一棵树。
因为 \(trie\) 根节点没有 \(fail\) 指针,剩下 \(n-1\) 个指针恰构成 \(n-1\) 条边。\(n\) 个节点,\(n-1\) 条边且图连通就是一棵树了。
于是每次就只用在当前节点打上标记,最后再用树形 \(DP\) 跑一遍就好了。
和上题差不多,加个 unordered_map
判重就可以了。
字符串模板全家桶
然鹅,并没有保证正确性,只是单纯地打了一遍。
#include <cstring>
#include <cstdio>
#include <queue>
#include <iostream>
namespace string_algorithm {
struct kmp {
static const int lim = 1e6;
int nxt[lim + 5];
void __kmp(std::string a, std::string b) {
int az = a.size(), bz = b.size(), cnt = 0;
nxt[0] = -1;
for (int i = 0, j = -1; j < bz; j++) {
while (~j && b[j + 1] != b[i]) j = nxt[j];
if (b[j + 1] == b[i]) j++;
nxt[i] = j;
}
for (int i = 0, j = -1; j < bz; j++) {
while (~j && b[j + 1] != a[i]) j = nxt[j];
if (b[j + 1] == a[i]) j++;
if (j == bz) cnt++, j = nxt[j];
}
}
};
struct exkmp {
static const int Maxn = 1e7 + 5;
int z[Maxn];
void exkmp_getz(std::string s) {
int sz = s.size(), k = 0;
for (int i = 1; i < sz; i++) {
if (k + z[k] > i) z[i] = std::min(k + z[k] - i, z[i - k]);
while (i + z[i] < sz && s[z[i]] == s[i + z[i]]) z[i]++;
if (i + z[i] > k + z[k]) k = i;
}
z[0] = sz;
}
};
struct hash {
static const int Maxn = 1e6 + 5;
unsigned long long hash[Maxn], pw[Maxn];
const int P = 13331;
void Init_hash(std::string s) {
hash[0] = int(s[0]), pw[0] = 1;
for (int i = 1, sz = s.size(); i < sz; i++) {
hash[i] = hash[i - 1] * P + int(s[i]);
pw[i] = pw[i - 1] * P;
}
}
unsigned long long get_hash(int l, int r) {
if (!l) return hash[r];
else return hash[r] - hash[l - 1] * pw[r - l + 1];
}
};
struct trie {
static const int Maxn = 1e4 + 5;
int tr[Maxn * 50][26], exist[Maxn * 50], idx;
void insert(std::string s) {
int p = 1, len = s.size();
for (int i = 0; i < s.size(); i++) {
int ch = s[i] - 'a';
if (!tr[p][ch]) tr[p][ch] = ++idx;
p = tr[p][ch];
}
if (!exist[p]) exist[p] = 1;
}
bool query(std::string s) {
int p = 1, len = s.size();
for (int i = 0; i < s.size(); i++) {
int ch = s[i] - 'a';
if (!tr[p][ch]) return false;
p = tr[p][ch];
}
return exist[p];
}
};
struct AC_automation {
static const int lim = 1e6;
int tr[lim + 5][26], exists[lim + 5], fail[lim + 5], n, idx;
void insert(std::string &s) {
int l = int(s.length()), p = 0;
for (int i = 0; i < l; i++) {
int ch = s[i] - 'a';
if (!tr[p][ch]) tr[p][ch] = ++idx;
p = tr[p][ch];
}
exists[p]++;
}
void get_fail() {
std::queue<int> q;
for (int i = 0; i < 26; i++)
if (tr[0][i]) q.push(tr[0][i]);
while (!q.empty()) {
int p = q.front();
q.pop();
for (int i = 0; i < 26; i++) {
if (tr[p][i]) {
q.push(tr[p][i]);
fail[tr[p][i]] = tr[fail[p]][i];
}
else tr[p][i] = tr[fail[p]][i];
}
}
}
int query(std::string &t) {
int l = int(t.length()), p = 0, res = 0;
for (int i = 0; i < l; i++) {
p = tr[p][t[i] - 'a'];
for (int j = p; j && ~exists[j]; j = fail[j]) {
res += exists[j];
exists[j] = -1;
}
}
return res;
}
};
};