题解 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
*/
posted @ 2023-02-01 10:27  caijianhong  阅读(128)  评论(0编辑  收藏  举报