📅  最后修改于: 2023-12-03 15:12:35.218000             🧑  作者: Mango
给定一个大小至多 $10^5$ 的字母表,一串仅由此字母表中字符组成的文本 $T$,以及一个包含 $k$ 个关键字的集合 $P={P_1,P_2,...,P_k}$。要求设计一种数据结构,支持以下 $3$ 个操作:
对于插入和删除操作,可以使用 Trie 树来完成。这是一种非常常见的将字符串集合组织在一起的方法,可以满足前缀、后缀搜索、搜索某一个字符串是否在集合中等操作。本题的第 $(1)$ 操作可以使用 Trie 树的插入操作完成,第 $(3)$ 操作可以使用 Trie 树的删除操作。
对于查询操作,可以使用 Aho–Corasick 自动机。这是一种多模式匹配算法,其特点在于能够同时匹配多个模式串,并且时间复杂度仅与文本串长度有关。使用 Aho–Corasick 自动机的原理是:将 Trie 树上某个节点到根节点的路径表示的字符串称为这个节点的字符串,对 Trie 树节点构建后缀链接,将每个节点匹配失败时转移到的节点称为这个节点的失败指针,然后在输入文本字符串上进行搜索时,从第一个字符开始依次匹配模式串,并沿失败指针不断地跳转。具体实现方法可以参考 geeksforgeeks。
下面是该数据结构的实现代码,其中 trieNode
表示 Trie 树节点,包括该节点的子节点指针以及该节点对应的字符。Trie
表示 Trie 树,包括树的根节点指针以及插入、删除、查找操作。acNode
表示 Aho–Corasick 自动机的节点,包括该节点的子节点指针、该节点对应的字符、该节点的失败指针、该节点的输出指针以及该节点是否是某个模式串的结束节点。ACAutomaton
表示 Aho–Corasick 自动机,包括树的根节点指针以及插入、删除、匹配操作。其中 insert
函数用于插入一个字符串,remove
函数用于删除一个字符串,search
函数用于在文本串中查找是否有关键字在其中出现过。
typedef struct trieNode {
struct trieNode *child[26];
bool isEndOfWord;
} TrieNode;
class Trie {
public:
TrieNode *root = new TrieNode();
// Inserts a word into trie
void insert(string word) {
TrieNode *curr = root;
for (int i = 0; i < (int)word.length(); i++) {
int index = word[i] - 'a';
if (!curr->child[index])
curr->child[index] = new TrieNode();
curr = curr->child[index];
}
curr->isEndOfWord = true;
}
// Deletes a word from trie
bool remove(TrieNode *curr, string word, int level, int len) {
if (curr) {
if (level == len) {
if (curr->isEndOfWord) {
curr->isEndOfWord = false;
return isEmpty(curr);
}
}
else {
int index = word[level] - 'a';
if (remove(curr->child[index], word, level + 1, len)) {
delete curr->child[index];
curr->child[index] = nullptr;
return isEmpty(curr) && !curr->isEndOfWord;
}
}
}
return false;
}
// Returns true if parent has no child
bool isEmpty(TrieNode *curr) {
for (int i = 0; i < 26; i++)
if (curr->child[i])
return false;
return true;
}
};
typedef struct acNode {
struct acNode *child[26];
struct acNode *fail;
struct acNode *output;
bool isEndOfWord;
string word;
} ACNode;
class ACAutomaton {
public:
ACNode *root = new ACNode();
Trie trie;
void insert(string word) {
trie.insert(word);
ACNode *curr = root;
for (int i = 0; i < (int)word.length(); i++) {
int index = word[i] - 'a';
if (!curr->child[index])
curr->child[index] = new ACNode();
curr = curr->child[index];
}
curr->isEndOfWord = true;
curr->word = word;
}
void remove(string word) {
trie.remove(trie.root, word, 0, (int)word.length());
ACNode *curr = root;
for (int i = 0; i < (int)word.length(); i++) {
int index = word[i] - 'a';
curr = curr->child[index];
}
curr->isEndOfWord = false;
curr->word = "";
}
void search(string text) {
int n = (int)text.length();
ACNode *curr = root;
for (int i = 0; i < n; i++) {
int index = text[i] - 'a';
while (!curr->child[index] && curr != root)
curr = curr->fail;
curr = curr->child[index];
if (!curr)
curr = root;
ACNode *tmp = curr;
while (tmp != root) {
if (tmp->isEndOfWord) {
cout << tmp->word << " ";
tmp->isEndOfWord = false;
}
tmp = tmp->output;
}
}
cout << endl;
}
void buildFailLinks() {
queue<ACNode *> Q;
root->fail = root;
for (int i = 0; i < 26; i++) {
if (root->child[i]) {
root->child[i]->fail = root;
Q.push(root->child[i]);
}
}
while (!Q.empty()) {
ACNode *curr = Q.front();
Q.pop();
for (int i = 0; i < 26; i++) {
if (curr->child[i]) {
ACNode *tmp = curr->fail;
while (!tmp->child[i] && tmp != root)
tmp = tmp->fail;
curr->child[i]->fail = tmp->child[i] ? tmp->child[i] : root;
curr->child[i]->output = curr->child[i]->fail->isEndOfWord ? curr->child[i]->fail : curr->child[i]->fail->output;
Q.push(curr->child[i]);
}
}
}
}
};
int main() {
ACAutomaton ac;
ac.insert("abcd");
ac.insert("bcd");
ac.insert("cdef");
ac.insert("def");
ac.insert("efgh");
ac.insert("gh");
ac.buildFailLinks();
ac.search("abcdefghi"); // Output: abcd def bc cdef gh efgh
ac.remove("gh");
ac.search("abcdefghi"); // Output: abcd def bc cdef efgh
return 0;
}
参考文献:
[geeksforgeeks] (https://www.geeksforgeeks.org/trie-insert-and-search/)
[geeksforgeeks] (https://www.geeksforgeeks.org/aho-corasick-algorithm-pattern-searching/)