CF1037H Security 题解
传送门
wind_whisper 好闪,拜谢 wind_whisper
要是洛谷有 CF 的 RMJ 就好了
题解
题意简述
给一个字符串 \(s\),每次询问给出 \(l\) \(r\) \(t\),查询一个 \(\large s_{l\dots r}\) 的子串,使得这个子串在满足字典序严格大于 \(t\) 的条件下字典序最小。
分析
显然这个问题可以使用 SAM 来解决。
考虑问题的弱化版本,即没有区间限制的求解。
对 \(s\) 建出 SAM,对每个询问,从左往右遍历 \(t\) 串。
设当前遍历到 \(t\) 的第 \(i\) 个字符,并且转移到了 SAM 的节点 \(x\)。
如果 \(x\) 有 \(t_i\) 这条转移边,那么沿着这条边转移,否则找到第一条大于 \(t_i\) 的转移边。
如果这条边存在,那么这就是所求字符串的最后一个字符,求解完毕,因为此时我们得到的字符串的字典序已经大于 \(t\)。
如果这条边不存在,即 \(x\) 的所有转移边都小于 \(t_i\),或者 \(x\) 没有转移边,此时需要回溯到上一个节点继续求解。
这里有一种边界情况:当 \(t\) 的每一位都成功匹配时,直接沿着当前节点最小的转移边转移。这样 \(t\) 是得到的字符串的前缀,字典序小于它。
现在考虑原问题,因为添加了区间的限制,所以不能看到一条边就转移,需要考虑要转移到的节点有没有一个合法的 \(endpos\)。
一个 \(endpos\) 是合法的,首先需要它在 \(r\) 的左边;其次,当我们转移到它的时候,我们的字符串会以它结尾,这个字符串的左端点需要在 \(l\) 的右边。
由于我们需要用到 \(endpos\) 的具体值,所以与模板题不同,我们需要显式地存储 \(endpos\)。
怎么求每个节点 \(endpos\) 呢?由于 SAM 拥有 fail 树,求一个节点的 \(endpos\) 集合可以对它在 fail 树上的子树的 \(endpos\) 取并(包括可能存在的它自己特有的 \(endpos\),这个在插入字符时就能解决),而解决像这样的子树类问题可以使用线段树合并,所以我们使用线段树合并。
这里需要注意,由于我们需要在访问一个节点在 fail 树上的祖先后访问这个节点,所以在线段树合并的时候要新建节点存储合并后的值。
实现
于是可以按照如下方式实现这个程序:
首先对 \(s\) 建出 SAM,给每个节点开一棵权值线段树;
接着用线段树合并求出每个节点的 \(endpos\);
对每个询问,对 SAM 进行 DFS;
当能匹配上且能转移的时候,转移;如果往这条边转移有解,把这条边的字符加进字符串最后面,返回有解;
否则,找到大于 \(t_i\) 的最小转移边,把这个边的字符加进字符串最后面,返回有解;
如果还是无解,那么返回无解。
注意这样求出的字符串是倒过来的,而且数据不保证答案都是回文串,所以输出答案的时候需要反过来。
代码
#include <iostream> #define N 200005 int n,q,li[N],buc[N]; std::string s,ans; struct query {int l,r;std::string t;} a[N]; struct sgt { int d[N<<5],ls[N<<5],rs[N<<5],idx; #define mid (lb+rb>>1) void modify(int &x,int t,int k,int lb,int rb) { if(!x) x=++idx; d[x]+=k; if(lb==rb) return; if(t<=mid) modify(ls[x],t,k,lb,mid); else modify(rs[x],t,k,mid+1,rb); } int query(int x,int l,int r,int lb,int rb) { if(!x) return 0; if(l<=lb&&rb<=r) return d[x]; int ret=0; if(l<=mid) ret+=query(ls[x],l,r,lb,mid); if(r>mid) ret+=query(rs[x],l,r,mid+1,rb); return ret; } int merge(int x,int y,int lb,int rb) { if((!x)||(!y)) return x|y; int nx=++idx; d[nx]=d[x]+d[y]; if(lb<rb) ls[nx]=merge(ls[x],ls[y],lb,mid),rs[nx]=merge(rs[x],rs[y],mid+1,rb); return nx; } #undef mid } tre; struct SAM { int tr[N<<1][26],fail[N<<1],len[N<<1],rt[N<<1],idx,last; bool vis[N<<1]; void insert(char c,int pos) { c-='a'; int cur=++idx,p=last; len[cur]=len[p]+1; tre.modify(rt[cur],pos,1,1,n); while(p&&!tr[p][c]) tr[p][c]=cur,p=fail[p]; if(!p) fail[cur]=1; else { int q=tr[p][c]; if(len[q]==len[p]+1) fail[cur]=q; else { int cq=++idx; len[cq]=len[p]+1; for(int i=0;i<26;i++) tr[cq][i]=tr[q][i]; fail[cq]=fail[q],fail[q]=fail[cur]=cq; while(p&&tr[p][c]==q) tr[p][c]=cq,p=fail[p]; } } last=cur; } void build() { for(int i=1;i<=idx;i++) buc[len[i]]++; for(int i=1;i<=n;i++) buc[i]+=buc[i-1]; for(int i=1;i<=idx;i++) li[buc[len[i]]--]=i; for(int i=idx;i;i--) rt[fail[li[i]]]=tre.merge(rt[fail[li[i]]],rt[li[i]],1,n); } bool dfs(int k,int x,int id) { if(k<a[id].t.size()) { int u=tr[x][a[id].t[k]-'a']; if(u&&tre.query(rt[u],a[id].l+k,a[id].r,1,n)) if(dfs(k+1,u,id)) {ans.push_back(a[id].t[k]);return 1;} } int lim=0; if(k<a[id].t.size()) lim=a[id].t[k]-'a'+1; for(int i=lim;i<26;i++) { int v=tr[x][i]; if(v&&tre.query(rt[v],a[id].l+k,a[id].r,1,n)) {ans.push_back(i+'a');return 1;} } return 0; } } sam; main() { sam.idx=sam.last=1; std::ios::sync_with_stdio(0),std::cin.tie(0); std::cin>>s>>q; n=s.size(); for(int i=1;i<=n;i++) sam.insert(s[i-1],i); sam.build(); for(int i=1;i<=q;i++) { std::cin>>a[i].l>>a[i].r>>a[i].t; ans=""; if(!sam.dfs(0,1,i)) std::cout<<-1; else for(int j=ans.size();j;j--) std::cout<<ans[j-1]; std::cout<<'\n'; } }
最开始想的是离线处理,然后假了
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】