📜  门| GATE 2017 模拟 |第 64 题(1)

📅  最后修改于: 2023-12-03 15:12:35.218000             🧑  作者: Mango

GATE 2017 模拟 |第 64 题
题目描述

给定一个大小至多 $10^5$ 的字母表,一串仅由此字母表中字符组成的文本 $T$,以及一个包含 $k$ 个关键字的集合 $P={P_1,P_2,...,P_k}$。要求设计一种数据结构,支持以下 $3$ 个操作:

  1. 将一个新字符串 $P_i$ 插入到 $P$ 中;
  2. 在文本 $T$ 中查询是否有关键字在其中出现过,如果有,返回任意一个出现过的关键字;
  3. 从集合 $P$ 中删除一个字符串 $P_i$。
解题思路

对于插入和删除操作,可以使用 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/)