首页 > 其他分享 >后缀自动机 (SAM) 学习笔记

后缀自动机 (SAM) 学习笔记

时间:2025-01-15 21:21:48浏览次数:1  
标签:SAM sam 后缀 len int endpos 自动机 operatorname

\(\text{后缀自动机 (SAM) 学习笔记}\)

一、定义

字符串 \(s\) 的 SAM 是一个接受 \(s\) 的所有后缀的最小 DFA (确定性有限自动机或确定性有限状态自动机),也就是说:

  • SAM 是一张有向无环图。它的结点是图中的状态,边是状态之间的转移。
  • SAM 有源点 \(t_0\),且其它各结点均可从 \(t_0\) 出发到达。
  • SAM 中每个转移都标有一个字母,且从一个结点出发的所有转移都是不同的。
  • SAM 存在一些终止状态。特殊地,到达一个终止状态时从 \(t_0\) 到该状态的路径连接起来是字符串 \(s\) 的后缀。反之,\(s\) 的每个后缀同样可从一条由 \(t_0\) 到某个终止状态的路径构成。
  • 满足这样条件的自动机有多个,而 SAM 的结点数是最少的。

让我们举一个例子来描述一个对于字符串 \(\texttt{abcbc}\) 的 SAM:picture1

需要注意的是,SAM 的结点个数和边数都是 \(O(n)\) 的。具体地,一个 SAM 最多会有 \(2\times n-1\) 个结点和 \(3\times n-4\) 条转移边。

二、\(\operatorname{endpos}\) 等价类及其性质

这一部分的内容,似乎和 SAM 没有直接关系,但却是 SAM 中很重要的一部分。我们需要证明关于它的一些性质,并得出一些结论,这是我们构建 SAM 的基础。

1. 定义

对于字符串 \(s\) 的一个子串,它在原串中会出现若干次。一个子串 \(p\) 在 \(s\) 中出现的右端点位置的集合,就称为 \(\operatorname{endpos}(p)\)。对于上文中串 \(\texttt{abcbc}\),对于字符串 \(\texttt{bc}\),其 \(\operatorname{endpos}\) 集合为 \(\{2,4\}\)。需要说明的是,每一个 \(\operatorname{endpos}\) 等价类对应着 SAM 上的一个结点。根据定义我们显然可以这样做,这样 SAM 中一个状态就会对应这个 \(\operatorname{endpos}\) 等价类中所有的字符串。

2. 性质及其证明

  1. 若串 \(s_1,s_2\) 满足 \(\operatorname{endpos}(s_1)=\operatorname{endpos}(s_2)\),且 \(s_1\neq s_2\),则 \(\operatorname{len}(s_1)\neq \operatorname{len}(s_2)\)。

证明:若存在 \(s_1,s_2\) 满足 \(\operatorname{endpos}(s_1)=\operatorname{endpos}(s_2)=R,s_1\neq s_2,\operatorname{len}(s_1)=\operatorname{len}(s_2)=l\),则对于任意 \(pos\subset R\),由 \(s_1\neq s_2\) 有 \(s[pos-l+1,pos]\neq s[pos-l+1,pos]\),显然矛盾。

  1. 对于 \(s\) 的任意后缀 \(t_s\),有 \(\operatorname{endpos}(s)\subset \operatorname{endpos}(t_s)\)。

证明:由 \(\operatorname{endpos}\) 的定义知道每个 \(s\) 能匹配到的右端点 \(t_s\) 必能匹配。

  1. 若两个不同的串 \(s_1,s_2\) 满足 \(\operatorname{endpos}(s_1)=\operatorname{endpos}(s_2)=R\),则对于 \(\operatorname{len}(s_1)\le l \le \operatorname{len}(s_2)\),一定存在 \(s_3\) 满足 \(\operatorname{len}(s_3)=l\) 且 \(\operatorname{endpos}(s_3)=R\)。

证明:由 1,\(\operatorname{len}(s_1)\neq \operatorname{len}(s_2)\),不妨设 \(\operatorname{len}(s_1)<\operatorname{len}(s_2)\)。令 \(s_3=s_2[l_2-l+1,l_2]\),由 2,\(R\subset \operatorname{endpos}(s_3),\operatorname{endpos}(s_3)\subset R\)。因此 \(\operatorname{endpos}(s_3)=R\)。

  1. \(\operatorname{endpos}\) 集合相等的字符串的长度必然是连续的。

证明:设其中两个不同串为 \(s_1,s_2\),由 1,可不妨设 \(\operatorname{len}(s_1)<\operatorname{len}(s_2)\)。由 3,必然有 \(\operatorname{len}(s_1)\le l \le \operatorname{len}(s_2)\) 满足 \(\operatorname{endpos}=R\)。

  1. 对于两个 \(\operatorname{endpos}\) 集合 \(R_a,R_b\),要么 \(R_a\subseteq R_b\),要么 \(R_a\cap R_b=\varnothing\)。

证明:设 \(R_a\cap R_b=r\neq \varnothing\),那么设从 \(t_0\) 到 \(R_a,R_b\) 结点路径表示的字符串的集合为 \(S_a,S_b\)。记 \(\max_s,\) 表示集合 \(S_a\) 中最长的串的长度,\(\min_a\) 同理,则由 4,\([\min_a,\max_a]\cap [\min_b,\max_b]=\varnothing\)。不妨设 \(\max_b<\min_a\),则对于任意 \(s_a\in S_a\),有 \(\operatorname{len}(a)>\operatorname{len}(b)\)。又因为 \(R_a\cap R_b=r\neq \varnothing\),由 2,\(R_a\subset R_b\)。考虑 \(R_a=R_b\) 的情形,则 \(R_a\subseteq R_b\)。

三、 parent 树及 SAM 的复杂度

根据上面的性质,任意两个 \(\operatorname{endpos}\) 集合或是不相交,或是其中一个是另一个的子集。那么对于任意一个不为初始状态的状态 \(a\),一定恰好存在一个状态 \(b\) 满足 \(\operatorname{endpos}(a)\subseteq \operatorname{endpos}(b)\) 且 \(\max_b=\max_a-1\)。这种关系可以抽象成一个树形结构,记非根状态 \(x\) 在 parent 树上的父亲为 \(\operatorname{link}(x)\)。那么容易发现的性质是一个结点在 parent 树上的子结点至少有两个,否则其 \(\operatorname{endpos}\) 集合应当相同。且子结点代表的 \(\operatorname{endpos}\) 集合互不相交。那么仍然以串 \(\texttt{abcbc}\) 举例,我们可以用绿色的线表示 parent 树上的边。

image

让我们进一步发现 parent 树上的一些奇妙性质:

  1. parent 树有 \(\operatorname{len}(s)\) 个儿子。

证明:显然会存在的叶子结点 \(\operatorname{endpos}\) 集合为 \(\{1\},\{2\},\cdots,\{\operatorname{len}(s)\}\)。

  1. parent 树的状态不会超过 \(O(n)\) 级别。

证明:由于一个点至少有两个子结点,那么新增一个结点必然会删去两个结点,因此最多新增 \(n-1\) 个非叶子结点。于是总的状态级别是 \(O(n)\)。

那么我们已经证明了 SAM 状态数是 \(O(n)\) 的。需要知道的是 SAM 的转移边数同样是 \(O(n)\) 的,不过这个性质没有状态数那么重要,且较难证明,因此略去。

四、SAM 的构造

1. 构造流程

初始情况是只有状态 \(t_0=1\),其 \(\operatorname{len}=0,\operatorname{link}=0\),现在将字符 \(c\) 加入 SAM 中,加入之前 SAM 的最终状态为 \(p\)。

我们创建新的状态 \(np\),令 \(\operatorname{len}(np)=\operatorname{len}(p)+1\)。从 \(p\) 开始跳 \(\operatorname{link}\),若没有 \(c\) 的转移,添加到 \(np\) 为字符 \(c\) 的转移,直到找到一个有该转移的状态,改这个状态为 \(p\)。若没有找到 \(p\),那么其 \(\operatorname{endpos}\) 是一个全新的 \(\operatorname{endpos}\),直接令 \(\operatorname{link}(np)=1\) 即可,否则我们记 \(p\) 关于 \(c\) 的转移为 \(q\)。

若 \(\operatorname{len}(q)=\operatorname{len}(p)+1\),那么显然令 \(\operatorname{link}(np)=q\) 是正确的。考虑 \(\operatorname{len}(q)\neq \operatorname{len}(p)+1\) 的情形:我们将状态 \(q\) 复制至 \(nq\),但将 \(\operatorname{len}(nq)\) 置为 \(\operatorname{len}(p)+1\),并将 \(q,np\) 的 \(\operatorname{link}\) 信息指向 \(nq\)。

最后,我们从 \(p\) 开始跳 \(\operatorname{link}\),若 \(p\) 有 \(c\) 的转移且转移到了 \(q\),将这个转移改到 \(nq\) 即可,直到找不到或是回到源点停止。

需要知道的是,建完这个 SAM 后对应的终止状态就是 \(np\)。

2. 正确性证明

需要证明的部分只有 \(\operatorname{len}(q)\neq \operatorname{len}(p)+1\) 的部分。此时显然 \(\operatorname{len}(q)>\operatorname{len}(p)+1\),也就是状态 \(q\) 在对应长度为 \(\operatorname{len}(p)+1\) 后缀的同时也对应了更长的子串。于是将状态 \(q\) 拆一个状态 \(nq\) 出来,且将其 \(\operatorname{len}\) 设为 \(\operatorname{len}(p)+1\)。这样一来,\(nq\) 继承 \(q\) 的其它信息是理所当然的。同时需要留意的是,要将状态 \(p\) 原有到 \(q\) 的转移改到 \(nq\),于是跳 \(\operatorname{link}\) 的后缀直到找不到转移 \((c,q)\) 为止。

需要知晓的是,这样构建 SAM 的时间复杂度是 \(O(n)\)。这是建立在字符集大小为常数的前提下。否则一般使用 std::map 来存边,此时时间复杂度为 \(O(n\log |\sum|)\) 而空间复杂度为 \(O(n)\)。

这里给出 SAM 的一般实现:

struct SAM {
	int len, fa;
	int s[M];
} sam[N];
int tot = 1, lst = 1;
void insert(int c) {
	int p = lst, np = lst = ++tot;
	sam[np].len = sam[p].len + 1;
	for (; p && !sam[p].s[c]; p = sam[p].fa) sam[p].s[c] = np;
	if (!p) sam[np].fa = 1;
	else {
		int q = sam[p].s[c];
		if (sam[q].len == sam[p].len + 1) sam[np].fa = q;
		else {
			int nq = ++tot;
			sam[nq] = sam[q], sam[nq].len = sam[p].len + 1;
			sam[q].fa = sam[np].fa = nq;
			for (; p && sam[p].s[c] == q; p = sam[p].fa) sam[p].s[c] = nq;
		}
	}
}

五、SAM 的基础应用

1. 求本质不同子串个数

一般的方法是求每个状态内子串的个数。也就是 \(\sum \operatorname{len}(i)-\operatorname{link}(\operatorname{len}(i))\)。

2. 求第 \(k\) 小子串

考虑到每个子串唯一对应着 SAM 上一条路径,于是转化为求 SAM 上字典序第 \(k\) 小的路径。于是简单 dp 可以处理。

3.求两个字符串的最长公共子串

对于两个字符串 \(s,t\),对于 \(s\) 建出后缀自动机,对 \(t\) 进行匹配处理。我们使用两个变量进行匹配:当前状态 \(p\) 和当前长度 \(l\)。初始时 \(p=t_0,l=0\)。

当 \(p\) 存在字符 \(t_i\) 的转移时,我们转移长度并让 \(l\) 加一即可。若不存在 \(p\) 的转移,需要将 \(p\) 跳 \(\operatorname{link}\) 数组知道满足当前字符的转移。对于时间复杂度,显然每次最多使 \(l\) 加一,或是将 \(l\) 减小一些,调整加减顺序不难得到总的时间复杂度为 \(O(|s|+|t|)\)。

给出代码实现:

int fnd(char *s) {
	int p = 1, ans = 0, l = strlen(s), res = 0;
	for (int i = 0; i < l; i++) {
		int c = s[i] - '0';
		while (p > 1 && !sam[p].s[c]) {
			p = sam[p].fa;
			ans = sam[p].len;
		}
		if (sam[p].s[c]) {
			p = sam[p].s[c];
			++ans;
		}
        res = max(res, ans);
	}
    return res;
}

4. 线段树合并维护 \(\operatorname{endpos}\) 集合

考虑在 parent 树上对 \(\operatorname{endpos}\) 集合进行线段树合并,这样通常可以维护出每个点 \(\operatorname{endpos}\) 的一些信息,在一些题目中会用到。

5. SAM 求后缀的最长公共前缀

考虑将字符串反向后插入 SAM 中,这样问题转化为了前缀的最长公共后缀,那么 parent 树上所有父亲串一定是儿子串的前缀。于是找到两个字符串对应的结点,求它们的 LCA 即可。

六、广义 SAM

广义 SAM,就是对多个字符串建出的 SAM。如果暴力将它们连接,往往会出现各种各样的问题,因此需要掌握建立正确的广义 SAM 的方法。

我们先针对这些字符串建出 Trie,在此基础上将 Trie 的每条边建到广义 SAM 中即可。我们通常适用的方法是离线 BFS。下面给出代码实现:

struct Trie {
	int fa, ch;
	int s[M];
} tr[N];
int cnt = 1;
void ins(char *s) {
	int l = strlen(s), p = 1;
	for (int i = 0; i < l; i++) {
		int ch = s[i] - '0';
		if (!tr[p].s[ch]) {
			tr[p].s[ch] = ++cnt;
			tr[cnt].fa = p;
			tr[cnt].ch = ch;
		}
		p = tr[p].s[ch];
	}
}
struct SAM {
	int len, fa;
	int s[M];
} sam[N];
int tot = 1;
int insert(int c, int lst) {
	int p = lst, np = lst = ++tot;
	sam[np].len = sam[p].len + 1;
	for (; p && !sam[p].s[c]; p = sam[p].fa) sam[p].s[c] = np;
	if (!p) sam[np].fa = 1;
	else {
		int q = sam[p].s[c];
		if (sam[q].len == sam[p].len + 1) sam[np].fa = q;
		else {
			int nq = ++tot;
			sam[nq] = sam[q], sam[nq].len = sam[p].len + 1;
			sam[q].fa = sam[np].fa = nq;
			for (; p && sam[p].s[c] == q; p = sam[p].fa) sam[p].s[c] = nq;
		}
	}
	return lst;
}

queue<int>q;
int pos[N];
void build() {
	for (int i = 0; i < M; i++)
		if (tr[1].s[i]) q.push(tr[1].s[i]);
	pos[1] = 1;
	while (!q.empty()) {
		int p = q.front();
		q.pop();
		pos[p] = insert(tr[p].ch, pos[tr[p].fa]);
		for (int i = 0; i < M; i++)
			if (tr[p].s[i]) q.push(tr[p].s[i]);
	}
}

标签:SAM,sam,后缀,len,int,endpos,自动机,operatorname
From: https://www.cnblogs.com/Rock-N-Roll/p/18673731

相关文章

  • not_the_same_3dsctf_2016 1
    打开ida能看到栈溢出,返回地址填到get_secret函数里面,可以看到get_secret函数是直接读取了flag的,现在就需要把它输出即可。输出我们可以利用代码里面的printf,因为printf从缓冲区打印出东西需要满足条件,比如有换行符或缓冲区已满或程序正常退出。这里我们用exit让程序正常退出,s......
  • 狂揽多篇一区!速度狂彪200%-卡尔曼滤波+SAM
    AI科研灵感致力于成为您在人工智能领域的领航者,定期更新人工智能领域的重大新闻与最新动态,和您一起探索AI的无限可能。立即关注我们,开启您的AI学习之旅!2025深度学习发论文&模型涨点之——卡尔曼滤波+SAM卡尔曼滤波(KalmanFilter)与SAM(SegmentAnythingModel)结合,构成了一种高......
  • R语言caret包的resamples函数比较在同一数据集上多个机器学习模型的比较结果实战、sum
    R语言caret包的resamples函数比较在同一数据集上多个机器学习模型的比较结果实战、使用summary函数比较模型的汇总信息、使用lattice包的bwplot函数使用箱图对比多个模型在多个指标上的性能差异目录R语言使用caret包的resamples函数比较在同一数据集上多个机器学习模型的比......
  • Hetao P3804 Cut 题解 [ 蓝 ] [ AC 自动机 ] [ 差分 ]
    Cut:AC自动机简单题。思路看见多个模式串以及求前缀,很显然能想到构建一个AC自动机。那么在用\(T\)查询时,当前指针的深度就是该位置的最长前缀匹配长度。这个在字典树insert的时候就能求出来。求出每一位的最长前缀后,因为这些部分都不能作为分割点,所以将这些区域用差分......
  • SamOut v3 发布-感叹转义词表能力太强【用em(voc_size=8000多,h)表达2000w 词汇 竟然
    项目地址说明v3主要更换了sky-pile数据集v3使用了转义词表技术,使得8000多的emsize能够表达2000多w的词表v3由于词表是使用jieaba分词,自然在相同token_id数量的情况下信息量更多(更多的字符)v3解码速度保持不变,同样训练消耗算力不变v3幻觉不变v3解码消耗显存......
  • LIO-SAM代码解析:mapOptmization.cpp(二)
    文章目录1.cornerOptimization1.点集中心计算2.协方差矩阵计算3.特征值分解4.主方向选择5.距离与权重计算6.优化目标2.surfOptimization1.平面方程拟合2.平面方程归一化3.点到平面的距离4.权重因子计算5.优化系数1.cornerOptimizationvoidcornerOp......
  • Ubuntu20.04下修改samba用户密码
    引言Samba是一个用于Linux和Windows系统之间文件和打印共享的强大工具。在Ubuntu20.04上,管理Samba用户和密码是系统管理员的常见任务之一。本文将详细介绍在Ubuntu20.04上如何修改Samba用户密码。安装和配置Samba在修改Samba用户密码之前,确保已经安装并配置......
  • LIO-SAM代码解析:mapOptmization.cpp(一)
    文章目录主流程1.`loopInfoHandler`1.1`updateInitialGuess`1.2`extractSurroundingKeyFrames`1.3`downsampleCurrentScan`1.4`scan2MapOptimization`1.5`saveKeyFramesAndFactor`1.6`correctPoses`1.7`publishOdometry`1.8`publishFrames`主流程1.loo......
  • 【学习笔记】AC自动机
    期末考试前学了下这个东西,感觉很简单,不像某mp。然而期末Day1考完就忘了,所以还是写篇笔记吧。前置知识:字典树先来看一下洛谷上的AC自动机模版题。P5357【模板】AC自动机给你一个文本串\(S\)和\(n\)个模式串\(T_{1\simn}\),请你分别求出每个模式串\(T_i\)在\(......
  • 如何用python去保存文件后缀名
    用python保存文件后缀名的方法:1、splittext()方法2、endswith()方法path = "test_user_info.py"bool = path.endswith(".py")print(bool)3.用split方法切割path = "test_user_info.py"suffix = path.split(".")[1]print("suffix: {}&......