Trie树实战:三道典型例题

前言:

推荐三个不错的Trie数题目,前两个比较简单,第三个比较有难度。

MC0489黛玉葬花

分析:很前缀字符串的思路,找到一个点就代表一个字母,根据题意就是这一位置的相同前缀的数量为x , 计算一下结果 ans += x *(x - 1) , 对每一个节点都这样操作一次。

做法一:Trie

cpp 复制代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1000010;
int nodes;
const int mod = 998244353;

struct TrieNode{
    int child[26];
    int sz;
}tr[N];

void add(string s)
{
    int u = 0;
    tr[u].sz++;
    for (int i = 0; i < s.size(); i++)
    {
        int c_num = s[i] - 'a';
        if (!tr[u].child[c_num])
        {
            tr[u].child[c_num] = ++nodes;
        }
        u = tr[u].child[c_num];
        tr[u].sz++;
    }   
}

ll C(ll x)
{
    if (x == 1)
        return 0;
    return (x - 1) * x;
}

// 2 2 1 2 3 1 1 1 1
// 2 1 1

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int n;
    cin >> n;
    ll ans = 0;
    for (int i = 0; i < n; i++)
    {
        string s;
        cin >> s;
        ans += s.size();
        add(s);
    }
    for (int u = 1; u <= nodes; u++)
    {
        ans = (ans + C(tr[u].sz)) % mod;
    }
    cout << ans << endl;

    return 0;
}

做法二:字符串哈希(略显麻烦了)思路是大致一样的

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
typedef unsigned long long ull;
const int N = 1e6 + 10 , P = 131;
const int mod = 998244353;
ull h[N], p[N];
ull find( int l, int r){
    return h[r] - h[l-1] * p[r-l+1];
}
int cal(int x){
    return x * (x - 1) / 2;
}
void solve(){
    unordered_map<ull, int> mp;
    int n , ans = 0;
    cin >> n;
    for (int i = 0; i < n; i++){
        string s;
        cin >> s;
        ans += s.length();
        p[0] = 1;
        for (int i = 1; i <= s.size(); i++){
            h[i] = h[i-1] * P + s[i - 1];
            p[i] = p[i-1] * P;
        }
        for (int i = 0; i < s.size(); i++){
            mp[find(1, i + 1)] ++;
        }
    }
    for (auto p : mp){
        int cnt = p.second;
        if(cnt > 1){
            ans = (ans + cal(cnt) * 2) % mod;
        }
    }
    cout << ans << endl;
}
signed main(){
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    solve();
    return 0;
}

D1. Max Sum OR (Easy Version)

题意:题意:给出了两组0到r个数字(一个 2(r +1)个数字 , l == 0 )求两个个组中的数相互异或的和的最大值,想到前缀树,各位取反的思路,比如10101就需要去找01010或者00000才能没有损失。

做法一:Trie

求两个数异或的最大值要想到前缀树,本身就是各位取反的思维。需要注意倒序遍历,因为我们要先满足大的数字,再去满足小的数字,可以认为大的数字更加重要,先满足高位一定会产生最优解具体证明略。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define endl "\n"
#define ll long long
const int N = 200010, B = 19, M = N * B;
int vis[N], p[N]; // 节点的个数 每个点的每一位置代表一个节点
struct TrieNode
{
    int child[2];
    int sz;
} tr[M];

int nodes;

void init()
{
    tr[0].child[0] = tr[0].child[1] = 0;
    tr[0].sz = 0;
    nodes = 0;
}

void add(int x)
{
    int u = 0;
    tr[u].sz++; // 根节点开始
    for (int i = B - 1; i >= 0; i--)
    {
        int b_t = (x >> i) & 1;
        if (!tr[u].child[b_t])
        {
            tr[u].child[b_t] = ++nodes;
            tr[nodes].child[0] = tr[nodes].child[1] = 0;
            tr[nodes].sz = 0;
        }
        u = tr[u].child[b_t];
        tr[u].sz++;
    }
}

void del(int x)
{
    int u = 0;
    tr[u].sz;
    for (int i = B - 1; i >= 0; i--)
    {
        int b_t = (x >> i) & 1;
        u = tr[u].child[b_t];
        tr[u].sz--;
    }
}

int find(int x) // del负责修改
{
    int u = 0;
    int res = 0;
    for (int i = B - 1; i >= 0; i--)
    {
        int b_t = (x >> i) & 1;
        int b_p = 1 - b_t;
        if (tr[u].child[b_p] && tr[tr[u].child[b_p]].sz > 0)
        {
            u = tr[u].child[b_p];
            res = res | (b_p << i);
        }
        else
        {
            u = tr[u].child[b_t];
            res = res | (b_t << i);
        }
    }
    return res;
}

void solve()
{
    init();
    int l, r;
    cin >> l >> r;
    for (int i = l; i <= r; i++)
    {
        add(i);
        vis[i] = 0, p[i] = 0;
    }
    for (int i = r; i >= l; i--)
    {
        if (!vis[i])
        {
            del(i);
            int j = find(i);
            del(j);
            p[i] = j;
            p[j] = i;
            vis[i] = vis[j] = 1;
        }
    }
    cout << (ll)(r + 1) * r << endl;
    for (int i = l; i <= r; i++)
    {
        cout << p[i] << " ";
    }
    cout << endl;
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int t;
    cin >> t;
    while (t--)
        solve();
    return 0;
}

做法二:找规律。

对于任何一个配对 (i, j),我们有 j = K ^ i。根据异或的性质 A ^ B = C 等价于 A ^ C = B,我们可以推导出: i ^ j = i ^ (K ^ i) = K 这意味着,通过这种方法找到的每一对数字 (i, j),它们的异或和都等于那个掩码 K

  • i[8, 15] 之间时,它的二进制长度是4位,K 就是 15 (1111_2)。所有这个范围内的数字都会和另一个数字配对,使得异或和为15。

    • 15 ^ 0 = 15

    • 14 ^ 1 = 15

    • 13 ^ 2 = 15

    • ...

    • 8 ^ 7 = 15

  • i[4, 7] 之间时,它的二进制长度是3位,K 就是 7 (111_2)。

    • 7 ^ 0 = 7

    • 6 ^ 1 = 7

    • ...

  • 以此类推。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
int masks[N] , ans[N];

int main(){
    int t;
    cin >> t;
    while(t --){
        int l, r;
        cin >> l >> r;
        cout << (ll)r * (r + 1) << endl;
        for (int i = l; i <= r; i ++)
            masks[i] = 0;
        for (int i = r; i > 0; i--)
        {
            if (!masks[i])
            {
                int j = ((1 << (32 - __builtin_clz(i))) - 1) ^ i;
                // 统计出来0的个数
                ans[i] = j;
                ans[j] = i;
                masks[i] = 1;
                masks[j] = 1;
            }
        }
        if(!masks[0]) // 如果前面没有数字配对 就代表自己和自己配对
            ans[0] = 0;
        for (int i = l; i <= r; i++){
            cout << ans[i] << " ";
        }
        cout << endl;
    }

    return 0;
}

G. Yelkrab

这题比较有难度一点,是2024香港区域赛银牌题,(AI)解析如下(太菜乐~):

解题思路
第1步:理解问题核心 f(i, j)

首先,我们需要解决最核心的子问题:对于给定的 i 个字符串和分组大小 j,如何求得 f(i, j),也就是最大的总评分?

评分规则:一个小组的评分是组内所有字符串的"最长公共前缀"(LCP) 的长度 。总评分是所有小组评分之和 。

贪心策略 :一个直观且正确的想法是采用贪心策略。我们应该不断地从当前还未分组的字符串中,找出能组成最长 LCP 的 j 个字符串,把它们分为一组,然后对剩下的字符串重复此过程 。

第2步:用前缀树(Trie)优化 f(i, j) 的计算

贪心策略虽然正确,但实现起来很慢。处理 LCP 问题,最自然的工具就是前缀树

  • 前缀树的性质:我们将所有字符串插入前缀树。树上的每个节点代表一个前缀,节点的深度就等于前缀的长度。所有经过同一个节点的字符串,都共享这个节点代表的前缀。

  • 计算总评分 :假设前缀树上有一个节点 x,有 sz_x 个字符串经过它(即最终都落在了 x 的子树中) 。这意味着这

    sz_x 个字符串都拥有 x 所代表的那个前缀。对于分组大小为 j 的情况,我们可以在 x 这个节点层面形成 floor(sz_x / j) 个分组。

  • 一个重要的转换 :一个 LCP 长度为 L 的分组,其评分为 L。这可以看作是在其 LCP 路径上的 L 个节点(深度从1到L)处,每个节点都贡献了 1 的评分。因此,f(i, j) 的总评分可以等价于所有节点 xfloor(sz_x / j) 的总和

第3步:处理动态增加的字符串

题目要求我们为每个 i (从1到n) 都输出一个结果,这是一个动态过程。我们需要在加入第 i 个字符串后,快速更新所有 f(i, j) 的值。

增量更新 :当我们加入一个新的字符串 s_i 时,它会在前缀树中走过一条路径。路径上每个节点的 sz 计数都会加 1 。

  • 关键发现 :当一个节点的 szk-1 变为 k 时,floor(sz / j) 的值并不会对所有的 j 都改变。只有当 jk 的因子(除数)时,floor((k-1) / j) 才会比 floor(k / j) 小 1。

  • 更新策略 :因此,在 s_i 路径上的每个节点 x,当它的 sz_x 更新后,我们只需要遍历新 sz_x 的所有因子 d,并将对应的 F[d](即 f(i,d))的值加 1 即可 。

第4步:高效计算异或和

我们需要计算的最终结果是

(f(i,1)×1) ⊕ (f(i,2)×2) ⊕ ... ⊕ (f(i,i)×i),其中 是异或操作 。

  • 问题 :在加入第 i 个字符串后,很多 f(i, j) 的值都可能发生变化。如果每次都从头计算这个异或和,总复杂度会过高。

  • 解决方案 :我们需要一个能够支持"单点更新"和"前缀查询"的数据结构。树状数组(Fenwick Tree) 是完美的选择。

    • 单点更新 :当 F[d] 的值从 old 变为 new 时,我们需要更新的项是 F[d] * d。我们向树状数组的第 d 个位置异或上 (old * d) ^ (new * d),就可以消除旧值的影响,并加入新值。

    • 前缀查询 :使用树状数组,我们可以在 O(log n) 的时间内查询到 term[1] ⊕ term[2] ⊕ ... ⊕ term[i] 的结果。

知识点:前缀异或,树状数组,Trie

cpp 复制代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll N = 500010;   
ll n;
struct TrieNode{
    ll child[26];
    ll sz;
} tr[N * 2];
vector<int> divi[N];
ll F[N] , bit[N] , nodes;

void cal(){   //预处理因子
    for (ll i = 1; i < N; i++)
    {
        for (ll j = i; j < N; j += i)
        {
            divi[j].push_back(i);
        }
    }
}

void init(int n)  // 针对多组测试数据初始化
{
    memset(tr[0].child, 0, sizeof(tr[0].child));
    memset(bit , 0,  sizeof(ll) *(n + 1));
    memset(F, 0, sizeof(ll) * (n + 1));
    nodes = 0 ; 
}

void update(ll i  ,ll val){      // 双庄数组模板
    while(i <= n){
        bit[i] ^= val;
        i += i & -i;
    }
}

ll query(ll i){
    ll sum = 0 ; 
    while(i > 0){
        sum ^= bit[i];
        i -= i & -i;
    }
    return sum;
}

void add(string s){
    ll u = 0;
    tr[u].sz++;
    for(auto c : s){ 
        ll c_num = c - 'a';
        if(!tr[u].child[c_num]){
            tr[u].child[c_num] = ++nodes;
            memset(tr[nodes].child , 0 , sizeof(tr[nodes].child));
            tr[nodes].sz = 0;
        }
        u = tr[u].child[c_num];
        tr[u].sz++;
        // 上面是tire模板  下面是更新新加入的字符串对结果的影响

        for(auto d : divi[tr[u].sz]){
            ll old = F[d];
            ll neww = ++F[d];
            update(d, (old * d) ^ (neww * d));
        }
    }
}

void solve(){
    
    cin >> n;
    init(n);
    for (ll i = 1; i <= n; i ++){
        string s;
        cin >> s;
        add(s);
        // 找到所有的字符串进行操作 树状数组快速求和是怎么求的呢
        cout << query(i) << (i == n ? "\n" : " ");
    }
    return;
}

int main(){
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    cal();
    ll t;
    cin >> t;
    while( t -- ){
        solve();
    }
    return 0;
}
相关推荐
dragoooon344 小时前
[优选算法专题三.二分查找——NO.20搜索插入位置 ]
算法·leetcode·动态规划
红尘客栈24 小时前
ELK 企业级日志分析系统实战指南
数据结构
hn小菜鸡4 小时前
LeetCode 1089.复写零
算法·leetcode·职场和发展
与己斗其乐无穷4 小时前
算法(一)双指针法
数据结构·算法·排序算法
努力努力再努力wz5 小时前
【C++进阶系列】:位图和布隆过滤器(附模拟实现的源码)
java·linux·运维·开发语言·数据结构·c++
eSoftJourney5 小时前
C 语言核心关键字与数据结构:volatile、struct、union 详解
c语言·数据结构
(❁´◡`❁)Jimmy(❁´◡`❁)5 小时前
【Trie】 UVA1401 Remember the Word
算法·word·图论
小年糕是糕手7 小时前
【C语言】C语言预处理详解,从基础到进阶的全面讲解
linux·c语言·开发语言·数据结构·c++·windows·microsoft
高山有多高7 小时前
从 0 到 1 保姆级实现C语言双向链表
c语言·开发语言·数据结构·c++·算法·visual studio