BZOJ4502: 串

4502: 串

Time Limit: 30 Sec  Memory Limit: 512 MB
Submit: 245  Solved: 123
[Submit][Status][Discuss]

Description

兔子们在玩字符串的游戏。首先,它们拿出了一个字符串集合S,然后它们定义一个字
符串为“好”的,当且仅当它可以被分成非空的两段,其中每一段都是字符串集合S中某个字符串的前缀。
比如对于字符串集合{"abc","bca"},字符串"abb","abab"是“好”的("abb"="ab"+"b",abab="ab"+"ab"),而字符串“bc”不是“好”的。
兔子们想知道,一共有多少不同的“好”的字符串。

Input

第一行一个整数n,表示字符串集合中字符串的个数
接下来每行一个字符串

Output

一个整数,表示有多少不同的“好”的字符串

Sample Input

2
ab
ac

Sample Output

9

HINT

1<=n<=10000,每个字符串非空且长度不超过30,均为小写字母组成。


Source

 

 

【题解】

感觉稍微了解了一点AC自动机上DP的套路。。

先考虑答案串在AC自动机上匹配会怎样

1、没有失配,说明答案串是某个串的前缀,但是答案串应是两个非空串拼起来的,因此要判断匹配终点是否有fail值,有fail值才能加入答案

2、失配了,失配之后可能会跳很多fail,到达终点v

关键是第二种情况如何统计?

一种状态是dp[l][i]表示已匹配串长为l,失配过,到达点i的方案数。转移时枚举下一个字符,若没有需要跳fail。

不跳fail可以直接转移,但是如果跳fail,怎么保证跳fail跳到终点v后,root->v能跟前面的串拼接或有重叠部分拼接呢?

画画图,发现root->v的串长必须大于等于第一次失配后又添加字符的串长,不然肯定形成的串中间有一部分既不属于第一次失配前的串也不属于root->v

显然dp[l][i]是不够的应有dp[l][i][j]表示已匹配串长为l,到达点i,第一次在j点失配

进一步观察发现,如果两个字符串第一次失配的位置一样,最终到达的位置也一样,这两个串相同

因此每一个串与失配的地方和最终到达的地方是一一对应的

为了方便转移,我们选最终到达的地方,统计由多少失配的地方走过来,为了判断答案是否符合要求,还应记录第一次失配后又添加串长是多少

于是就有dp[i][j]表示第一次失配后又走了i步,到达j的方案

枚举下一个字母k,j儿子里有k直接转移,没有就按匹配的规则,跳到有k的fail转移

这里加了一点优化,就是插入的时候,如果没有ch[u][k],就让他连上ch[fail[u][k],这样就省去了跳的一步,获得更加稳定的复杂度

因此要加一个vis[u][k]表示u原本有没有k这个儿子

别忘了第一种情况

 1 #include <iostream>
 2 #include <cstdio>
 3 #include <cstring>
 4 #include <cstdlib>
 5 #include <algorithm>
 6 #define max(a, b) ((a) > (b) ?(a) : (b))
 7 inline void read(long long &x)
 8 {
 9     x = 0;char ch = getchar(), c = ch;
10     while(ch < '0' || ch > '9') c = ch, ch = getchar();
11     while(ch <= '9' && ch >= '0') x = x * 10 + ch - '0', ch = getchar();
12     if(c == '-') x = -x;
13 }
14 
15 const long long INF = 0x3f3f3f3f;
16 const long long MAXN = 10000 + 5;
17 const long long MAXLEN = 30 + 5;
18 const long long MAXNODE = MAXN * MAXLEN + 10; 
19 
20 long long n,cnt,ch[MAXNODE][30], vis[MAXNODE][30], deep[MAXNODE], tag[MAXNODE], fail[MAXNODE], refail[MAXNODE];
21 char s[MAXN][MAXLEN];
22 
23 void insert(long long x)
24 {
25     long long now = 0;
26     for(long long i = 1;s[x][i] != '\0';++ i)
27     {
28         long long &tmp = ch[now][s[x][i] - 'a' + 1];
29         vis[now][s[x][i] - 'a' + 1] = 1;
30         if(tmp) now = tmp;
31         else now = tmp = ++ cnt;
32     }
33     ++ tag[now];
34 }
35 long long q[MAXNODE], he, ta;
36 void build()
37 {
38     he = ta = 0;
39     for(long long i = 1;i <= 26;++ i) if(ch[0][i]) q[ta ++] = ch[0][i], deep[ch[0][i]] = 1;
40     while(he < ta)
41     {
42         long long now = q[he ++];
43         for(long long i = 1;i <= 26;++ i)
44         {
45             long long u = ch[now][i];
46             if(!u){ch[now][i] = ch[fail[now]][i];continue;}
47             long long v = fail[now];q[ta ++] = u;
48             while(v && !ch[v][i]) v = fail[v];
49             fail[u] = ch[v][i];
50             deep[u] = deep[now] + 1;
51         }
52     }
53 }
54 
55 long long dp[40][MAXNODE], ma; 
56 
57 long long DP()
58 {
59     long long ans = 0;
60     for(long long i = 1;i <= cnt;++ i) if(fail[i]) ++ ans;
61     for(long long i = 1;i <= cnt;++ i)
62         for(long long j = 1;j <= 26;++ j)
63             if(!vis[i][j] && ch[i][j])
64                 ++ dp[1][ch[i][j]];
65     for(long long i = 1;i < ma;++ i)
66         for(long long j = 1;j <= cnt;++ j)
67         {
68             ans += dp[i][j];
69             for(long long k = 1;k <= 26;++ k)
70                 if(vis[j][k] || deep[ch[j][k]] > i)
71                     dp[i + 1][ch[j][k]] += dp[i][j];
72         } 
73     for(long long j = 1;j <= cnt;++ j) ans += dp[ma][j];
74     return ans;
75 }
76 
77 int main()
78 {
79     read(n);
80     for(long long i = 1;i <= n;++ i) scanf("%s", s[i] + 1), insert(i), ma = max(ma, strlen(s[i] + 1));
81     build();
82     printf("%lld", DP());
83     return 0;
84 }
BZOJ4502

还有一种O(|S|)的做法,见http://www.cnblogs.com/Oncle-Ha/p/7056109.html

大概说一下,对于每一个答案串,我们令其可以分割的地方的最后一个为分割点,其余的都是冗余点

分割点 + 冗余点 = 串长和的平方

下面统计冗余点有多少

枚举每一个作为后缀的字符串前缀A,在他前面切割的点为冗余点当且仅当这个串A上被fail直接或间接指过

统计直接或间接指过的串的个数

fail树上统计子树大小!

可是如果A上有多个字符被fail指,会统计重复,因为有些串可能有多个节点fail同时直接或间接指向A上节点

发现fail指向A的串B,B上一定有一个节点的fail直接或间接指向A的最小前缀

于是找到最小前缀,在fail树上统计子树个数 - 1即可

比较麻烦,不写了

 

posted @ 2018-01-22 09:50  嘒彼小星  阅读(221)  评论(0编辑  收藏  举报