[字符串学习笔记] 4. Trie

4.1. 概念

Trie 又称 字典树,是储存 字符 的一种树形数据结构。

对于 \(\texttt{abbc}, \texttt{abc}, \texttt{bbb}\) 这些字符串,所构造的 Trie 如下:

Trie 用边来表示一个字母,而根结点至另一个节点的路径链表示一个字符串。如 \(0 \to 1 \to 2 \to 5\) 表示 \(\texttt{abc}\),而 \(0 \to 6 \to 7\) 表示 \(\texttt{bb}\)

通常来说,\(\delta(u, i)\) 表示节点 \(u\) 通过边权为 \(i\) 的边到达的下一个节点。边权在字符串的 字符集 内,即 \(i \in \Sigma\)

01 Trie 指字符集 \(\Sigma = \{\texttt 0, \texttt 1\}\) 的 Trie,通常用来解决异或等位运算问题。

4.2. 实现

Trie 的 插入 操作简单直观。先令当前节点 \(u\) 为根,一步步深入即可。如果要确认一条路径是否是插入的字符串,则要维护一个 字符串末尾 标识。

时间复杂度 \(\Theta(|s|)\)

int top, tr[...][26];
bool e[...];

void insert(string s) {
    int u = 0; 
    for (char i : s) {
        if (tr[u][i - 'a'] == 0)
            tr[u][i - 'a'] = ++top;
        u = tr[u][i - 'a'];
    }
    e[u] = 1;
}

查询 操作分为很多种。这里以查询是否在给定的字符串集中出现为例,时间复杂度 \(\Theta(|s|)\)

bool query(string s) {
    int u = 0;
    for (char i : s) {
        if (tr[u][i - 'a'] == 0)
            return 0;
        u = tr[u][i - 'a'];
    }
    return e[u];
}

注意,Trie 数组第一维为字符总个数,第二维为 \(|\Sigma|\)

例题 CF898C Phone Numbers

题解

由于后缀不好处理,将所有的字符串翻转。

首先可以注意到,如果 只取 根结点到叶子结点的字符串,就不会取到某一个字符串的后缀。使用 dfs 解决即可。

对于不同的名字,使用 unordered_map 存储结构体化的 Trie 即可。

参考代码:

struct Trie {
    int top, tr[N][10];
    vector<string> res;

    void insert(string s) {
        // 略
    }

    void query(int u, string s) {
        bool leaf = 1;
        for (int i = 0; i < 10; i++)
            if (tr[u][i]) {
                query(tr[u][i], s + char(i + '0'));
                leaf = 0;
            }
        if (leaf) {
            res.push_back(s);
            return;
        }
    }
};
unordered_map<string, Trie> S;

signed main() {
    cin >> n;
    while (n--) {
        cin >> name >> m;
        while (m--) {
            cin >> tel;
            reverse(tel.begin(), tel.end());
            S[name].insert(tel);
        }
    }
    cout << S.size() << '\n';
    for (auto [i, j] : S) {
        j.query(0, "");
        cout << i << ' ' << j.res.size() << ' ';
        for (string k : j.res) {
            reverse(k.begin(), k.end());
            cout << k << ' ';
        }
        cout << '\n';
    }
}

例题 UVA11488 Hyper Prefix Sets

题解

\(s(u)\) 表示根节点到 \(u\) 的路径链所代表的字符串。将 Trie 考虑为一棵树,则

\[s(\operatorname{lca}(u, v)) = \operatorname{lcp}(s(u), s(v)) \]

逆向思考,假设答案所选的字符串集 \(T\)\(\operatorname{lcp}\)\(s(p)\)

由于要最大化 \(|s(p)| \times |T|\),肯定要 尽量多选\(s(p)\) 这个前缀的字符串。在 Trie 的插入操作中,深入到节点 \(p\) 时次数 \(l[p] \gets l[p] + 1\)。插入完所有的字符串后,\(l[p]\) 就是有 \(s(p)\) 这个前缀的字符串个数。

同时,为了快速获得 \(s(p)\) 的长度,也要记录节点的深度 \(d[p]\)。故答案为 \(\max d[i] \times l[i]\)

参考代码:

void insert(string s) {
    int u = 0;
    for (int i : s) {
        int &v = tr[u][i - '0'];
        if (v == 0) v = ++top;
        d[v] = d[u] + 1;
        u = v;
        l[u]++;
    }
}

void solve() {
    cin >> n;
    // 初始化,略
    for (int i = 1; i <= n; i++) {
        cin >> s;
        insert(s);
    }
    int res = 0;
    for (int i = 1; i <= top; i++)
        res = max(res, d[i] * l[i]);
    cout << res << '\n';
}

例题 UVA12506 Shortest Names

题解

不难发现,如果节点 \(u\) 在插入 Trie 时经过的次数 \(l_u = 1\),那么根到 \(u\) 的链便仅为一个字符串的前缀。

查询时同时记录根到当前节点的长度,第一次 \(l_u = 1\) 时直接返回即可。

void insert(string s) {
    int u = 0;
    for (char i : s) {
        if (tr[u][i - 'a'] == 0)
            tr[u][i - 'a'] = ++top;
        u = tr[u][i - 'a'];
        l[u]++;
    }
}

int query(string s) {
    int u = 0, d = 1;
    for (char i : s) {
        if (l[tr[u][i - 'a']] == 1)
            return d;
        u = tr[u][i - 'a'];
        d++;
    }
    return d;
}

void solve() {
    cin >> n;
    // 初始化,略
    for (int i = 1; i <= n; i++) {
        cin >> s[i];
        insert(s[i]);
    }
    int res = 0;
    for (int i = 1; i <= n; i++)
        res += query(s[i]);
    cout << res << '\n';
}

例题 SP10381 DICT - Search in the dictionary!

题解

先对所有字符串建 Trie,同样维护 Trie 上以 \(u\) 结尾的字符串个数 \(e[u]\)。每一次询问 \(s\) 时,

  • 若 Trie 内不包含 \(s\),显然无解。
  • 否则,从 \(s\) 的末尾节点开始 dfs,如果搜到节点 \(u\) 满足 \(e[u]\) 为真,就将根到 \(u\) 路径代表的字符串存入答案。

参考代码:

void dfs(int u, string s) {
    for (int i = 0; i < 26; i++)
        if (tr[u][i]) {
            if (e[tr[u][i]])
                res.push_back(s + char(i + 'a'));
            dfs(tr[u][i], s + char(i + 'a'));
        }
}

void query(string s) {
    int u = 0;
    for (char i : s) {
        if (tr[u][i - 'a'] == 0) {
            cout << "No match.\n";
            return;
        }
        u = tr[u][i - 'a'];
    }
    dfs(u, s);
    sort(res.begin(), res.end());
    for (string i : res)
        cout << i << '\n';
}

例题 UVA1401 Remember the Word

题面

给定字符串 \(s\) 及字符串集 \(\mathbf S\),求有多少不同的有序子集 \(\mathbf T \subseteq \mathbf S\),满足 \(\mathbf T\) 中的字符串按顺序拼接后与 \(s\) 相等。

例如,当 \(s = \texttt{abcd}\)\(\mathbf S = \{\texttt a, \texttt b, \texttt{cd}, \texttt{ab}\}\) 时,\(\mathbf T\) 可能为 \(\{\texttt a, \texttt b, \texttt{cd}\}\)\(\{\texttt{ab}, \texttt{cd}\}\)

题解

考虑 dp。令 \(f[i]\) 表示组成后缀 \(s[i \ldots |s| - 1]\) 的方案数。当前 \(i\) 下的 \(s[i \ldots |s| - 1]\) 记作 \(p\)

显然有转移方程

\[f[i] = \sum_{s[i + 1 \ldots j] \in \mathbf T} f[j + 1] \]

\(\mathbf S\) 内的字符串存入 Trie,按照转移方程 dp 即可。

参考代码:

void DP() {
    f[s.size()] = 1;
    for (int i = s.size() - 1; i >= 0; i--) {
        int u = 0;
        for (int j = i; j < s.size(); j++) {
            if (tr[u][s[j] - 'a'] == 0)
                break; // 不存在直接退出
            u = tr[u][s[j] - 'a'];
            if (e[u])
                f[i] = (f[i] + f[j + 1]) % P;
        }
    } 
}

void solve(int tc) {
    // 初始化,略
    cin >> s >> n;
    while (n--) {
        cin >> t;
        insert(t);
    }
    DP();
    cout << "Case " << tc << ": " << f[0] << '\n';
}
posted @ 2024-06-22 12:45  Carrot-Meow~  阅读(40)  评论(0)    收藏  举报