NOI2011 阿狸的打字机
阿狸的打字机
阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机。打字机上只有28个按键,分别印有26个小写英文字母和'B'、'P'两个字母。
经阿狸研究发现,这个打字机是这样工作的:
- 输入小写字母,打字机的一个凹槽中会加入这个字母(这个字母加在凹槽的最后)。
- 按一下印有'B'的按键,打字机凹槽中最后一个字母会消失。
- 按一下印有'P'的按键,打字机会在纸上打印出凹槽中现有的所有字母并换行,但凹槽中的字母不会消失。
例如,阿狸输入aPaPBbP,纸上被打印的字符如下:
a
aa
ab
我们把纸上打印出来的字符串从1开始顺序编号,一直到n。打字机有一个非常有趣的功能,在打字机中暗藏一个带数字的小键盘,在小键盘上输入两个数(x,y)(其中1≤x,y≤n),打字机会显示第x个打印的字符串在第y个打印的字符串中出现了多少次。
阿狸发现了这个功能以后很兴奋,他想写个程序完成同样的功能,你能帮助他么?
\(1<=N<=10^5\),\(1<=M<=10^5\),输入总长\(<=10^5\)
huzecong的题解
看到这个题目我们可以想到一个朴素的算法:预处理出每个字串,对于每个询问,直接计算x串在y串中出现的次数。令有字串总长为L,每个字串平均长度为L,复杂度为\(O(ML^2)\)。如果使用KMP算法优化,则可以达到\(O(ML)\)的复杂度。但是本题输入总长可以达到十万,还是会超时。那么我们就得转换一下思路。
既然这道题要求对多个字符串进行匹配,那么我们很自然地会想到用AC自动机。下面简单地介绍一下AC自动机,实现和代码可以参考这个链接。
AC自动机全名Aho-Corasick字符串匹配算法,是基于Trie树的算法。Trie树又称字典树、单词查找树、前缀树……等。Trie树除其根节点之外,每个节点包含一个字符,从根节点到某一节点路径上经过的字符连接起来,即为该节点所对应的字符串。每个节点的子节点的字符都不一样,因此可以杰森空间,并减少无谓的字符串比较。AC自动机则是在Trie树的基础上给每个节点增加了一个fail指针,指向的是和当前节点的字符串拥有最长相同后缀的字符串对应的节点。
由于AC自动机的这个性质,我们可以用它以O(N)的时间来统计一个字符串中出现了多少给定的字符串。
那么这道题和AC自动机有什么关系?
既然题目中是对各个子串进行匹配,那么我们首先对这些子串建一个Trie树,然后再构建其fail指针:
我们可以发现,如果字符串a可以通过fail指针指向字符串b,那么就说明a串中包含b串。那么对于一个字符串a,如果其中有n个节点的fail指针可以指向字符串b,就说明b串在a串中出现了n次。于是我们可以得到一个基于这个思想的朴素离线算法:枚举每个y字串,维护一个计数器,从根一路遍历到y字串的末尾节点,途中对于每个节点,如果其fail指针指向的是某x串的末尾节点,那么就累加这个串的计数器。但是这个算法的复杂度还是可以达到\(O(ML)\)。
我们不妨转换一下思路,对于每个x串,只有能通过fail指针指向它的末尾节点的y串节点才能计数。那么我们不妨把fail指针反向,构建一棵fail树。由于在一颗树中,一个节点及其子树在DFS序中是连续的一段,那么我们可以用一个树状数组来维护x串末尾节点及其子树上有多少个属于y串的节点。
那么我们可以得到一个离线算法:对fail树遍历一遍,得到一个DFS序,再维护一个树状数组,对原Trie树进行遍历,每访问一个节点,就修改树状数组,对树状数组中该节点的DFS序起点的位置加上1。每往回走一步,就减去1。如果访问到了一个y字串的末尾节点,枚举询问中每个y串对应的x串,查询树状数组中x串末尾节点从DFS序中的起始位置到结束位置的和,并记录答案。这样,我们就得到了一个时间复杂度为\(O(N+M \log_2N)\)的优美的算法。因为\(N\)和\(M\)都不超过\(10^5\),所以这个算法是可行的。
co int N=1e5+1;
int n;
char s[N];
int sz,num;
int ch[N][26],fa[N],ref[N],fail[N];
int nx[N],to[N];
int L[N],R[N],dfn;
void dfs(int u)
{
L[u]=++dfn; // edit 1:++dfn
for(int i=to[u];i;i=nx[i])
dfs(i);
R[u]=dfn;
}
void build()
{
int u=0;
for(int i=0;i<n;++i)
{
if(s[i]=='P')
ref[++num]=u;
else if(s[i]=='B')
u=fa[u];
else
{
int k=s[i]-'a';
if(!ch[u][k])
ch[u][k]=++sz,fa[sz]=u;
u=ch[u][k];
}
}
std::queue<int>Q;
for(int i=0;i<26;++i)
if(ch[0][i])
Q.push(ch[0][i]);
while(Q.size())
{
int u=Q.front();Q.pop();
nx[u]=to[fail[u]],to[fail[u]]=u;
for(int i=0;i<26;++i)
{
if(ch[u][i])
{
fail[ch[u][i]]=ch[fail[u]][i];
Q.push(ch[u][i]);
}
else
ch[u][i]=ch[fail[u]][i];
}
}
dfs(0);
}
int c[N];
#define lowbit(x) (x&-x)
void insert(int p,int v)
{
for(int i=p;i<=dfn;i+=lowbit(i))
c[i]+=v;
}
int query(int p)
{
int res=0;
for(int i=p;i;i-=lowbit(i))
res+=c[i];
return res;
}
typedef std::pair<int,int> pii;
std::vector<pii>g[N];
int ans[N];
void solve()
{
int u=0;
for(int i=0;i<n;++i)
{
if(s[i]=='P')
for(int j=0;j<g[u].size();++j)
{
int v=g[u][j].first,id=g[u][j].second;
ans[id]=query(R[v])-query(L[v]-1);
}
else if(s[i]=='B')
{
insert(L[u],-1);
u=fa[u];
}
else
{
u=ch[u][s[i]-'a'];
insert(L[u],1);
}
}
}
int main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
scanf("%s",s);
n=strlen(s);
build();
int m=read<int>();
for(int i=1;i<=m;++i)
{
int x,y;
read(x),read(y);
g[ref[y]].push_back(pii(ref[x],i));
}
solve();
for(int i=1;i<=m;++i)
printf("%d\n",ans[i]);
return 0;
}
dfs序统计的时候,要用闭区间,只有这两种写法:
void dfs(int u)
{
L[u]=++dfn; // edit 1:++dfn
for(int i=to[u];i;i=nx[i])
dfs(i);
R[u]=dfn;
}
&
void dfs(int u)
{
L[u]=dfn++;
for(int i=to[u];i;i=nx[i])
dfs(i);
R[u]=dfn-1;
}