NOI2011 阿狸的打字机(BZOJ2434) 题解

 

Link

原题链接:http://www.lydsy.com/JudgeOnline/problem.php?id=2434

 

 

 

Description

阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机。打字机上只有28个按键,分别印有26个小写英文字母和'B'、'P'两个字母。

经阿狸研究发现,这个打字机是这样工作的:

  • 输入小写字母,打字机的一个凹槽中会加入这个字母(这个字母加在凹槽的最后)。
  • 按一下印有'B'的按键,打字机凹槽中最后一个字母会消失。
  • 按一下印有'P'的按键,打字机会在纸上打印出凹槽中现有的所有字母并换行,但凹槽中的字母不会消失。

例如,阿狸输入aPaPBbP,纸上被打印的字符如下:

a

aa

ab

我们把纸上打印出来的字符串从1开始顺序编号,一直到n。打字机有一个非常有趣的功能,在打字机中暗藏一个带数字的小键盘,在小键盘上输入两个数(x,y)(其 中1≤x,y≤n),打字机会显示第x个打印的字符串在第y个打印的字符串中出现了多少次。

阿狸发现了这个功能以后很兴奋,他想写个程序完成同样的功能,你能帮助他么?

 

Input

输入的第一行包含一个字符串,按阿狸的输入顺序给出所有阿狸输入的字符。

第二行包含一个整数m,表示询问个数。

接下来m行描述所有由小键盘输入的询问。其中第i行包含两个整数x, y,表示第i个询问为(x, y)。

 

Output

输出m行,其中第i行包含一个整数,表示第i个询问的答案。

 

Sample Input

aPaPBbP

3

1 2

1 3

2 3

 

Sample Output

2

1

0

 

Hint

1<=M<=10^5

输入总长<=10^5

 

 

 

Analysis

看到这个题目我们可以想到一个朴素的算法:预处理出每个字串,对于每个询问,直接计算x串在y串中出现的次数。令有字串总长为L,每个字串平均长度为L,复杂度为O(ML2)。如果使用KMP算法优化,则可以达到O(ML)的复杂度。但是本题输入总长可以达到十万,还是会超时。那么我们就得转换一下思路。

既然这道题要求对多个字符串进行匹配,那么我们很自然地会想到用AC自动机。下面简单地介绍一下AC自动机,实现和代码可以参考这个链接:http://www.cppblog.com/mythit/archive/2009/04/21/80633.html。

AC自动机全名Aho-Corasick字符串匹配算法,是基于Trie树的算法。Trie树又称字典树、单词查找树、前缀树……等。Trie树除其根节点之外,每个节点包含一个字符,从根节点到某一节点路径上经过的字符连接起来,即为该节点所对应的字符串。每个节点的子节点的字符都不一样,因此可以杰森空间,并减少无谓的字符串比较。AC自动机则是在Trie树的基础上给每个节点增加了一个fail指针,指向的是和当前节点的字符串拥有最长相同后缀的字符串对应的节点。

比方说下面的这个Trie树,蓝色的箭头指向的就是其fail指针指向的节点。为了表示便于理解,文章图中每个节点上写的是字符串,在实际的Trie树中每个节点应当只有一个字符。


由于AC自动机的这个性质,我们可以用它以O(N)的时间来统计一个字符串中出现了多少给定的字符串。

那么这道题和AC自动机有什么关系?

 

既然题目中是对各个子串进行匹配,那么我们首先对这些子串建一个Trie树。以样例数据为例,建出的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树。样例数据的fail树如下:

在串(a)的子树中,属于串(aa)的节点有2个(a)和(aa),属于串(ab)的节点有1个(a);在串(aa)的子树中,没有属于串(ab)的节点。

由于在一颗树中,一个节点及其子树在DFS序中是连续的一段,那么我们可以用一个树状数组来维护x串末尾节点及其子树上有多少个属于y串的节点。

那么我们可以得到一个离线算法:对fail树遍历一遍,得到一个DFS序,再维护一个树状数组,对原Trie树进行遍历,每访问一个节点,就修改树状数组,对树状数组中该节点的DFS序起点的位置加上1。每往回走一步,就减去1。如果访问到了一个y字串的末尾节点,枚举询问中每个y串对应的x串,查询树状数组中x串末尾节点从DFS序中的起始位置到结束位置的和,并记录答案。这样,我们就得到了一个时间复杂度为O(N+MlogN)的优美的算法。因为N和M都不超过105,所以这个算法是可行的。

 

 

 

Code

代码实现如下,关键的地方有英文的注解:

http://ideone.com/u6QNJ

//NOI2011(BZOJ2434); 阿狸的打字机; AC Automation - Fail Tree & Binary Indexed Tree
#include 
#include 
#include 
#define N 100000
 
struct edge
{
    int next, node;
};
struct map
{
    int head[N + 1], tot;
    edge e[N + 1];
    map() { tot = 0; }
    inline void addedge(int x, int y)
    {
        e[++tot].next = head[x];
        e[tot].node = y;
        head[x] = tot;
    }
}fail, queries;
//Forward stars, one for fail pointer tree and one for queries
const int root = 1;
struct node
{
    int f, son[26], fail;
}t[N + 1];
//Trie tree
char s[N + 1];
//Entry string
int len, tot = root, strs = 0, m;
int strNode[N + 1];
//Corresponding trie node for a string
int queue[N + 1];
int head[N + 1], tail[N + 1], count = 0;
//DFS sequence
int arr[N + 1];
//Binary indexed tree, storing how many nodes of string y are in a certain node and its subtrees
int ans[N + 1];
//Stores the answer to each of the queries
 
inline int query(int x)
{
    int ret = 0;
    for (; x; x -= x & (-x))
        ret += arr[x];
    return ret;
}
 
inline void modify(int x, int d)
{
    for (; x <= count; x += x & (-x))
        arr[x] += d;
}
 
void dfs(int x)
{
    head[x] = ++count;
    for (int i = fail.head[x]; i; i = fail.e[i].next)
        dfs(fail.e[i].node);
    tail[x] = count;
}
 
int main()
{
    gets(s);
    len = strlen(s);
    int now = root;
    //Read queries and build queries graph
    scanf("%d", &m);
    for (int x, y, i = 0; i < m; ++i)
    {
        scanf("%d%d", &x, &y);
        queries.addedge(y, x);
    }
    //Build trie tree
    for (int i = 0; i < len; ++i)
    {
        if (s[i] == 'P') strNode[++strs] = now;
        else if (s[i] == 'B') now = t[now].f;
        else
        {
            if (t[now].son[s[i] - 'a']) now = t[now].son[s[i] - 'a'];
            else
            {
                int cur = now;
                t[now = t[now].son[s[i] - 'a'] = ++tot].f = cur;
            }
        }
    }
    //Construct fail pointers and build fail pointer tree
    int l = 0, r = -1;
    for (int i = 0; i < 26; ++i)
    {
        if (t[root].son[i])
        {
            queue[++r] = t[root].son[i];
            fail.addedge(root, queue[r]);
            t[queue[r]].fail = root;
        }
    }
    for (; l <= r; ++l)
    {
        for (int i = 0; i < 26; ++i)
            if (t[queue[l]].son[i])
            {
                queue[++r] = t[queue[l]].son[i];
                for (now = t[queue[l]].fail; now != root && !t[now].son[i]; now = t[now].fail) ;
                t[queue[r]].fail = t[now].son[i] ? t[now].son[i] : root;
                fail.addedge(t[queue[r]].fail, queue[r]);
            }
    }
    //Construct DFS sequence of fail pointer tree
    dfs(root);
    //Traverse through trie tree while enumerating y string
    now = root, strs = 0;
    for (int qs, i = 0; i < len; ++i)
    {
        if (s[i] == 'B')
        {
            modify(head[now], -1);
            now = t[now].f;
        }
        else if (s[i] != 'P')
        {
            now = t[now].son[s[i] - 'a'];
            modify(head[now], 1);
        }
        else
        {
            for (int x = queries.head[++strs]; x; x = queries.e[x].next)
                ans[x] = query(tail[strNode[queries.e[x].node]]) - query(head[strNode[queries.e[x].node]] - 1);
        }
    }
    for (int i = 1; i <= m; ++i)
        printf("%d\n", ans[i]);
    return 0;
}

你可能感兴趣的:(OI)