(五)字符串算法

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\) 就好了。

[模板] AC 自动机(简单版)

询问时,考虑若一个子串能匹配成功,那么它的 \(Fail\) 一定也能匹配成功,于是我们不断的跳 \(Fail\) 就可以统计不同字符串的出现次数了。

评测记录 - 洛谷

[模板] AC 自动机(加强版)

求最大出现次数,一种暴力的方法就是类似于上题直接跳 \(fail\) 数组,但是由于不打标记,时间复杂度最坏情况下并不能通过(好像数据很水,这样写也能过)。

考虑如下结论,所有 \(fail\) 指针构成了一棵树。

因为 \(trie\) 根节点没有 \(fail\) 指针,剩下 \(n-1\) 个指针恰构成 \(n-1\) 条边。\(n\) 个节点,\(n-1\) 条边且图连通就是一棵树了。

于是每次就只用在当前节点打上标记,最后再用树形 \(DP\) 跑一遍就好了。

[模板] AC 自动机(二次加强版)

和上题差不多,加个 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;
		}
	};
};
posted @ 2022-10-16 13:31  mklzc  阅读(34)  评论(0编辑  收藏  举报