前言:
推荐三个不错的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)
的总评分可以等价于所有节点x
的floor(sz_x / j)
的总和 。
第3步:处理动态增加的字符串
题目要求我们为每个 i
(从1到n) 都输出一个结果,这是一个动态过程。我们需要在加入第 i
个字符串后,快速更新所有 f(i, j)
的值。
增量更新 :当我们加入一个新的字符串 s_i
时,它会在前缀树中走过一条路径。路径上每个节点的 sz
计数都会加 1 。
-
关键发现 :当一个节点的
sz
从k-1
变为k
时,floor(sz / j)
的值并不会对所有的j
都改变。只有当j
是k
的因子(除数)时,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;
}