昨天开始学的…随便写点记录一下以免忘了…
因为学的比较混乱,欢迎在评论互相交流,欢迎打脸
这是复习…
求nxt数组这个DP,思想好好想想就没问题,代码好好看看就没问题,不难
会了这个,才能继续往下学…
void getnxt(char s[])
{
nxt[0] = nxt[1] = 0;
int l = strlen(s);
for(int i = 1;i < l;i ++)
{
int j = nxt[i];
while(j && s[i] != s[j]) j = nxt[j];
nxt[i + 1] = s[i] == s[j] ? j + 1 : 0;
}
}
int kmp(char s1[],char s2[])
{
getnxt(s1);
int ans = 0;
int n = strlen(s1),m = strlen(s2);
for(int i = 0,j = 0;i < m;i ++)
{
while(j && s1[j] != s2[i]) j = nxt[j];
if(s1[j] == s2[i]) j ++;
if(j == n) ans ++;
}
return ans;
}
这个也是复习…
代码太好写,就不写了…
空间复杂度好谜啊,我还是开到内存限制吧…
kmp是单模式串匹配,那AC自动机就是多模式串匹配…
经典裸题是:给n个串,再给一个串s,询问n个串总共在s中出现多少次。
AC自动机 = trie树+KMP,在trie树上做kmp即可…构造方法和kmp很像
kmp重点在nxt数组,也就是fail指针,AC自动机也是这样。
模仿KMP:KMP求nxt数组是线性的DP,按第几位划分阶段,nxt一定指向前面某个状态。因为在trie树上有好多串,所以可以bfs构造。若用str(u)表示根节点到u号节点构成的串,那么str(fail[u])是str(u)的一个后缀,这是由定义可知的。
有了这个就可以方便做很多事。因为str(fail[u])是str(u)的一个后缀,也是str(u的子树中的节点)的一个子串。
fail指针是多对一的,反向后可以得到一颗树。可以反映为某一节点是它子孙节点的子串。在树中保存一些信息,可以把字符串查询问题转化成树上问题,就可以用各种东西维护了。
关于复杂度,我研究了一下…也不知道对不对…
若有n个串,长度为L,建trie树的时间复杂度是 O(NL) ,建立AC自动机的复杂度是 O(NL+L) 。若拿长度为m的串做匹配,时间复杂度是 O(NL+M) 。
代码给出插入字符串、建立AC自动机和查询匹配次数。
void insert(char s[])
{
int p = 0;
int l = strlen(s);
for(int i = 0;i < l;i ++)
{
int c = s[i] - 'a';
if(!ch[p][c]) ch[p][c] = ++ sz;
p = ch[p][c];
}
val[p] ++;
}
void build_ac()
{
fail[0] = 0;
for(int i = 0;i < 26;i ++)
{
int u = ch[0][i];
if(u) { q.push(u); fail[u] = 0; }
}
while(q.size())
{
int f = q.front(); q.pop();
for(int i = 0;i < 26;i ++)
{
int u = ch[f][i];
if(!u) continue;
q.push(u);
int v = fail[f];
while(v && !ch[v][i]) v = fail[v];
fail[u] = ch[v][i];
}
}
}
int add_ans(int p)
{
int ans = 0;
for(;p;p = fail[p])
{
ans += val[p];
val[p] = 0;
}
return ans;
}
int ask(char s[])
{
int ans = 0,p = 0;
int l = strlen(s);
for(int i = 0;i < l;i ++)
{
int c = s[i] - 'a';
while(p && !ch[p][c]) p = fail[p];
p = ch[p][c];
ans += add_ans(p);
}
return ans;
}
trie图和AC自动机很像。trie图把AC自动机上不存在的边补上,指向它fail的那个边所指向的点(也就是说用它的fail来替换),并且每个点继承它fail的信息。这样得到的图叫trie图,是一张有向图。
因为是有向图,所以可以很容易划分阶段,可以跑DP。
丢一份bzoj1030
#include<cstdio>
#include<cstring>
#include<iostream>
#include<queue>
#include<algorithm>
using namespace std;
const int SZ = 10010;
const int mod = 10007;
int dp[110][SZ];
int ch[SZ][30],sz = 0,val[SZ];
void insert(char s[])
{
int p = 0;
int l = strlen(s);
for(int i = 0;i < l;i ++)
{
int c = s[i] - 'A' + 1;
if(!ch[p][c]) ch[p][c] = ++ sz;
p = ch[p][c];
}
val[p] ++;
}
queue<int> q;
int fail[SZ];
void build_trieg()
{
fail[0] = 0;
for(int i = 1;i <= 26;i ++)
{
int u = ch[0][i];
if(u) { q.push(u); fail[u] = 0; }
}
while(q.size())
{
int f = q.front(); q.pop();
val[f] += val[fail[f]];
for(int c = 1;c <= 26;c ++)
{
int u = ch[f][c];
if(!u) { ch[f][c] = ch[fail[f]][c]; continue; }
q.push(u);
fail[u] = ch[fail[f]][c];
}
}
}
char s[SZ];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i ++)
{
scanf("%s",s);
insert(s);
}
build_trieg();
dp[0][0] = 1;
for(int len = 1;len <= m;len ++)
{
for(int u = 0;u <= sz;u ++)
{
if(val[u]) continue;
for(int c = 1;c <= 26;c ++)
{
int v = ch[u][c];
dp[len][v] = (dp[len][v] + dp[len - 1][u]) % mod;
}
}
}
int ans = 0,tot = 1;
for(int i = 1;i <= m;i ++) tot = tot * 26 % mod;
for(int i = 0;i <= sz;i ++)
if(!val[i]) ans = (ans + dp[m][i]) % mod;
printf("%d",(tot - ans + mod) % mod);
return 0;
}
好吧这个本来不打算学的,结果暂时没看懂后缀树和后缀自动机,就先学这个了…
我打的 O(nlog2n) 的,好写好调,双关键字计数排序简直恶心…再说如果考场上见到这种题我应该不会写后缀数组…
倍增法求sa数组,O(n)求height数组,没什么问题。
sa数组的用处是可以快速找到字典序第几大的后缀,用处不是很大。
而height数组就厉害得多了…
利用height数组可以方便查询子串信息,可以把两后缀的最长公共前缀转化成RMQ问题。二分答案+height分组验证是很常用的个方法。
利用后缀数组处理两个以及更多的子串,可以用特殊符号把它们连起来。
后缀数组的计数问题。例如用后缀数组计算一个串有多少个不同的子串,贡献是n-sa[i]-height[i],即当前后缀所表示的串减去重复的串。
ydc:后缀数组处理点对有一个技巧,就是启发式合并。按height分组后,从大到小枚举height然后启发式合并计算贡献… Orzydc,我看不懂
大概就是这样了…
bool cmp_sa(int i,int j)
{
if(rank[i] != rank[j]) return rank[i] < rank[j];
else
{
int x = i + k <= n ? rank[i + k] : -1;
int y = j + k <= n ? rank[j + k] : -1;
return x < y;
}
}
void get_sa(char s[])
{
for(int i = 0;i <= n;i ++)
{
sa[i] = i;
rank[i] = i < n ? s[i] : -1;
}
for(k = 1;k <= n;k <<= 1)
{
sort(sa,sa + 1 + n,cmp_sa);
tmp[sa[0]] = 0;
for(int i = 1;i <= n;i ++)
tmp[sa[i]] = tmp[sa[i - 1]] + (cmp_sa(sa[i - 1],sa[i]) ? 1 : 0);
for(int i = 0;i <= n;i ++)
rank[i] = tmp[i];
}
}
void get_lcp(char s[])
{
int h = 0;
lcp[0] = 0;
for(int i = 0;i <= n;i ++)
rank[sa[i]] = i;
for(int i = 0;i < n;i ++)
{
int j = sa[rank[i] - 1];
if(h) h --;
while(i + h < n && j + h < n)
{
if(s[i + h] == s[j + h]) h ++;
else break;
}
lcp[rank[i] - 1] = h;
}
}
卧槽刚刚搞懂,赶紧记下来QAQ
参考资料:
后缀自动机学习总结
【转】后缀自动机
以及CLJ课件:
2012年noi冬令营陈立杰讲稿
先说如何构建。
后缀自动机每个点保存:这个点的parent指针、根节点到当前点的最长串长度(step),以及这个点的儿子。
记录上一个插入的节点last,新字符c。新建节点np,将last向np连一条c边,然后开始找last的parent指针一直指到根节点路径上的节点,若没有c儿子则把c儿子设为np,然后找到第一个有c儿子的节点p(若p为根节点则跳出),p的c儿子称为q,看看q的step是否等于p的step+1,如果是则直接让np -> par = q;若不等于,则需要新建nq节点,拷贝q节点所有信息,然后若p到根节点的par构成的链中,从p开始若有c儿子指向q,则将其改为nq,否则直接break。
说的有点乱,之后看代码吧…
关于某些不是很显然的东西的证明(个人理解):
为什么若p有c儿子q且q -> step == p -> step + 1则直接np -> par = q呢?
首先,后缀自动机是后缀树+kmp
而step其实是根节点到当前节点的最长串的长度。
若q -> step == p -> step + 1,q是由状态p接受的,p转移到q的串就是last转移到np的串的后缀,因为在原串str后加了c字符,那么由last索引到的串就需要扩充它的Right,添加上Len(str+1),而因为是后缀,索引直接添加np -> par = q没有问题。
为什么若p有c儿子q且q -> step != p -> step + 1则需要拆点等等的事情呢?
先要明确一个性质:一个点的所有入边若边上字母相同则它们构成一个parent链。这个怎么证明…好像仔细想想就有了…
若存在q -> step != p -> step + 1,那同样是连向q的c边,q不是p接受的。定义q的入边发出者为p1,p2…pm,其中p1 -> par = p2,p2 -> par = p3…。则p1 -> step + 1 == q,设当前p为pi,pi把p组成的集合劈成两部分,可以证明pi~pm加上c转移的串不会出现在末尾,也就是Right(str(trans(pj,c)))不会有Len,这样转移就不合法了。
解决方法就是,把q拆点,q接受p1~pi,nq接受pi~pm,nq需要拷贝属于q的所有内容。这样nq成了接受态,p->par = nq,还可得q -> par = nq(kmp嘛233)。然后对于pi~pm,pi,pi+1,pi+2…依次,若trans(pi,c)=q,则改连nq,若碰到一个trans(pi,c)!=q,则立刻break。
虽然有些细节没搞懂,但还是理解了好多…就这样吧…
丢个链接:
后缀自动机(SAM)学习指南
有好多题以及简单题解