【学习笔记】简单字符串算法 —— 序列自动机

  • 序列自动机是一个比后缀自动机简单的自动机。后缀自动机可以看神仙 x y z 32768 xyz32768 xyz32768 的学习笔记:[学习笔记]省选数据结构·SAM
  • 这个自动机同样是一个有限状态确定自动机,能够识别一个串的子序列。
  • 即某个子序列对应到自动机上根到某个结点的一条路径。
  • 自动机一共 n + 1 n+1 n+1 个结点,每个位置开一个结点,加上一个根结点。
  • 每个结点的符号为 c c c 的出边指向序列中该结点对应位置后面第一个符号为 c c c 的结点。
  • 构造可以实现 O ( n × C ) O(n\times C) O(n×C) 的时间复杂度,其中 C C C 是字符集的大小。离线构造根据定义构造即可。
  • 考虑如果要在线构造,应当怎么构造。
  • 显然我们可以知道一个结点的 p a r e n t parent parent 指针指向前面最后一个和它符号一样的结点,因此我们记一个数组 l s t [ c ] lst[c] lst[c] 表示当前最后一个字符 c c c 的位置的对应结点。
  • 插入的时候就把 l s t lst lst 数组遍历一遍,然后不断跳 p a r e n t parent parent 来更新转移边即可。
  • 具体见代码,这里就不细说了,挺简单的。
struct node
{
	int par; 
	int trans[base]; 
};  
struct seq_AM
{
	int tot, lst[base]; 
	node tr[MaxN]; 
	inline void init()
	{
		tot = 1; 
		for (int i = 0; i < base; ++i) lst[i] = 1; 
	}
	inline void extend(int c)
	{
		tr[++tot].par = lst[c]; 
		for (int i = 0; i < base; ++i)
		{
			for (int x = lst[i]; x && !tr[x].trans[c]; x = tr[x].par)
				tr[x].trans[c] = tot; 
		}
		lst[c] = tot; 
	}
}; 

例题 1:[FJOI2016]所有公共子序列问题

题目大意:求两个只含大小写字母的字符串的本质不同的公共子序列个数,题目还要求在个数少的时候,输出所有公共子序列。长度都在 3010 3010 3010 内。

  • 我们对两个串分别建出序列自动机,然后同时在两个自动机上暴力 d p dp dp 即可。
  • f [ x ] [ y ] f[x][y] f[x][y] 表示在 A A A 串走到结点 x x x,在 B B B 串走到结点 y y y,往下走一共有多少种子序列方案。直接暴力记忆化搜索即可。
  • 输出方案直接暴力遍历即可。
  • 注:此题需要高精,但是最坏情况高精的答案是存不下的,但是数据水,所以高精不需要开很大。
// luogu-judger-enable-o2
#include <bits/stdc++.h>

template <class T>
inline void putint(T x)
{
	if (x < 0) x = -x, putchar('-'); 
	static char buf[50]; 
	static char *tail = buf; 
	for (; x; x /= 10) *++tail = x % 10 + '0'; 
	for (; tail != buf; --tail) putchar(*tail); 
}

const int base = 60; 
const int mod = 1e9; 
const int MaxN = 3e3 + 50; 

struct node
{
	int par; 
	int trans[base]; 
}; 

struct seq_am
{
	int tot, lst[base]; 
	node tr[MaxN]; 
	
	inline void init()
	{
		tot = 1; 
		for (int i = 0; i < base; ++i)
			lst[i] = 1; 
	}
	inline void extend(int c)
	{
		tr[++tot].par = lst[c]; 
		for (int i = 0; i < base; ++i)
		{
			for (int u = lst[i]; u && !tr[u].trans[c]; u = tr[u].par)
				tr[u].trans[c] = tot; 
		}
		lst[c] = tot; 
	}
}A, B; 

struct bigint
{
	int len, *a; 
	
	bigint(){}
	bigint(int t)
	{
		a = new int[21]; 
		for (int i = 1; i <= 20; ++i) a[i] = 0; 
		a[len = 1] = t; 
	}
	
	inline void operator += (bigint rhs)
	{
		len = std::max(len, rhs.len); 
		for (int i = 1; i <= len; ++i)
			a[i] += rhs.a[i]; 
		for (int i = 1; i <= len; ++i)
		{
			a[i + 1] += a[i] / mod; 
			a[i] %= mod; 
		}
		if (a[len + 1]) ++len; 
	}
	inline void print()
	{
		printf("%lld", a[len]); 
		for (int i = len - 1; i >= 1; --i)
			printf("%09lld", a[i]); 
		putchar('\n'); 
	}
}f[MaxN][MaxN]; 

int n, m, opt, lst[base]; 
int pos[200], idx[base]; 

int top; 
char s[MaxN], t[MaxN], stk[MaxN]; 

bool vis[MaxN][MaxN]; 

inline void print(int x, int y)
{
	if (!x || !y) return; 
	
	for (int i = 1; i <= top; ++i)
		putchar(stk[i]); 
	putchar('\n'); 
	
	for (int i = 0; i < base; ++i)
	{
		stk[++top] = idx[i]; 
		print(A.tr[x].trans[i], B.tr[y].trans[i]); 
		--top; 
	}
}

inline bigint dfs(int x, int y)
{
	if (vis[x][y]) return f[x][y]; 
	vis[x][y] = true; 
	bigint res(1); 
	for (int i = 0; i < base; ++i)
		if (A.tr[x].trans[i] && B.tr[y].trans[i])
			res += dfs(A.tr[x].trans[i], B.tr[y].trans[i]); 
	return f[x][y] = res; 
}

int main()
{
	for (int i = 'A'; i <= 'Z'; ++i) idx[pos[i] = i - 'A'] = i; 
	for (int i = 'a'; i <= 'z'; ++i) idx[pos[i] = i - 'a' + 26] = i; 
	A.init(), B.init(); 
	
	scanf("%d%d%s%s%d", &n, &m, s + 1, t + 1, &opt); 
	
	for (int i = 1; i <= n; ++i) A.extend(pos[s[i]]); 
	for (int i = 1; i <= m; ++i) B.extend(pos[t[i]]); 
	
	if (opt) print(1, 1); 
	dfs(1, 1).print(); 
	
	return 0; 
}-

例题 2:[HEOI2015]最短不公共子串

题目大意:给两个小写字母串 A A A B B B,字符串长度均在 2000 2000 2000 范围内。请你计算:

  1. A A A 的一个最短的子串,它不是 B B B 的子串
  2. A A A 的一个最短的子串,它不是 B B B 的子序列
  3. A A A 的一个最短的子序列,它不是 B B B 的子串
  4. A A A 的一个最短的子序列,它不是 B B B 的子序列
  • 我们知道问题 1 1 1 是一个简单 S A M SAM SAM 问题。
  • 问题二、三、四都涉及到子序列的问题。
  • 我们考虑如果我们能够构建出识别所有子序列的有限状态确定自动机,问题就可以迎刃而解。
  • 考虑在某个要求下,我们对 A A A B B B 分别构建出了满足条件的自动机,那么我们只要在两个自动机上同时 b f s bfs bfs。遇到走到某一步 A A A 串能匹配, B B B 串失配直接返回深度即可。
  • 而在没有学习序列自动机之前想出如何构建这个自动机是有点困难的,学习了之后我们就知道,对于每个串构建 S A M SAM SAM 和序列自动机即可。
#include <bits/stdc++.h>

const int base = 26; 
const int MaxN = 2e3 + 5; 
const int MaxS = MaxN << 1; 

struct node
{
	int maxl, par; 
	int trans[base]; 
};  

struct SAM
{
	int tot, lst, nowlen; 
	node tr[MaxS]; 
	inline void init()
	{
		tot = lst = 1; 
	}
	inline void extend(int c)
	{
		int x = lst, q; 
		
		++nowlen; 
		tr[lst = ++tot].maxl = nowlen; 
		for (; x && !tr[x].trans[c]; x = tr[x].par)
			tr[x].trans[c] = lst; 
		if (!x) tr[lst].par = 1; 
		else
		{
			int y = tr[x].trans[c]; 
			if (tr[x].maxl + 1 == tr[y].maxl)
				tr[lst].par = y; 
			else
			{
				tr[q = ++tot] = tr[y]; 
				tr[q].maxl = tr[x].maxl + 1; 
				tr[y].par = tr[lst].par = q; 
				for (; x && tr[x].trans[c] == y; x = tr[x].par)
					tr[x].trans[c] = q; 
			}
		}
	}
}sam_a, sam_b; 

struct seq_AM
{
	int tot, lst[base]; 
	node tr[MaxN]; 
	inline void init()
	{
		tot = 1; 
		for (int i = 0; i < base; ++i) lst[i] = 1; 
	}
	inline void extend(int c)
	{
		tr[++tot].par = lst[c]; 
		for (int i = 0; i < base; ++i)
		{
			for (int x = lst[i]; x && !tr[x].trans[c]; x = tr[x].par)
				tr[x].trans[c] = tot; 
		}
		lst[c] = tot; 
	}
}seq_a, seq_b; 

int n1, n2; 
char s1[MaxN], s2[MaxN]; 

template <class P, class Q>
inline int solve(P &A, Q &B)
{
	static int que[MaxS * MaxS][3], qr; 
	static bool vis[MaxS][MaxS]; 
	memset(vis, 0, sizeof(vis)); 
	
	que[qr = 1][0] = 0; 
	que[1][1] = que[1][2] = 1; 
	vis[1][1] = true; 
	
	for (int ql = 1; ql <= qr; ++ql)
	{
		int ux = que[ql][1], uy = que[ql][2], ud = que[ql][0]; 
		for (int i = 0; i < base; ++i)
			if (A.tr[ux].trans[i] || B.tr[uy].trans[i])
			{
				int vx = A.tr[ux].trans[i], vy = B.tr[uy].trans[i]; 
				if (!vy) return ud + 1; 
				if (!vx || vis[vx][vy]) continue; 
				
				que[++qr][0] = ud + 1; 
				que[qr][1] = vx; 
				que[qr][2] = vy; 
				vis[vx][vy] = true; 
			}
	}
	return -1; 
}

int main()
{
	sam_a.init(), sam_b.init(); 
	seq_a.init(), seq_b.init(); 
	
	scanf("%s%s", s1 + 1, s2 + 1); 
	n1 = strlen(s1 + 1), n2 = strlen(s2 + 1); 
	for (int i = 1; i <= n1; ++i) sam_a.extend(s1[i] - 'a'), seq_a.extend(s1[i] - 'a'); 
	for (int i = 1; i <= n2; ++i) sam_b.extend(s2[i] - 'a'), seq_b.extend(s2[i] - 'a'); 
	
	printf("%d\n%d\n%d\n%d\n", solve(sam_a, sam_b), solve(sam_a, seq_b), 
	                           solve(seq_a, sam_b), solve(seq_a, seq_b)); 
	
	return 0; 
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值