题解 LGP9016【[USACO23JAN] Find and Replace G】
一种字符串的自动机。
problem
有一个字符串 \(S\),初始时为 \(\tt 'a'\),现在进行 \(n\) 次操作,第 \(i\) 次操作形如:
- 将 \(S\) 中的所有的字符 \(ch_i\) 替换为字符串 \(T_i\)。
然后输出 \(S[l\sim r]\) 的所有字符。\(\Sigma\) 为全体小写字母,\(n,\sum|T_i|,r-l\leq 2\times 10^5,1\leq l\leq r\leq\min(10^{18},|S|)\)。
solution
考虑离线。考虑设 \(F(d,r)\) 表示在 \(d\) 时刻时,字符 \(r\) 展开得到的最终字符串。所求即 \(F(1,\texttt{'a'})\)。
那我们能将 \(F(d,r)\) 表示出来:
- \(F(d,r)=F(d+1,r)\),如果 \(ch_d\neq r\)。
- \(F(d,r)=F(d+1,{T_i}_1)+F(d+1,{T_i}_2)+\cdots\),如果 \(ch_d=r\)。其中 \(S+T\) 表示拼接字符串 \(S,T\)。
我们强令 \(F(n+1,r)=r\),则我们可以通过 DP 计算这些东西:
- \(|F(d,r))|\),称之为 \(siz(d,r)\)。因为它太大,我们计算时将它对着 \(10^{18}\) 取个 \(\min\) 就行。
现在我们尝试输出。先试着输出前缀。则我们可以
- 写一个
dfs(d,r)
表示展开 \(F(d,r)\) 的过程,然后跟据字符 \(r\) 在这一层的展开,大力的递归下去。 - 直到 \(d>n\) 就停止,输出一个字符。
- 当输出超过 \(R\) 个字符的时候,我们的任务就完成了,直接
exit(0)
走人。 - 我们发现这样做的时间复杂度是不对的。这个转移是一个 DAG,有可能一个点重复走了好多遍,时间用在遍历这些点上。
- 解决的方法:记忆化,我们记录下来输出了什么东西,假设现在输出了 \(S_0\),那么当我们
dfs(d,r)
结束之后,就知道 \(F(d,r)\) 是现在 \(S_0\) 的一段后缀。我们记录这一段后缀的位置,下一次访问时,直接复制上去。
我们需要舍弃掉一些字符,解决方法是简单的:
- 假设已经输出 \(now\) 个字符,
dfs(d,r)
时,我遍历完这一个状态后,会输出 \(siz(d,r)\) 个字符。 - 如果 \(now+siz(d,r)<L\),我们直接返回就是了,跳过遍历的过程。
复杂度分析:
- 每个点最多向下递归两次(一次是交界处,一次在 \([L,R]\) 中)。
- 共 \(O(|\Sigma|n)\) 个点,\(O(\sum|T_i|+|\Sigma|n)\) 条边,复杂度 \(O(\sum|T_i|+|\Sigma|n)\),是可以过的。
code
点击查看代码
考场上并不一定能想的如此清晰,代码写的也不是如此清晰,例如 \(to\) 数组是无用的。
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr,##__VA_ARGS__)
#else
#define debug(...) void(0)
#endif
typedef long long LL;
void red(LL&x){x=min(x,(LL)2e18);}
int n;
LL L,R;
int to[200010][26],use[200010][26];
LL siz[200010][26];//siz[i][r] 是在第 i 层展开字符 r 的最终字符串大小,和 2e18 取 min
string s[200110];
void build(){//补全自动机
for(int r=0;r<26;r++) to[n+1][r]=n+1,siz[n+1][r]=1,use[n+1][r]=n+r+1;
for(int i=n;i>=1;i--){
for(int r=0;r<26;r++){
if(!to[i][r]) use[i][r]=n+r+1,to[i][r]=i+1;
for(char&ch:s[use[i][r]]) red(siz[i][r]+=siz[to[i][r]][ch-'a']);//按照定义展开,不会 TLE
}
}
}
LL now=0;//当前已经输出了第 now 个字符,当然是假输出
char _[200010],*pos=_;
void print(int r){
// cerr<<char(r+'a');
*pos++=r+'a';
if(++now>=R) cout<<_<<endl,exit(0);
}
char*st[200010][26],*ed[200010][26];
void dfs(int u,int r){//当前在第 u 层,正在展开字符 r,可以用到 s[u]
debug("dfs(%d,%d),to=%d,siz=%lld,now=%lld\n",u,r,to[u][r],siz[u][r],now);
if(now+siz[u][r]<L) return now+=siz[u][r],void();//留回家过年
//我的任务完成了!!!
//否则 L<=now+1<=R 多少有点关系
//大力展开
if(st[u][r]){
debug("revisit (%d,%d)\n",u,r);
for(char*p=st[u][r];p!=ed[u][r];p++) print(*p-'a');
// debug("[");
// for(char*p=st[u][r];p!=ed[u][r];p++) debug("%c",*p);
// debug("]");
return ;
}
if(now>=L) st[u][r]=pos;debug("{");
if(u==n+1) print(r);//到家了
else for(char&ch:s[use[u][r]]) dfs(to[u][r],ch-'a');
ed[u][r]=pos;debug("}");
}
/*
复杂度不对
如果我们保证每个点值踩一次,复杂度也对,前提是不缩链
*/
int main(){
#ifdef LOCAL
freopen("input.in","r",stdin);
#else
cin.tie(0)->sync_with_stdio(0);
#endif
cin>>L>>R>>n;
for(int i=1;i<=n;i++){
char ch;
cin>>ch>>s[i];
to[i][ch-'a']=i+1;
use[i][ch-'a']=i;
}
for(int i=n+1;i<=n+26;i++) s[i]=i-n-1+'a';
build();
dfs(1,0);
cout<<_<<endl;
return 0;
}
/*
1 8 4
a ab
a bc
c de
b bbb
abbbbbbbbbcbbbbbbbbbezzzg
*/
本文来自博客园,作者:caijianhong,转载请注明原文链接:https://www.cnblogs.com/caijianhong/p/solution-p9016.html