《基本子串结构》阅读笔记
(就是对一些严谨概念的形式化)
定义 3.1(扩展串):将子串 \(t\) 左右扩张到最长的 \(t'\) 使得 \(t\) 与 \(t'\) 的出现次数相同,称为扩展串 \(\mathrm{ext}(t)\)。
引理 3.1:\(\mathrm{ext(t)}\) 存在且唯一。(注:这里的唯一指唯一的位置 \([l,r]\),而非仅仅字符串唯一)
证明:设 \([l_1,r_1],[l_2,r_2]\) 是不同的最长扩展串,则 \([\min(l_1,l_2),\max(r_1,r_2)]\) 也是扩展串,矛盾。
推论 3.1:若 \(t=[l,r]\) 的扩展串为 \(t'=[l',r']\),那么 \(t\subseteq t_0 \subseteq t'\) 的 \(t_0\) 的扩展串也是 \(t'\)。
推论 3.2:\(\mathrm{ext}(\mathrm{ext}(t))=\mathrm{ext}(t)\)。
定义 3.2(等价类):所有 \(\mathrm{ext}(t)\) 相同的 \(t\) 形成一个等价类。
定义 3.3(代表元):\(t=\mathrm{ext}(t)\) 的 \(t\) 称为该等价类的代表元,记做 \(\mathrm{rep}(a)\)。
引理 3.2:每个等价类的代表元唯一。(显然)
引理 3.3:若 \(t_1\subseteq t_2\) 且 \(\mathrm{occ}(t_1)=\mathrm{occ}(t_2)\),则 \(\mathrm{ext}(t_1)=\mathrm{ext}(t_2)\)。
证明:\(t_1\subseteq t_2\subseteq \mathrm{ext}(t_2)\),\(\mathrm{occ}(t_1)=\mathrm{occ}(t_2)=\mathrm{occ}(\mathrm{ext}(t_2))\),\(\nexists t'\supsetneq \mathrm{ext}(t_2),\mathrm{occ}(t')=\mathrm{occ}(\mathrm{ext}(t_2))\),故 \(\mathrm{ext}(t_1)=\mathrm{ext}(t_2)\)。
定义 3.4:定义 \(t\) 在 \(s\) 中第一次出现位置为 \(s[\mathrm{posl}(t),\mathrm{posr}(t)]\)。
定理 3.1:以 \(l,r\) 为轴建立平面直角坐标系,则对于等价类 \(a\),其元素的 \((\mathrm{posl},\mathrm{posr})\) 构成的点集在平面上形成一个上端与左端对齐的阶梯。
证明:\(t\) 的首次出现的位置与 \(\mathrm{ext}(t)\) 首次出现的位置对应,且由推论 3.1 可知一个点 \(t\) 为右下角,\(\mathrm{rep}(a)\) 为的矩形内的点都属于等价类 \(a\),证毕。
推论 3.3:平面上 \(1\le l\le r\le n\) 的点被划分为若干个互不相交的阶梯,每个等价类 \(a\) 对应于其中 \(\mathrm{occ}(\mathrm{rep}(a))\) 个形状相同的阶梯。(注:由引理 3.1 可得同一 \(a\) 之间的阶梯也不相交)
定义 3.5(周长):定义一个等价类 \(a\) 的周长 \(\mathrm{per}(a)\) 为其所对应的阶梯行数与列数之和。
Note:以下也将一个等价类称为一块。
定理 3.2:对于每一个等价类 \(a\),在其对应的阶梯中,每一行(\(r\) 相同)对应正 \(\mathrm{SAM}\) 上一个节点,每一列(\(l\) 相同)对应于反 \(\mathrm{SAM}\) 上一个节点,且所有等价类的所有行与正 \(\mathrm{SAM}\) 的所有节点一一对应,所有列与反 \(\mathrm{SAM}\) 的所有节点一一对应。“对应”指字符串集合相同。
证明:以下只证明行的部分,列的部分同理。对于一块中的一行,\(r\) 不变,\(\mathrm{occ}\) 不变,因此都在同一个正 \(\mathrm{SAM}\) 节点上;对于正 \(\mathrm{SAM}\) 的一个节点,其所有字符串 \(\mathrm{occ}\) 相同,因此在同一块内,\(r\) 相同,因此在同一行中。证毕。
定理 3.3:所有块的周长之和为 \(O(n)\)。(即正反串 \(\mathrm{SAM}\) 的节点数之和)
Note:最后将正反串 \(parent\) 树的树边补上去,每行向它右边那个节点连 \(T_0\) 的 \(fa\) 边(注意“右边”那个节点指的是右边那个点在整个结构中对应的正 SAM 节点),每列向下面那个节点连 \(T_1\) 的 \(fa\) 边。
定理 3.4:对于正 SAM 的节点,如果它上方的节点和它属于同一等价类,那么它在自动机上只有一条出边连向该节点;否则,其连向该节点在整个结构中所有出现位置上方的点。
证明:可以直接由定义推出。
至此,我们已经完成整个基本子串结构的构建。原串正反 SAM 以及 parent 树的所有信息都已在该基本子串结构中体现。
代码见文末。
说了这么多,看一些例题吧:
例题 0
题意: 给定一个长为 \(n\) 的字符串 \(s\),\(Q\) 次询问给出正 SAM 上节点 \(u\) 和反 SAM 上节点 \(v\),求任意一个同时在两个 SAM 节点中的子串 \(s[l,r]\) 或报告无解。\(n,Q\le 3\times 10^6\)。
解法: 建出基本子串结构,找到 \(u\) 和 \(v\) 所分别对应的行和列,如果二者不在同一等价类内则无解,否则答案即为该行和该列的交点。
点评: 这是基本子串结构最基本的应用:找到 SAM 节点对应的行和列。
例题 1(Uoj577 打击复读)
题意: 给定一个长为 \(n\) 的字符串 \(s\),第 \(i\) 个字符有两个权值 \(wl_i\) 和 \(wr_i\),定义一个子串 \(t\) 的左权值 \(vl(t)\) 为其在原串中出现的所有位置的左端点的 \(wl_i\) 之和,右权值 \(vr(t)\) 为其在原串中出现的所有位置的右端点的 \(wr_i\) 之和,定义其权值 \(w(t)\) 为 \(wl(t)\times wr(t)\)。定义整个串 \(s\) 的“复读程度”为 \(\sum\limits_{i=1}^n\sum\limits_{j=i}^n w(s[i,j])\)。\(Q\) 次修改某个 \(wl_i\) 后查询答案。\(n,Q\le 5\times 10^5\)。
解法: 显然答案是关于所有 \(wl_i\) 的一个线性函数,于是考虑求出这些系数后直接算答案。首先注意到一个子串 \(s[l,r]\) 的左权值即为其在反 parent 树上子树内所有含 \(s[i,n]\) 的节点的 \(wl_i\) 之和,右权值即为其在正 parent 树上子树内所有含 \(s[1,i]\) 的节点的 \(wr_i\) 之和。对应到网格图上,就是在每一个块中,每一行有一个权值 \(vr\),每一列有一个权值 \(vl\),答案即为所有格子“所在行的 \(vr\),所在列的 \(vl\) 以及其所在块的出现次数 \(occ\) 的积”之和。于是在每个块内对每一行的 \(vr\) 做一个前缀和,就能得到每一列的诸多 \(vl\) 会乘上的系数,再自顶至底推下去即可。时间复杂度 \(O(n+Q)\)。AC 代码
点评: 这道题很好地体现了基本子串结构的特点:利用网格图的特点,在每个块内进行前缀和处理信息,同时保留 parent tree 的性质。
例题 2(Uoj 697 广为人知题)
题意: 给定一个长为 \(n\) 的字符串 \(s\) 以及 \(s\) 的 \(m\) 个子串 \(s[l_i,r_i]\) 作为模式串,接下来 \(Q\) 次询问,每次询问给出 \(ql_i,qr_i\),求所有模式串在 \(s[ql_i,qr_i]\) 中出现的次数之和。\(n\le 4\times 10^5,m\le 10^6,Q\le 10^5\)。
解法:
template<int NN,int Z>
class SAM{
static constexpr int N=NN*2;
public:
int OO,tot,n,Lst,ch[N][Z],fa[N],len[N],pre[N],posl[N],posr[N],occ[N],lis[N],tcnt[N],tmp[N],_ffa[__lg(N)+1][N],ffa[N][__lg(N)+1]; vector<int> son[N];
SAM(int OOO=0) : OO(OOO) { tot=Lst=1; }
inline void push_back(int c){
int np=++tot,p=Lst; Lst=np; occ[np]=1; len[np]=++n; pre[n]=np; posr[np]=n;
while(p&&!ch[p][c]) ch[p][c]=np,p=fa[p];; if(!p) return fa[np]=1,void();
int v=ch[p][c]; if(len[v]==len[p]+1) return fa[np]=v,void();
int nv=++tot; memcpy(ch[nv],ch[v],sizeof(ch[nv])); fa[nv]=fa[v]; posr[nv]=posr[v]; len[nv]=len[p]+1; fa[v]=fa[np]=nv;
while(p&&ch[p][c]==v) ch[p][c]=nv,p=fa[p];
}
inline void build(int oo=0){
For(u,2,tot) _ffa[0][u]=fa[u];; For(i,1,__lg(n)) For(u,2,tot) _ffa[i][u]=_ffa[i-1][_ffa[i-1][u]];
For(u,1,tot) For(i,0,__lg(n)) ffa[u][i]=_ffa[i][u];
For(u,2,tot) son[fa[u]].push_back(u);;
function<void(int)> dfs; dfs = [&](int u) { for(int v:son[u]) dfs(v),posr[u]=max(posr[u],posr[v]); }; if(oo) dfs(1);
For(u,2,tot) posl[u]=posr[u]-len[u]+1;
For(u,1,tot) tcnt[posl[u]]++;; For(i,1,n) tcnt[i]+=tcnt[i-1];; For(u,1,tot) tmp[tcnt[posl[u]]--]=u;; if(oo) reverse(tmp+1,tmp+1+tot);
For(i,1,n) tcnt[i]=0;; For(i,1,tot) tcnt[len[tmp[i]]]++;; For(i,1,n) tcnt[i]+=tcnt[i-1];; Rev(i,tot,1) lis[tcnt[len[tmp[i]]]--]=tmp[i];
Rev(i,tot,1) { int u=lis[i]; for(int v:son[u]) occ[u]+=occ[v]; }
}
inline int find(int l,int r) const { int u=pre[r]; Rev(i,__lg(n),0) if(len[ffa[u][i]]>=r-l+1) u=ffa[u][i];; return u; }
};
template<int NN,int Z>
class BasicStringStructure{
public:
static constexpr int N=NN*2; struct Node { int u,r; }; using pii = pair<int,int>; struct Stair { int posl,posr; vector<Node> X,Y; } st[N];
int n,stcnt,num0[N],num1[N]; SAM<NN,Z> S0,S1;
BasicStringStructure<NN,Z>() : S0(0), S1(1) {}
inline void build(int _n,int* s){
n=_n; For(i,1,n) S0.push_back(s[i]);; Rev(i,n,1) S1.push_back(s[i]);; fprintf(stderr,"T1:%ld\n",clock()); S0.build(0); S1.build(1);
stcnt=0; Rev(i,S0.tot,2){
int u=S0.lis[i],l=S0.posl[u],r=S0.posr[u],l2=l+S0.len[u]-S0.len[S0.fa[u]]-1,v=0; For(c,0,Z-1) if(S0.ch[u][c]&&S0.occ[S0.ch[u][c]]==S0.occ[u]) { v=S0.ch[u][c]; break; }
if(!v) { num0[u]=++stcnt; st[stcnt].posl=l; st[stcnt].posr=r; st[stcnt].X.push_back(Node{u,l2}); }
else { st[num0[u]=num0[v]].X.push_back(Node{u,l2}); }
}
stcnt=0; Rev(i,S1.tot,2){
int u=S1.lis[i],l=n-S1.posr[u]+1,r=n-S1.posl[u]+1,r2=r-(S1.len[u]-S1.len[S1.fa[u]])+1,v=0; For(c,0,Z-1) if(S1.ch[u][c]&&S1.occ[S1.ch[u][c]]==S1.occ[u]) { v=S1.ch[u][c]; break; }
if(!v) { num1[u]=++stcnt; assume(st[stcnt].posl==l&&st[stcnt].posr==r); st[stcnt].Y.push_back(Node{u,r2}); }
else { st[num1[u]=num1[v]].Y.push_back(Node{u,r2}); }
}
// For(i,1,stcnt){
// printf("Stair #%d (%d,%d): X = ",i,st[i].posl,st[i].posr);
// for(auto x:st[i].X) printf("%d[%d,%d] ",x.u,x.l,x.r);
// printf(" Y = "); for(auto y:st[i].Y) printf("%d[%d,%d] ",y.u,y.l,y.r);; puts("");
// }
}
};