广义 SAM 学习笔记
首先,如何手造一个广义 SAM
本质上就是建多串后缀树(后缀树还是蛮好建的吧……
广义 SAM 主要解决多模式串问题(统计一些奇奇怪怪的问题
记号
记 \(O(|\Sigma|)\) 为字符集大小
记 \(G(T) = O(\sum len)=O(|T|)\)
(其中,\(G(T)\) 为 \(Trie\) 树 \(T\) 所有叶节点深度之和,\(|T|\) 为 \(Trie\) 树大小)
在线构造
在线算法指,不建出 \(Trie\) 树即可求出广义 \(SAM\)。
实现也非常简单,只需要在单串插入建 \(SAM\) 的基础上加几个特判,并且在新插入一个串的时候将 \(last = 1\) 即可。
inline void add(int x)
{
int p=last,q,now,k,ch=1;
if(q=son[x][p]) //特判有转移函数的时候考虑直接拆分节点,避免空节点干扰
{
if(len[p] + 1==len[q]) return (void)(last = q);//如果转移函数的目标恰好与我们想要的一致 ,直接返回即可
len[k=++cnt]=len[p]+1,link[k] = link[q],link[q] = last =k;//如果不一致考虑直接拆分出我们想要的
for(;ch<=26;son[ch][k]=son[ch][q],ch++);
for(;p&&!(son[x][p]^q);son[x][p]=k,p=link[p]);
return;
}
last=now=++cnt,len[now] = len[p] + 1;
for(;p&&!son[x][p];son[x][p]=now,p=link[p]);
if(!p) return (void)(link[now]=1);
if(len[p]+1==len[q=son[x][p]]) return (void)(link[now]=q);
len[k=++cnt] = len[p] + 1,link[k] = link[q],link[q] = link[now] = k;
for(;ch<=26;son[ch][k]=son[ch][q],ch++);
for(;p&&!(son[x][p]^q);son[x][p]=k,p=link[p]);
}
解释一下特判的含义
特判1:
if(q=son[x][p])
原转移函数是否存在,如果存在进一步处理,若不存在则按单串插入规则插入。
特判2:
if(q=son[x][p] && len[p] + 1 != len[q])
在原转移函数存在的情况下,并且处于一个节点的压缩状态,则把这个节点分裂开,按照单串插入中的分裂操作进行即可。返回时,last 为分裂出的节点。
若不处于压缩态,而是表达态,\(last\) 记为 \(q\) 即可。
正确性证明:
首先要知道为什么不在新建节点 \(now\) 然后连边处理。易知,若满足特判一且 len[p] + 1 == len[q]
时,\(q\) 这个节点与我们即将要插入的这个节点是等价的,为了保证子串信息不被分散,应该把这两个节点合并,所以直接返回 \(last = q\)。
若满足特判二,则说明 \(q\) 这个节点中有一个节点和我们当前要插入的节点是完全等价的。但是它现在处于压缩状态,我们需要分裂这个节点,让压缩态变成表达态。然而,若是分裂之后,不就又相当于特判一的情况了吗?所以我们在当前情况下不需要新建这个节点,只需要分裂 \(q\) ,返回 \(last\) 为分裂出的节点。
复杂度结论:(请牢牢记清)
状态数为线性 \(O(2|T|)\)
转移函数上界为 \(O(|T||\Sigma|)\)
在线算法时间复杂度 \(O(|T||A|+G(T))\)
实际来说,在线算法比离线快……(这可太离谱了,离线算法还有什么存在意义???
P6139 【模板】广义后缀自动机(广义 SAM)
板子,不多 bb
#include<bits/stdc++.h>
using namespace std;
#define Int unsigned long long
#define INF 1ll<<60
template<typename _T>
inline void read(_T &x)
{
x=0;char s=getchar();int f=1;
while(s<'0'||s>'9') {f=1;if(s=='-')f=-1;s=getchar();}
while('0'<=s&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
x*=f;
}
#define lowbit(x) (x&(-x))
#define gb(x) ((x-1)/T + 1)
#define gl(x) ((x-1)*T + 1)
#define pb push_back
#define fi first
#define sd second
#define Re register
#define MOD(x) (x = (x + mod)%mod)
#define pii pair<int,int>
#define inrange(L,R) (L<=l&&r<=R)
#define outofrange(L,R) (r < L||R < l)
#define sto set<node>::iterator
const int np = 3e6 + 5;
const int nnp = 2e3 + 5;
const int mod = 1e9 + 7;
namespace SAM{
int len[np];
int last = 1,cnt = 1;
int son[28][np];
int link[np];
inline void add(int x)
{
int p=last,q,now,k,ch=1;
if(q=son[x][p]) //特判有转移函数的时候考虑直接拆分节点,避免空节点干扰
{
if(len[p] + 1==len[q]) return (void)(last = q);//如果转移函数的目标恰好与我们想要的一致 ,直接返回即可
len[k=++cnt]=len[p]+1,link[k] = link[q],link[q] = last =k;//如果不一致考虑直接拆分出我们想要的
for(;ch<=26;son[ch][k]=son[ch][q],ch++);
for(;p&&!(son[x][p]^q);son[x][p]=k,p=link[p]);
return;
}
last=now=++cnt,len[now] = len[p] + 1;
for(;p&&!son[x][p];son[x][p]=now,p=link[p]);
if(!p) return (void)(link[now]=1);
if(len[p]+1==len[q=son[x][p]]) return (void)(link[now]=q);
len[k=++cnt] = len[p] + 1,link[k] = link[q],link[q] = link[now] = k;
for(;ch<=26;son[ch][k]=son[ch][q],ch++);
for(;p&&!(son[x][p]^q);son[x][p]=k,p=link[p]);
}
}
int n;
char c[np];
int bac[np],sa[np];
signed main()
{
read(n);
for(int i=1;i<=n;i++)
{
scanf("%s",c+1);
int len = strlen(c + 1);
for(int j=1;j<=len;j++) SAM::add(c[j]-'a'+1);
SAM::last = 1;
}
long long Ans = 0;
for(int i=1;i<=SAM::cnt;i++)Ans +=SAM::len[i] - SAM::len[SAM::link[i]];
cout<<Ans;
}
P3346 [ZJOI2015]诸神眷顾的幻想乡
ajh 放的傻逼题,看到叶子节点很少,直接对叶子节点暴力建广义就行……
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define Int unsigned long long
#define INF 1ll<<60
template<typename _T>
inline void read(_T &x)
{
x=0;char s=getchar();int f=1;
while(s<'0'||s>'9') {f=1;if(s=='-')f=-1;s=getchar();}
while('0'<=s&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
x*=f;
}
#define lowbit(x) (x&(-x))
#define gb(x) ((x-1)/T + 1)
#define gl(x) ((x-1)*T + 1)
#define pb push_back
#define fi first
#define sd second
#define Re register
#define MOD(x) (x = (x + mod)%mod)
#define pii pair<int,int>
#define inrange(L,R) (L<=l&&r<=R)
#define outofrange(L,R) (r < L||R < l)
#define sto set<node>::iterator
const int np = 3e6 + 5;
namespace SAM{
int len[np];
int link[np];
int cnt=1,last=1;
int son[10][np];
inline void add(int x)
{
int p=last,q,now,k,ch=0;
if(q=son[x][p])
{
if(len[p]+1==len[q]) return (void)(last=q);
len[k = ++cnt] = len[p] + 1,link[k] = link[q],link[q] = last = k;
for(;ch<=9;son[ch][k]=son[ch][q],ch++);
for(;p&&!(son[x][p]^q);son[x][p]=k,p=link[p]);
return ;
}
now = last = ++cnt,len[now] = len[p] + 1;
for(;p && !son[x][p];son[x][p]=now,p=link[p]);
if(!p) return (void)(link[now]=1);
if(len[p] + 1 == len[q=son[x][p]]) return (void)(link[now] = q);
len[k = ++cnt] = len[p] + 1,link[k] = link[q],link[q] = link[now] = k;
for(;ch<=9;son[ch][k]=son[ch][q],ch++);
for(;p&&!(son[x][p]^q);son[x][p]=k,p=link[p]);
}
};
int n,c;
int head[np],cx[np],nxt[np * 2],ver[np * 2],tit,top;
int s[np],s_[np],top_;
inline void add(int x,int y)
{
ver[++tit] = y;
nxt[tit] = head[x];
head[x] = tit;
}
inline void dfs(int x,int ff)
{
int leaf = 1;
for(int i=head[x],v;i;i=nxt[i])
{
v = ver[i];
if(v == ff) continue;
leaf=0;
dfs(v,x);
}
if(leaf) s[++top] = x;
}
inline void dfs_(int x,int ff,int la)
{
int cop = SAM::last;
SAM::last = la;
SAM::add(cx[x]);
la = SAM::last;
for(int i=head[x],v;i;i=nxt[i])
{
v = ver[i];
if(v == ff) continue;
dfs_(v,x,la);
}
SAM::last = cop;
}
signed main()
{
read(n);
read(c);
for(int i=1;i<=n;i++) read(cx[i]);
for(int i=1,u,v;i<n;i++)
{
read(u);
read(v);
add(u,v);
add(v,u);
}
int num =0 ;
for(int i=head[1];i;i=nxt[i]) num++;
if(num==1) s[++top] = 1;
dfs(1,0);
for(int i=1;i<=top;i++) dfs_(s[i],0,1);
int Ans = 0;
for(int i=1;i<=SAM::cnt;i++)
Ans += SAM::len[i] - SAM::len[SAM::link[i]] ;
cout<<Ans<<'\n';
}
其实我本来是不想学广义的,但培训要讲……
行吧,培训的时候找个机会切掉 [NOI2018] 你的名字