「NOIP Record」#3 Trie
少年找到了节奏
Trie
luogu2922 [USACO08DEC]Secret Message G
给定两个字符串序列 与 ,对于 中每个字符串 ,求 中有多少个字符串 ,满足以下两个条件之一
- 是 的前缀。
- 是 的前缀。
两个字符串序列中所有字符串长度之和不超过 。
把 中所有字符串插入 Trie,记录每个节点处结尾的字符串数量 。对于第 1 种,答案就是 路径上的 之和。
对于第 2 种,在 Trie 上 求出以 为根的子树内有多少字符串的结尾位置 ,然后顺着 走,如果走不完 就是 ,否则就是 结尾那个节点的 。
注意算第一种要忽略 结尾那个节点的 ,否则会重复。
UVA1401 Remember the Word
给定一个由 个不同单词组成的字典 和一个长度为 的字符串 ,求把这个字符串按照字典划分为若干单词有多少种方法。
,。
单个字典中的单词长度不超过 。
朴素 DP,设 为 的划分方案数,则
或者
不太能优化。
尝试另外一种状态,设 为后缀 的划分方案数
注意到字典中单词长度不超过 ,所以可以从 暴力枚举,通过 hash 快速判断是否可以转移。
但是这篇文章写 Trie,考虑一些 Trie 做法。
将 中字符串插入 Trie,搜一下 ,找一下路径上的结束节点即可。
luogu7537. [COCI2016-2017#4] Rima
设字符串 与 的最长公共后缀的长度为 。
称两个字符串 合法,当且仅当 。
给定 个字符串,要求组合出一个长度最长的字符串序列,满足相邻两个字符串合法。输出序列长度。
,字符串总长度不超过 。
后缀不好做,可以转化成前缀插入 Trie,记录在每个节点结束的串的个数。
这样就转化成了 合法,当且仅当在 Trie 树上,二者结束节点的 LCA,距离深度较大的那个结束节点不超过一条边。
还能继续发现性质。合法的字符串序列,相邻两个串的 可以是先递减后递增的。这样一定最优。
因此在 Trie 上 DFS,枚举中间这个最小的 。
设 为最长公共后缀长度单调不增,最后两个串的最长公共后缀是 对应的字符串的最长序列。相当于时求了最优序列的一半。
节点 是所有子节点的 ,所以它的所有子节点都能加入序列,以 为结尾的串放到中间。而序列两边还能接上以 的某两个子节点的 值,取最大和次大即可。
#include<bits/stdc++.h>using namespace std;#define int long long#define SET(a,b) memset(a,b,sizeof(a))#define rep(i,j,k) for(int i=(j);i<=(k);++i)#define per(i,j,k) for(int i=(j);i>=(k);--i)int read() { int a=0, f=1; char c=getchar(); while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); } while(isdigit(c)) a=a*10+c-'0', c=getchar(); return a*f;}const int N=3e6+5;int n;int tot, ans, trie[N][26], cnt[N], f[N];char s[N];struct Trie { void insert(char* s) { int x=0, len=strlen(s); per(i,len-1,0) { int a=s[i]-'a'; if(!trie[x][a]) trie[x][a]=++tot; x=trie[x][a]; } ++cnt[x]; } void dfs(int x) { f[x]=cnt[x]; int sz=0, fr=0, sc=0; rep(i,0,25) if(trie[x][i]) { int y=trie[x][i]; dfs(y); if(cnt[y]) { ++sz; if(f[y]>fr) sc=fr, fr=f[y]; else if(f[y]>sc) sc=f[y]; } } f[x]+=fr+max(0ll,sz-1); ans=max(ans,fr+sc+cnt[x]+max(0ll,sz-2)); }} tr;signed main() { n=read(); rep(i,1,n) { scanf("%s",s); tr.insert(s); } tr.dfs(0); printf("%lld\n",ans);}luogu9218 「TAOI-1」Apollo
如果 与 的整数部分不同,那么一定能找到一个整数 使得 ,从而 。
如果 与 的整数部分相同,小数部分不同,那么能找到一个 是的 是它们小数部分 的长度 ,这也是 的最小值。
比如
a=11.4514b=11.4523c=11.452, g(a,b)=3而当 时, 是 小数部分 的长度,这个显然。
考虑用 Trie 维护前缀信息,把 长度拆成每个节点被经过的次数。
问题在于对于每一个 求 。
由上述分析知道 有贡献的必要条件是 与 整数部分相同。我们可以将所有 按照整数部分排序,一次处理整数部分相同的一块 ,这些数肯定共用一棵 Trie。然后用每个 的小数部分去匹配这棵 Trie,将这些贡献加入 。
匹配的过程中到达了节点 ,无论子节点是什么都会产生贡献,所以在记录每个点被经过的次数时,直接加到它的父亲节点即可。这样同时避免了 匹配 时特判最后一位。
#include<bits/stdc++.h>using namespace std;#define int long long#define SET(a,b) memset(a,b,sizeof(a))#define CPY(a,b) memcpy(a,b,sizeof(b))#define rep(i,j,k) for(int i=(j);i<=(k);++i)#define per(i,j,k) for(int i=(j);i>=(k);--i)int read() { int a=0, f=1; char c=getchar(); while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); } while(isdigit(c)) a=a*10+c-'0', c=getchar(); return a*f;}const int N=1e5+5, M=3e6+5;int n, ans[N];int tot, trie[M][10], cnt[M];struct qwq { int it, id; string s;} e[N];bool operator<(qwq a,qwq b) { return a.it<b.it;}void insert(string S,int d) { int x=0, len=S.size(); for(int i=0;i<len;++i) { cnt[x]+=d; // 在父节点处修改 int a=S[i]-'0'; if(!trie[x][a]) trie[x][a]=++tot; x=trie[x][a]; }}int query(string S) { int x=0, res=0, len=S.size(); for(int i=0;i<len;++i) { res+=cnt[x]; // 在父节点处统计 int a=S[i]-'0'; if(!trie[x][a]) break; x=trie[x][a]; } return res;}void solve(int l,int r) { for(int i=l;i<=r;++i) insert(e[i].s,1); for(int i=l;i<=r;++i) ans[e[i].id]+=query(e[i].s); for(int i=l;i<=r;++i) insert(e[i].s,-1);}signed main() { n=read(); rep(i,1,n) { scanf("%lld.",&e[i].it); cin>>e[i].s; e[i].id=i; } sort(e+1,e+n+1); int lst=1; for(int i=1;i<n;++i) { if(e[i].it!=e[i+1].it) solve(lst,i), lst=i+1; } if(lst!=n) solve(lst,n); rep(i,1,n) printf("%lld\n",ans[i]);}某模拟赛题
小 F 正在写一个磁盘搜索系统。磁盘中共有 个文件,它们的文件名 由小写字母组成,两两不同。小 F 想快速知道 个问题的答案:第 次给定 ,求以 为文件名的文件是否存在。
这个问题很快被小 F 解决了。但是小 F 遇到了一个新的问题:他记不清 具体是什么,只记得它的开头一部分和结尾一部分,中间部分用一个
*代替。小 F 想知道满足这个条件的文件有多少个。对于 的数据,。
对于另外 的数据,
*出现在开头或结尾。对于 的数据,,。
正反建两棵 Trie。
设 为 中*之前的部分最后一个字符在 Trie 上对应的节点, 为后面的部分的。
如果匹配 时在正 Trie 匹配到 ,在反 Trie 匹配到 , 必须在 子树内, 必须在 子树内。
暴力做法:对于每个 ,在正 Trie 上 DFS。匹配完 之后,对其子树内所有点都加上 的权值,然后是反 Trie 上的 ,其子树权值和即为答案。子树内 连续,只涉及区间加区间查,可以用树状数组维护。
正解:把询问离线了。在 DFS 到 时,求出此时 的子树权值和;DFS 结束后再统计一次,做差即可。
可以只保存 Trie 上的 节点,重新建图。
给出 std。
#include<cstdio>#include<cstdlib>#include<cstring>#include<algorithm>#define IO(x) freopen(x".in","r",stdin),freopen(x".out","w",stdout)using std::reverse;const int N=1e6+5;int n,m;char s[N];int ans[N];int trieTotal,linkTotal,dfsCount;struct BIT { int d[N]; inline void update(int p) { for(; p<=dfsCount; p+=p&-p)++d[p]; } inline int query(int p) { static int r; for(r=0; p; p-=p&-p)r+=d[p]; return r; } inline int querySum(int l,int r) { return query(r)-query(l-1); }} b;struct queryNode { int to,nt; inline void set(int t,int n) { to=t,nt=n; }} q[N];struct linkNode { int to,nt,lp,rp; inline void set(int t,int n,int l,int r) { to=t,nt=n,lp=l,rp=r; }} l[N<<1];struct trieNode { int h,v1,v2;#define siz v1#define dfn v2#define lnk v1#define fir v2 inline void addQuery(int i,int t) { q[i].set(t,fir),fir=i; }} a[N<<1];inline void update(int pos,int flg=1) { static int tmp; for(int i=a[pos].fir; i; i=q[i].nt) { tmp=q[i].to; ans[i]+=flg*b.querySum(a[tmp].dfn,a[tmp].dfn+a[tmp].siz-1); }}inline int newTrieNode() { return ++trieTotal;}inline int newLinkNode() { return ++linkTotal;}struct trie { int root,cur; char buf[N]; inline trie():root(newTrieNode()) {}; inline int addLink(int src,int dst,char*&s) { int tmp=newLinkNode(),old=cur; while(*s)buf[cur++]=*(s++); l[tmp].set(dst,a[src].h,old,cur),a[src].h=tmp; return dst; } inline int findLink(int pos,char*&s) { for(int i=a[pos].h,p; i; i=l[i].nt){ for(p=l[i].lp;p<l[i].rp&&buf[p]==*s;++p)++s; if(p==l[i].lp)continue; if(p==l[i].rp)return l[i].to; pos=newTrieNode(); int tmp=newLinkNode(); l[tmp].set(l[i].to,0,p,l[i].rp),a[pos].h=tmp; l[i].to=pos,l[i].rp=p; break; } return addLink(pos,newTrieNode(),s); } inline int getLink(int pos,char*&s) { for(int i=a[pos].h,p; i; i=l[i].nt){ for(p=l[i].lp;p<l[i].rp&&buf[p]==*s;++p)++s; if(p==l[i].lp)continue; if(p==l[i].rp||*s=='*')return l[i].to; break; } return 0; } inline int insert(char* s) { int pos=root; while(*s)pos=findLink(pos,s); return pos; } inline int travel(char* s) { int pos=root; while(pos&&*s!='*')pos=getLink(pos,s); return pos; }} t[2];inline int dfs1(int pos) { a[pos].siz=1,a[pos].dfn=++dfsCount; for(int i=a[pos].h; i; i=l[i].nt)a[pos].siz+=dfs1(l[i].to); return a[pos].siz;}inline void dfs2(int pos) { update(pos,-1); if(a[pos].lnk)b.update(a[a[pos].lnk].dfn); for(int i=a[pos].h; i; i=l[i].nt)dfs2(l[i].to); update(pos,1);}int len,t0,t1;inline void funcS() { scanf("%s",s); len=strlen(s),t0=t[0].insert(s); reverse(s,s+len),t1=t[1].insert(s); a[t0].lnk=t1;}inline void funcT(int i) { scanf("%s",s); len=strlen(s),t0=t[0].travel(s); reverse(s,s+len),t1=t[1].travel(s); if(t0&&t1)a[t0].addQuery(i,t1);}int main() { scanf("%d%d",&n,&m); for(int i=1; i<=n; ++i)funcS(); dfs1(t[1].root); for(int i=1; i<=m; ++i)funcT(i); dfs2(t[0].root); for(int i=1; i<=m; ++i)printf("%d\n",ans[i]);}0-1 Trie
在 0-1 Trie 上实现 很容易。
luogu5283 [十二省联考 2019] 异或粽子
求前缀异或和 ,问题转化为求 个点对 ,满足 是前 大的。
考虑用 Trie。由于 Trie 是无序的而点对有序,所以一个点对会被找到两次,这样求出答案再除以 即可。
暴力插入显然是不行的。但是对于 与 ,若 ,那么 一定优先于 。所以开一个大根堆,维护三元组 表示与 异或结果从大到小排名为 的异或值 ,贪心选择,然后再加入 。
虽然 不合法且能被选到,但 ,所以没有影响。
复杂度 。
#include<bits/stdc++.h>using namespace std;#define int long long#define SET(a,b) memset(a,b,sizeof(a))#define CPY(a,b) memcpy(a,b,sizeof(b))#define rep(i,j,k) for(int i=(j);i<=(k);++i)#define per(i,j,k) for(int i=(j);i>=(k);--i)int read() { int a=0, f=1; char c=getchar(); while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); } while(isdigit(c)) a=a*10+c-'0', c=getchar(); return a*f;}const int N=5e5+5;int n, k, ans, s[N];struct node { int x, id, rk;};bool operator<(node a,node b) { return a.x<b.x;}priority_queue<node> q;struct Trie { int tot, trie[N*31][2], cnt[N*31]; void insert(int s) { int x=0; for(int i=31;~i;--i) { int a=(s>>i)&1; if(!trie[x][a]) trie[x][a]=++tot; x=trie[x][a]; ++cnt[x]; } } int query(int s,int k) { int x=0, ans=0; for(int i=31;~i;--i) { int a=(s>>i)&1; if(cnt[trie[x][a^1]]>=k) { ans|=1ll<<i; // 注意用1ll x=trie[x][a^1]; } else { k-=cnt[trie[x][a^1]]; x=trie[x][a]; } } return ans; }} T;signed main() { n=read(), k=read(); rep(i,1,n) s[i]=s[i-1]^read(), T.insert(s[i]); T.insert(s[0]); // 插入s[0] rep(i,0,n) { int x=T.query(s[i],1); q.push({x,i,1}); } k<<=1; while(k--) { node t=q.top(); q.pop(); ans+=t.x; int x=T.query(s[t.id],t.rk+1); q.push({x,t.id,t.rk+1}); } printf("%lld\n",ans>>1);}luogu6824 「EZEC-4」可乐
把 插入 Trie,记录结束节点。
设 为以 为根的子树所能得到的最大值。
但是这样会有两个问题。
- 不是所有情况下都能知道 与 的大小关系。
- 只有叶子节点能产生贡献,且只能贡献 次。
进一步地,只有 与 这一位同号且 这一位为 时才能让以 为根的子树中所有叶子节点的贡献转移到 。
因此当 这一位是 时,有
否则只继承状态
其中 表示以 为根的子树中叶子节点的数量。这样就能解决上述两个问题。
#include<bits/stdc++.h>using namespace std;#define int long long#define SET(a,b) memset(a,b,sizeof(a))#define CPY(a,b) memcpy(a,b,sizeof(b))#define rep(i,j,k) for(int i=(j);i<=(k);++i)#define per(i,j,k) for(int i=(j);i>=(k);--i)int read() { int a=0, f=1; char c=getchar(); while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); } while(isdigit(c)) a=a*10+c-'0', c=getchar(); return a*f;}const int N=1e6+5;int n, k, f[N*20], cnt[N*20];int tot=1, trie[N*20][2];void insert(int S) { int x=1; for(int i=20;~i;--i) { int a=(S>>i)&1; if(!trie[x][a]) trie[x][a]=++tot; x=trie[x][a]; } ++cnt[x];}void ddfs(int x,int d) { if(!trie[x][0]&&!trie[x][1]) return; rep(i,0,1) { if(trie[x][i]) ddfs(trie[x][i],d-1), cnt[x]+=cnt[trie[x][i]];
}}void dfs(int x,int d) { if(!trie[x][0]&&!trie[x][1]) { f[x]=cnt[x]; return; } rep(i,0,1) { if(trie[x][i]) dfs(trie[x][i],d-1); } int x0=trie[x][0], x1=trie[x][1]; if((k>>d)&1) f[x]=max(cnt[trie[x][0]]+f[trie[x][1]],cnt[trie[x][1]]+f[trie[x][0]]); else f[x]=max(f[trie[x][0]],f[trie[x][1]]);}signed main() { n=read(), k=read(); rep(i,1,n) insert(read()); ddfs(1,20); dfs(1,20); printf("%lld\n",f[1]);}CF Gym102331B Bitwise Xor
luogu7717 「EZEC-10」序列
觉得这篇文章怎么样?
点个赞,让更多人看到!

评论区