【C++】字符串|匹配类问题模板

这一篇文章主要涉及到在一个主字符串中匹配是否出现某个/某几个需要寻找的字符串等问题的算法。

字典树 (Trie)

字典树是一种很常用的树形数据结构。相当于将字符串压缩存储。

字典树的应用还是非常广泛的,除了基础的压缩存储字符串外还能用于异或操作和AC 自动机。

基本模板

题目:P2580 于是他错误的点名开始了https://www.luogu.com.cn/problem/P2580

代码默认为存26个小写字母,如果要存更多字符修改vector第二维大小和j的处理即可。

C++
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

using namespace std;
vector<vector<int>> trie;
vector<int> cnt;
int pid = 0;

void insert(string& s)
{
	int pos = 0;
	for (auto& i : s)
	{
		int j = i - 'a';
		if (!trie[pos][j])
		{
			trie[pos][j] = ++pid;
			trie.emplace_back(vector<int>(26));
			cnt.emplace_back(0);
		}
		pos = trie[pos][j];
	}
	cnt[pos]++;
}

int query(string& s)
{
	int pos = 0;
	for (auto& i : s)
	{
		int j = i - 'a';
		if (!trie[pos][j]) return 0;
		pos = trie[pos][j];
	}
	int res = cnt[pos]++;
	return res;
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	int n, m;
	string tmp;
	trie.emplace_back(vector<int>(26));
	cnt.emplace_back(0);
	cin >> n;
	while (n--)
	{
		cin >> tmp;
		insert(tmp);
	}
	cin >> m;
	while (m--)
	{
		cin >> tmp;
		int q = query(tmp);
		if (q == 1) cout << "OK" << endl;
		else if (q > 1) cout << "REPEAT" << endl;
		else cout << "WRONG" << endl;
	}
	return 0;
}

01-trie

将数的二进制表示看做一个字符串,就可以建出字符集为 {0,1} 的 trie 树。

题目:P10471 最大异或对 The XOR Largest Pairhttps://www.luogu.com.cn/problem/P10471

使用贪心思想:想要异或大,就按位尽量取不同的数字。

C++
#include <iostream>
#include <vector>
#include <algorithm>
#include <bitset>

using namespace std;
vector<vector<int>> trie;
int pid = 0;

void insert(int n)
{
	bitset<31> bs(n);
	int pos = 0;
	for (int i = 30; i >= 0; i--)
	{
		if (!trie[pos][bs[i]])
		{
			trie[pos][bs[i]] = ++pid;
			trie.emplace_back(vector<int>(2));
		}
		pos = trie[pos][bs[i]];
	}
}

int query(int n)
{
	bitset<31> bs(n), res;
	int pos = 0;
	for (int i = 30; i >= 0; i--)
	{
		if (trie[pos][!bs[i]])
		{
			res[i] = 1;
			pos = trie[pos][!bs[i]];
		}
		else pos = trie[pos][bs[i]];
	}
	return res.to_ulong();
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	trie.emplace_back(vector<int>(2));
	int n, ans = 0;
	cin >> n;
	vector<int> arr(n);
	for (auto& i : arr) cin >> i;
	for (auto& i : arr) insert(i);
	for (auto& i : arr) ans = max(ans, query(i));
	cout << ans << endl;
	return 0;
}

KMP 算法

该算法由 Knuth、Pratt 和 Morris 在 1977 年共同发布,是一种可以在 O(n) 的时间内查找某一个字符串(模式串)在主串内所有出现位置的贪心算法。

目前网上主要流行着3~4类实现,由于博主学了luogu/oi-wiki上的版本然后被数据结构老师用其他版本狠狠拷打了一次,所以决定把所有版本都记录一下。

首先来大致说明一下各版本之间的区别:其实仅仅是next数组的定义上不太一样。

在原论文/数据结构(严蔚敏)版中:next[j]指的是前j-1位的最长的相等的真前缀与真后缀的长度。而这两者均采用了1作为字符串起始下标(因此产生了同学以0为起始下标的版本)

在luogu/oi-wiki版中:next[j]指的是前j位的最长的相等的真前缀与真后缀的长度。

同时,也存在着在此基础上继续优化重合部分的nextval版本(其实原论文中就将此称为next数组,但是为了方便理解我们按照数据结构教材来)

如果上面的解释成功让你一头雾水了的话,我们直接举个例子:

字符串abababcaa
下标(1-based)123456789
next[j](原论文版)011234512
next[j](竞赛版)001234011
next[j](同学版)-100123401

可以看出,在同学版的基础上每一位加1就可以得到教材中的版本(其实就是从0-based升一位到1-based)。而luogu/oi-wiki的版本右移一位就是同学的版本(因为竞赛版算第j位时考虑1~j,领先同学版的算j位考虑1~j-1一位)

那么基于next数组可以进一步求出nextval数组。即当next跳回后如果字符仍相同,可以继续跳的情况下,直接一步跳到next多次跳到的最终位置。

字符串abababcaa
下标(1-based)123456789
next[j](原论文版)011234512
nextval[j]010101502

题目:P3375 【模板】KMPhttps://www.luogu.com.cn/problem/P3375

注意:luogu中KMP模板题目采用的是和oi-wiki一致的next[j]定义,即1~j

OI-WIKI版

C++
// KMP oi-wiki 版
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

using namespace std;

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	string s1, s2;
	cin >> s1 >> s2;
	s1.insert(s1.begin(), '#');
	s2.insert(s2.begin(), '#');
	vector<int> next(s2.size());
	for (int i = 2, j = 0; i < s2.size(); i++)
	{
		while (j && s2[i] != s2[j + 1]) j = next[j];
		if (s2[i] == s2[j + 1]) j++;
		next[i] = j;
	}
	for (int i = 1, j = 0; i < s1.size(); i++)
	{
		while (j && s1[i] != s2[j + 1]) j = next[j];
		if (s1[i] == s2[j + 1]) j++;
		if (j == s2.size() - 1)
		{
			cout << i - j + 1 << endl;
			j = next[j];
		}
	}
	for (int i = 1; i < s2.size(); i++)
	{
		cout << next[i] << " ";
	}
	cout << endl;
	return 0;
}

其中18-23行与以下写法等效:

C++
int i = 2, j = 0;
while (i < s2.size())
{
	if (!j || s2[i] == s2[j + 1])
	{
		if (s2[i] == s2[j + 1]) j++;
		next[i] = j;
		i++;
	}
	else j = next[j];
}

原论文/教材基础版

C++
// KMP 教材版
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

using namespace std;

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	string s1, s2;
	cin >> s1 >> s2;
	s1.insert(s1.begin(), '$');
	s2.insert(s2.begin(), '$');
	vector<int> next(s2.size());
	int i = 1, j = 0;
	while (i < s2.size())
	{
		if (!j || s2[i] == s2[j])
		{
			i++, j++;
			next[i] = j;
		}
		else j = next[j];
	}
	i = 1, j = 1;
	while (i < s1.size() && j < s2.size())
	{
		if (!j || s1[i] == s2[j]) i++, j++;
		else j = next[j];
		if (j > s2.size() - 1)
		{
			cout << i - j + 1 << endl;
			j = next[j];
		}
	}
	return 0;
}

原论文/教材升级版

即基于nextval的版本,nextval是通过当前位前面的next求出的,因此可以通过修改next计算过程直接求出。

C++
// KMP 教材版
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

using namespace std;

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	string s1, s2;
	cin >> s1 >> s2;
	s1.insert(s1.begin(), '$');
	s2.insert(s2.begin(), '$');
	vector<int> nextval(s2.size());
	int i = 1, j = 0;
	while (i < s2.size())
	{
		if (!j || s2[i] == s2[j])
		{
			i++, j++;
			if (s2[i] != s2[j]) nextval[i] = j;
			else nextval[i] = nextval[j];
		}
		else j = nextval[j];
	}
	i = 1, j = 1;
	while (i < s1.size() && j < s2.size())
	{
		if (!j || s1[i] == s2[j]) i++, j++;
		else j = nextval[j];
		if (j > s2.size() - 1)
		{
			cout << i - j + 1 << endl;
			j = nextval[j];
		}
	}
	return 0;
}

Z 函数 (扩展 KMP)

Z 函数定义:对于一个长度为 n 的字符串 s,定义函数 z[i] 表示 s 和 s[i,n](即以 s[i] 开头的后缀)的最长公共前缀(LCP)的长度。

规定 z[1]=n (字符串本身的长度)。

字符串abacaba
下标(1-based)1234567
z[i]7010301

存在一种线性计算的方法,使用一种盒子(Z-box)加速计算。

题目:P5410 【模板】扩展 KMP/exKMP(Z 函数)https://www.luogu.com.cn/problem/P5410

C++
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

using namespace std;
using ll = long long;

ll calc(vector<int>& arr)
{
	ll res = 0;
	for (ll i = 1; i < arr.size(); i++)
	{
		res ^= (i * (arr[i] + 1));
	}
	return res;
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	string sa, sb;
	cin >> sa >> sb;
	int n = sa.size(), m = sb.size();
	sa.insert(sa.begin(), '%');
	sb.insert(sb.begin(), '%');
	vector<int> z(sb.size()), p(sa.size());
	z[1] = sb.size() - 1;
	for (int i = 2, l = 0, r = 0; i <= m; i++)
	{
		if (i <= r) z[i] = min(z[i - l + 1], r - i + 1);
		while (i + z[i] <= m && sb[1 + z[i]] == sb[i + z[i]]) z[i]++;
		if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
	}
	cout << calc(z) << endl;
	for (int i = 1, l = 0, r = 0; i <= n; i++)
	{
		if (i <= r) p[i] = min(z[i - l + 1], r - i + 1);
		while (1 + p[i] <= m && i + p[i] <= n && sb[1 + p[i]] == sa[i + p[i]]) p[i]++;
		if (i + p[i] - 1 > r) l = i, r = i + p[i] - 1;
	}
	cout << calc(p) << endl;
	return 0;
}

AC 自动机

介绍:AC 自动机是以 Trie 的结构为基础,结合 KMP 的思想建立的自动机,可用于解决查找多个字符串(模式串)在主串内的所有出现位置等任务。

ps:AC在这里是人名(Aho–Corasick)的缩写,不是Accepted的缩写。。很可惜:使用AC自动机并不能自动AC

AC 自动机可以利用fail回跳指针一次求出当前匹配位置的所有匹配模式串(利用后缀)。并利用转移边处理只匹配了一部分的问题(防止从头匹配)。

基本模板

题目:P3808 AC 自动机(简单版)https://www.luogu.com.cn/problem/P3808

代码默认只统计模式串的首次出现,如需统计全部出现只需去掉重置cnt[j]即可。

C++
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
#include <queue>

using namespace std;
vector<vector<int>> trie;
vector<int> cnt, fail;
int pid = 0;

void insert(string& s)
{
	int pos = 0;
	for (auto& i : s)
	{
		int j = i - 'a';
		if (!trie[pos][j])
		{
			trie[pos][j] = ++pid;
			trie.emplace_back(vector<int>(26));
			cnt.emplace_back(0);
		}
		pos = trie[pos][j];
	}
	cnt[pos]++;
}

void build()
{
	fail.resize(cnt.size());
	queue<int> q;
	for (int i = 0; i < 26; i++)
	{
		if (trie[0][i]) q.emplace(trie[0][i]);
	}
	while (!q.empty())
	{
		auto u = q.front();
		q.pop();
		for (int i = 0; i < 26; i++)
		{
			int v = trie[u][i];
			if (v)
			{
				fail[v] = trie[fail[u]][i];
				q.emplace(v);
			}
			else trie[u][i] = trie[fail[u]][i];
		}
	}
}

int query(string& s)
{
	int pos = 0, ans = 0;
	for (auto& i : s)
	{
		pos = trie[pos][i - 'a'];
		for (int j = pos; j && cnt[j] != -1; j = fail[j])
		{
			ans += cnt[j];
			cnt[j] = -1;
		}
	}
	return ans;
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	int n;
	string tmp;
	trie.emplace_back(vector<int>(26));
	cnt.emplace_back(0);
	cin >> n;
	while (n--)
	{
		cin >> tmp;
		insert(tmp);
	}
	build();
	cin >> tmp;
	cout << query(tmp) << endl;
	return 0;
}

效率优化

虽然 AC 自动机已经很快,但是每次匹配一直向 fail 边跳来找到所有的匹配,仍然可能超时。

观察到时间主要浪费在查询时每次都要跳 fail。如果我们可以预先记录,最后一并求和,那么效率就会优化。

于是我们按照 fail 树,做一次内向树上的拓扑排序,就能一次性求出所有模式串的出现次数。

题目:P5357 【模板】AC 自动机https://www.luogu.com.cn/problem/P5357

C++
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
#include <queue>

using namespace std;
vector<int> ans;
int pid = 0, cid = 0;

struct node
{
	vector<int> ch = vector<int>(26);
	int fail = 0;
	int in = 0;
	int sid = 0;
	int cnt = 0;
};
vector<node> trie;

int insert(string& s)
{
	int pos = 0;
	for (auto& i : s)
	{
		int j = i - 'a';
		if (!trie[pos].ch[j])
		{
			trie[pos].ch[j] = ++pid;
			trie.emplace_back();
		}
		pos = trie[pos].ch[j];
	}
	if (!trie[pos].sid)
	{
		trie[pos].sid = ++cid;
		ans.emplace_back(0);
	}
	return trie[pos].sid;
}

void build()
{
	queue<int> q;
	for (int i = 0; i < 26; i++)
	{
		if (trie[0].ch[i]) q.emplace(trie[0].ch[i]);
	}
	while (!q.empty())
	{
		auto u = q.front();
		q.pop();
		for (int i = 0; i < 26; i++)
		{
			int v = trie[u].ch[i];
			if (v)
			{
				trie[v].fail = trie[trie[u].fail].ch[i];
				trie[trie[v].fail].in++;
				q.emplace(v);
			}
			else trie[u].ch[i] = trie[trie[u].fail].ch[i];
		}
	}
}

void query(string& s)
{
	int pos = 0;
	for (auto& i : s)
	{
		pos = trie[pos].ch[i - 'a'];
		trie[pos].cnt++;
	}
}

void topusort()
{
	queue<int> q;
	for (int i = 1; i <= pid; i++)
	{
		if (trie[i].in == 0) q.emplace(i);
	}
	while (!q.empty())
	{
		auto u = q.front();
		q.pop();
		ans[trie[u].sid] = trie[u].cnt;
		int v = trie[u].fail;
		trie[v].cnt += trie[u].cnt;
		trie[v].in--;
		if (trie[v].in == 0) q.emplace(v);
	}
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	int n;
	string tmp;
	ans.emplace_back(0);
	trie.emplace_back();
	cin >> n;
	vector<int> order(n);
	for (auto& i : order)
	{
		cin >> tmp;
		i = insert(tmp);
	}
	build();
	cin >> tmp;
	query(tmp);
	topusort();
	for (auto& i : order)
	{
		cout << ans[i] << endl;
	}
	return 0;
}

目前有关字符串的匹配相关问题就这些内容。希望我以后遗忘可以慢一点。。。同时如果这也能帮到你,我很荣幸。真的。。

版权声明:
作者:Toms Project
链接:https://blog.projectoms.com/pages/1683.html
来源:Toms Project 官方博客
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
< <上一篇
下一篇>>
文章目录
关闭
目 录