广义后缀自动机 练习题从入门到精通

多串模式下的后缀自动机被称为广义后缀自动机(广义sam)

关于广义后缀自动机的建立代码江湖上流传着三种版本

版本一: trie树上建sam

考虑将所有串建成trie树,在trie树上建sam,在插入当前字符的时候last就是它的父亲节点对应的sam节点编号,具体代码如下

void ins(char *ss)
{
	int p=1; // trie树的根节点为1
	for(int i=1; i<=lenlen; i++)
	{
		int c=ss[i]-'a';
		if(!trie[p][c])
		{
			trie[p][c]=++cnt_2;
			fa[cnt_2]=p,num[cnt_2]=c; // 记录该点的父节点 该点由什么边连接而来
		}
		p=trie[p][c];
	}
}

int add(int c, int last) // 普通的单串sam
{
	int p,cur=++sam_cnt;
	len[cur]=len[last]+1;
	for(p=last; p && !sam[p][c]; p=link[p])
		sam[p][c]=cur;
	if(!p)
		link[cur]=1;
	else
	{
		int q=sam[p][c];
		if(len[q]==len[p]+1)
			link[cur]=q;
		else
		{
			int cl=++sam_cnt;
			len[cl]=len[p]+1;
			memcpy(sam[cl], sam[q], sizeof(sam[q]));
			link[cl]=link[q];
			while(p && sam[p][c]==q)
			{
				sam[p][c]=cl;
				p=link[p];
			}
			link[cur]=link[q]=cl;
		}
	}
	return cur; // 返回last
}

void bulid()
{
	queue<int>que;
	for(int i=0; i<26; i++)
		if(trie[1][i])
			que.push(trie[1][i]);
	pos[1]=1;
	while(que.size())
	{
		int x=que.front();
		que.pop();
		pos[x]=add(num[x], pos[fa[x]]); // 每一次添加节点的last都是其在trie树上的父节点
		for(int i=0; i<26; i++)
			if(trie[x][i])
				que.push(trie[x][i]);
	}
}

这种方式常用于题目给定的串的形式是树的情况

时间复杂度为O(串长总和)

版本二:最常用的版本

void add(int c)
{
	if(sam[last][c] && len[last]+1==len[sam[last][c]]) // 已经存在完全相同的endpos
	{
		last=sam[last][c];
//		sz[last][id]=1  // 记录子串出现次数时记得加上这句
		return ;
	}
	int p,cur=++sam_cnt;
	len[cur]=len[last]+1;
	for(p=last; p && !sam[p][c]; p=link[p])
		sam[p][c]=cur;
	if(!p)
		link[cur]=1;
	else
	{
		int q=sam[p][c];
		if(len[q]==len[p]+1)
			link[cur]=q;
		else
		{
			int cl;
			if(p==last) // 原本的sam[last][c]需要分裂 即第一个if只满足前半部分 后半部分不满足
				cl=cur;
			else
			{
				cl=++sam_cnt;
				link[cur]=cl;
			}
			len[cl]=len[p]+1;
			memcpy(sam[cl], sam[q], sizeof(sam[q]));
			link[cl]=link[q];
			while(p && sam[p][c]==q)
			{
				sam[p][c]=cl;
				p=link[p];
			}
			link[q]=cl;
		}
	}
	last=cur;
}

其实就是三种情况

1.当前需要构造的节点已经存在,且sam[last][c]不需要分裂

2.当前需要构造的节点已经存在,但是sam[last][c]需要分裂

3.当前需要构造的节点还未存在

对于第一种情况把sam[last][c]赋给last后直接return 即可

对于第二种情况就是将新建的节点cur作为克隆节点去使用即可

第三种情况就是直接使用普通的后缀自动机add方法

版本三:其实就是版本二的假做法,但目前很多人使用的都是这种方法,这中做法就只是每次添加新的串之前将last置为1,add方法与普通的一致。但这样做对绝大多数题目而言都不会出现问题,

只是会产生一些空节点,也就是这个节点存在,但它不具备任何意义,详细可以参考

https://www.bilibili.com/video/BV1J64y1c7Hu/?spm_id_from=333.337.search-card.all.click&vd_source=dab21cbce26dba72a4bfa052ed90b2de

练习题:

P4081 [USACO17DEC]Standing Out from the Herd P - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

求只在本串中出现的本质不同子串数

每次都给新添加的sam节点打上当前串编号的标记,如果有多个标记了就直接标为-1

#include <iostream> /// 洛谷 4081 只在本串中出现的本质不同子串数 
#include <cstring> // 建sam访问节点后 按子串长度基数排序一下去除某个字符串出现多次但其后缀只标记出现一次的情况 
using namespace std;

const int N=200005; // 总串长*2
int trie[N][26],link[N],len[N],cnt,last,lenlen,a[N],c[N];
char s[N];
int vis[N],ans[N];

void init()
{
	last=cnt=1;
}

void add(int c)
{
	if(trie[last][c] && len[last]+1==len[trie[last][c]]) // 已经存在完全相同的endpos
	{
		last=trie[last][c];
		return ;
	}
	int p,cur=++cnt;
	len[cur]=len[last]+1;
	for(p=last; p && !trie[p][c]; p=link[p])
		trie[p][c]=cur;
	if(!p)
		link[cur]=1;
	else
	{
		int q=trie[p][c];
		if(len[q]==len[p]+1)
			link[cur]=q;
		else
		{
			int cl;
			if(p==last) // 原本的trie[last][c]需要分裂 即第一个if只满足前半部分 后半部分不满足
				cl=cur;
			else
			{
				cl=++cnt;
				link[cur]=cl;
			}
			len[cl]=len[p]+1;
			vis[cl]=vis[q];
			memcpy(trie[cl], trie[q], sizeof(trie[q]));
			link[cl]=link[q];
			while(p && trie[p][c]==q)
			{
				trie[p][c]=cl;
				p=link[p];
			}
			link[q]=cl;
		}
	}
	last=cur;
}

void calc()
{
	for(int i=1; i<=cnt; i++)
		c[len[i]]++;
	for(int i=1; i<=cnt; i++)
		c[i]+=c[i-1];
	for(int i=1; i<=cnt; i++)
		a[c[len[i]]--]=i;
	for(int i=cnt; i>=1; i--)
		if(vis[link[a[i]]]!=vis[a[i]]) // 1.在分裂节点时 vis[cl]=vis[q]导致vis[cl]可能与vis[cur]不同 
			vis[link[a[i]]]=-1;        // 2.节点a[i]被标记多次访问了 但是其后缀串(link)并没有被更新 
}

int main()
{
	int n;
	scanf("%d", &n);
	init();
	for(int i=1; i<=n; i++)
	{
		scanf("%s", s+1);
		lenlen=strlen(s+1);
		last=1; // 每次都要把last置为1 即回到根节点
		for(int j=1; j<=lenlen; j++)
		{
			add(s[j]-'a');
			vis[last]=(!vis[last] ? i : -1);
		}
	}
	calc();
	for(int i=1; i<=cnt; i++)
		if(vis[i]!=-1)
			ans[vis[i]]+=len[i]-len[link[i]];
	for(int i=1; i<=n; i++)
		cout << ans[i] << '\n';
	return 0;
}

http://poj.org/problem?id=3294

北大OJ-3294 求在超过一半的字符串中出现的最长子串,要求按字典序输出所有符合条件的串

每一次更新last都按link链往上跳打标记即可

字典序输出部分:

void dfs(int x, int maxl, int now) // 当前节点 需要输出的长度 当前长度
{
	if(now==maxl)
	{
		ss[now]='\0';
		cout << ss << '\n';
		return ;
	}
	for(int i=0; i<26; i++)
	{
		if(sam[x][i] && vis_cnt[sam[x][i]]>n/2)
		{
			ss[now]='a'+i;
			dfs(sam[x][i], maxl, now+1);
		}
	}
}

P3181 [HAOI2016] 找相同字符 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

给定两个字符串,求出在两个字符串中各取出一个子串使得这两个子串相同的方案数。两个方案不同当且仅当这两个子串中有一个位置不同。

分别计算两个串的所有子串出现的次数,答案就是每个sam节点的本质不同子串数*在串1中出现的次数*在串2中出现的次数

#include <iostream> /// 洛谷 3181 两个字符串的所有公共子串的对数 广义SAM
#include <cstring>  // 分别求出两个串的所有子串在本串中的出现次数
using namespace std;

const int N=800005; // 总串长*2
int sam[N][26],link[N],len[N],cnt,last,lenlen;
int size[N][2],a[N],c[N];
char s[N];

void init()
{
	last=cnt=1;
}

void add(int c)
{
	if(sam[last][c] && len[last]+1==len[sam[last][c]]) // 已经存在完全相同的endpos
	{
		last=sam[last][c];
		return ;
	}
	int p,cur=++cnt;
	len[cur]=len[last]+1;
	for(p=last; p && !sam[p][c]; p=link[p])
		sam[p][c]=cur;
	if(!p)
		link[cur]=1;
	else
	{
		int q=sam[p][c];
		if(len[q]==len[p]+1)
			link[cur]=q;
		else
		{
			int cl;
			if(p==last) // 原本的sam[last][c]需要分裂 即第一个if只满足前半部分 后半部分不满足
				cl=cur;
			else
			{
				cl=++cnt;
				link[cur]=cl;
			}
			len[cl]=len[p]+1;
			memcpy(sam[cl], sam[q], sizeof(sam[q]));
			link[cl]=link[q];
			while(p && sam[p][c]==q)
			{
				sam[p][c]=cl;
				p=link[p];
			}
			link[q]=cl;
		}
	}
	last=cur;
}

void calc()
{
	for(int i=1; i<=cnt; i++)
		c[len[i]]++;
	for(int i=1; i<=cnt; i++)
		c[i]+=c[i-1];
	for(int i=1; i<=cnt; i++)
		a[c[len[i]]--]=i;
	for(int i=cnt; i>=1; i--)
		for(int j=0; j<2; j++)
			size[link[a[i]]][j]+=size[a[i]][j];
}

int main()
{
	init();
	scanf("%s", s+1);
	lenlen=strlen(s+1);
	for(int i=1; i<=lenlen; i++)
	{
		add(s[i]-'a');
		size[last][0]=1;
	}
	last=1;
	scanf("%s", s+1);
	lenlen=strlen(s+1);
	for(int i=1; i<=lenlen; i++)
	{
		add(s[i]-'a');
		size[last][1]=1;
	}
	calc();
	long long ans=0;
	for(int i=1; i<=cnt; i++)
		ans+=1ll*(len[i]-len[link[i]])*size[i][0]*size[i][1];
	cout << ans << endl;
	return 0;
}

P6793 [SNOI2020] 字符串 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

每次选择一段后缀转化为任意串 求将s串的所有长度为k的子串组成的集合转变为t串所有长度为k的子串组成的集合的最少花费

每次都贪心地找当前公共前缀最长的需要转化的子串算贡献

sam处理公共前缀问题最常用的方法就是将字符串翻转,转变为求公共后缀最长

先计算能作为最终长度为k的子串的后缀的串的出现次数,然后按串长从大到小依次枚举去匹配即可

#include <iostream> /// 洛谷 6793 每次选择一段后缀转化为任意串 求将s串的所有长度为k的子串组成的集合转变为t串所有长度为k的子串组成的集合的最少花费 O(n)
#include <cstring>   // 转化的花费为当前子串需要转化的后缀的长度
using namespace std; // 找出每个子串能同时作为两个集合中几个串的公共前缀

const int N=600005;
int sam[N][26],link[N],len[N],a[N],c[N],last,sam_cnt;
char s[N];
int cnt[N][2]; // cnt[i][0/1] --> sam节点i在0/1号串中出现多少次

void add(int c, int flag, int w)
{
	if(sam[last][c] && len[last]+1==len[sam[last][c]])
	{
		last=sam[last][c];
		cnt[last][flag]+=w;
		return ;
	}
	int p,cur=++sam_cnt;
	cnt[cur][flag]+=w;
	len[cur]=len[last]+1;
	for(p=last; p && !sam[p][c]; p=link[p])
		sam[p][c]=cur;
	if(!p)
		link[cur]=1;
	else
	{
		int q=sam[p][c];
		if(len[q]==len[p]+1)
			link[cur]=q;
		else
		{
			int cl;
			if(p==last)
				cl=cur;
			else
			{
				cl=++sam_cnt;
				link[cur]=cl;
			}
			len[cl]=len[p]+1;
			memcpy(sam[cl], sam[q], sizeof(sam[q]));
			link[cl]=link[q];
			while(p && sam[p][c]==q)
			{
				sam[p][c]=cl;
				p=link[p];
			}
			link[q]=cl;
		}
	}
	last=cur;
}

void calc()
{
	for(int i=1; i<=sam_cnt; i++)
		c[len[i]]++;
	for(int i=1; i<=sam_cnt; i++)
		c[i]+=c[i-1];
	for(int i=1; i<=sam_cnt; i++)
		a[c[len[i]]--]=i;
}

int main()
{
	int n,k;
	sam_cnt=last=1;
	scanf("%d%d", &n, &k);
	scanf("%s", s+1);
	for(int i=n; i>=1; i--) // 反串建sam 求原串的公共前缀变为求反串共后缀
		add(s[i]-'a', 1, (i+k-1<=n)); // i+k-1<=n 代表其能否作为长度为k的串的公共前缀
	last=1;
	scanf("%s", s+1);
	for(int i=n; i>=1; i--)
		add(s[i]-'a', 0, (i+k-1<=n));
	calc();
	long long ans=0;
	for(int i=sam_cnt; i>=2; i--) // 贪心从长串开始遍历 可以匹配则匹配
	{
		int x=a[i];
		int com=min(cnt[x][0], cnt[x][1]); // 作为公共前缀的数量
		ans+=1ll*com*min(k, len[x]);
		cnt[x][0]-=com,cnt[x][1]-=com; // 减去已匹配的子串对数
		cnt[link[x]][0]+=cnt[x][0],cnt[link[x]][1]+=cnt[x][1]; // 剩余权值转移到link节点
	}
	cout << 1ll*k*(n-k+1)-ans << '\n'; // 总共需要转化的串长 - 公共前缀长度
	return 0;
}

P3346 [ZJOI2015]诸神眷顾的幻想乡 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

树上的本质不同路径数

这题是求树上的本质不同路径数,此时就要用上第一种版本的建广义sam的方法

不过我们并不需要把trie树建出来,因为题目数据给的就是树,只需要遍历这个树就可以了

一个结论:树上的所有路径可以通过从所有叶子节点处的dfs获取

注意dfs时要存一下父节点对应的sam节点编号

#include <iostream> /// 洛谷 3346 树上本质不同路径数
#include <cstring> // 树上的所有路径可以通过从所有叶子节点处的dfs获取
using namespace std;

const int N=2000005; // 总串长*2
int sam[N][26],link[N],len[N],cnt,lenlen;
char s[N];
int a[N],kind,deg[N];

struct pp
{
	int to;
	int old;
}edge[N];
int newly[100005],add_cnt;

void add_edge(int u, int v)
{
	edge[add_cnt]={v, newly[u]};
	newly[u]=add_cnt++;
}

void init()
{
	cnt=1;
}

int add(int c, int last)
{
	if(sam[last][c] && len[last]+1==len[sam[last][c]]) // 已经存在完全相同的endpos
	{
		last=sam[last][c];
		return last;
	}
	int p,cur=++cnt;
	len[cur]=len[last]+1;
	for(p=last; p && !sam[p][c]; p=link[p])
		sam[p][c]=cur;
	if(!p)
		link[cur]=1;
	else
	{
		int q=sam[p][c];
		if(len[q]==len[p]+1)
			link[cur]=q;
		else
		{
			int cl;
			if(p==last) // 原本的sam[last][c]需要分裂 即第一个if只满足前半部分 后半部分不满足
				cl=cur;
			else
			{
				cl=++cnt;
				link[cur]=cl;
			}
			len[cl]=len[p]+1;
			memcpy(sam[cl], sam[q], sizeof(sam[q]));
			link[cl]=link[q];
			while(p && sam[p][c]==q)
			{
				sam[p][c]=cl;
				p=link[p];
			}
			link[q]=cl;
		}
	}
	last=cur;
	return last;
}

void dfs(int flag, int fa, int fap) // dfs建立sam
{
	int xp=add(a[flag], fap);
	for(int i=newly[flag]; ~i; i=edge[i].old)
	{
		int son=edge[i].to;
		if(son==fa)
			continue;
		dfs(son, flag, xp);
	}
}

int main()
{
	memset(newly,-1,sizeof(newly));
	scanf("%d%d", &lenlen, &kind);
	for(int i=1; i<=lenlen; i++)
		scanf("%d", &a[i]);
	int x,y;
	for(int i=1; i<lenlen; i++)
	{
		scanf("%d %d", &x, &y);
		add_edge(x, y);
		add_edge(y, x);
		deg[x]++,deg[y]++;
	}
	init();
	for(int i=1; i<=lenlen; i++)
		if(deg[i]==1) // 从叶子节点dfs
			dfs(i, -1, 1); // 每次dfs都相当于将last置为1
	long long ans=0;
	for(int i=1; i<=cnt; i++)
		ans+=len[i]-len[link[i]];
	cout << ans << '\n';
	return 0;
}

1166:String Set (csgrandeur.cn)

询问一个串在几个字典串中出现过 实现添加串和查询以及删除以某个串为子串的所有字典串

2021年湖南省赛 J

对于添加和查询操作:每次在添加节点时跳link链打标记即可

对于删除操作:由于字符串长度<=1000,并且删除操作的数量<=1000,因此每次都暴力找到对应的sam节点,然后把所有以该串作为子串的字典串都删除掉

#include <iostream> /// 2021省赛 J 询问一个串在几个字典串中出现过 实现添加串和查询以及删除以某个串为子串的所有字典串
#include <cstring>   // 保存sam上每个节点在哪些字典串出现过 每次删除操作都找到对应串并删除其在sam上的所有贡献
#include <vector>
#include <cstdio> // 记得加这个头文件 CSG-CPC上会报CE
using namespace std; // 翻版 : 询问一个串在给出的字典串集合中出现了几次 实现增删查
                     // 解 : 计算出现了几次 需要用到树形dp 但此处是在线询问 因此考虑二进制重构广义sam
const int N=2000005; // 类比 cf edu round 16 F
int sam[N][26],len[N],link[N],cnt,last,now_cnt;
int ans[N];
char s[10005][1005],sr[1005];
vector<int>to[N],node[10002]; // to[i] --> 哪些字符串能遍历到i号节点  node[i] --> i号字符串包含哪些节点
int vis[N],Vis[N],uu[10005]; // 辅助标记节点  辅助标记节点 标记哪些字符串已被删除

void init()
{
	last=cnt=1;
}

void add(int c, int idx)
{
	if(sam[last][c] && len[last]+1==len[sam[last][c]]) // 已经存在完全相同的endpos
	{
		for(int p=sam[last][c]; p && vis[p]!=idx; p=link[p])
		{
			node[idx].push_back(p);
			to[p].push_back(idx);
			ans[p]++;
			vis[p]=idx;
		}
		last=sam[last][c];
		return ;
	}
	int p,cur=++cnt;
	len[cur]=len[last]+1;
	for(p=last; p && !sam[p][c]; p=link[p])
		sam[p][c]=cur;
	if(!p)
		link[cur]=1;
	else
	{
		int q=sam[p][c];
		if(len[q]==len[p]+1)
			link[cur]=q;
		else
		{
			int cl;
			if(p==last) // 原本的sam[last][c]需要分裂 即第一个if只满足前半部分 后半部分不满足
				cl=cur;
			else
			{
				cl=++cnt;
				link[cur]=cl;
			}
			len[cl]=len[p]+1;
			vis[cl]=vis[q];
			to[cl]=to[q];
			for(int k=0; k<to[q].size(); k++) // 把cl添加进以q为子串的所有字典串对应的vector中
				node[to[q][k]].push_back(cl);
			ans[cl]=ans[q];
			memcpy(sam[cl], sam[q], sizeof(sam[q]));
			link[cl]=link[q];
			while(p && sam[p][c]==q)
			{
				sam[p][c]=cl;
				p=link[p];
			}
			link[q]=cl;
		}
	}
	for(int p=cur; p && vis[p]!=idx; p=link[p])
	{
		node[idx].push_back(p);
		to[p].push_back(idx);
		ans[p]++;
		vis[p]=idx;
	}
	last=cur;
}

int main()
{
	int n;
	char op[2];
	init();
	scanf("%d", &n);
	while(n--)
	{
		scanf("%s", op);
		if(op[0]=='I') //添加字符串操作
		{
			scanf("%s", s[++now_cnt]+1);
			int lenlen=strlen(s[now_cnt]+1);
			last=1;
			for(int k=1; k<=lenlen; k++)
				add(s[now_cnt][k]-'a', now_cnt);
		}
		else if(op[0]=='Q') // 查询操作
		{
			scanf("%s", sr+1);
			int p=1,lenlen=strlen(sr+1);
			for(int k=1; k<=lenlen; k++)
				p=sam[p][sr[k]-'a'];
			cout << ans[p] << '\n';
		}
		else
		{
			scanf("%s", sr+1);
			int p=1,lenlen=strlen(sr+1);
			for(int k=1; k<=lenlen; k++)
				p=sam[p][sr[k]-'a'];

			for(int k=0; k<to[p].size(); k++) // 使该字符串在sam上的所有节点-1
			{
				int v=to[p][k]; // 要删除的字符串编号
				if(!Vis[v]) // 判断该字符串是否已被删除过
				{
					Vis[v]=1;
					for(int j=0; j<node[v].size(); j++) // 遍历其在sam上的节点
						ans[node[v][j]]--;
				}
			}
		}
	}
	return 0;
}

下面提出我自己脑补的一个问题:询问一个串在给出的字典串集合中出现了几次 实现增删查

可以看出与这道题不同的地方就是这里是统计出现次数,虽然只相差几个字但做法却完全不同

我们考虑对sam进行二进制重构,即把当前已添加的串按数量1,2,4,8...分组,一共logn组,对每一组都构建sam然后计算一遍子串出现次数,查询的时候对每一组都分别查询一次,总共查询logn次,在添加串时先将其作为数量为1的sam,然后合并大小相同的sam,每次合并的sam大小是log(串总长度)的,合并完后重新计算子串出现次数即可。

对于删除操作,我们只需要对删除的串都放到一个与添加串相同的环境下即可,每次查询的答案就是添加串的集合的出现次数-删除串的集合的出现次数

总时间复杂度O(n * logn),空间复杂度O(n * logn)

这道题是我在做 Problem - F - Codeforces 的时候想到的,目前并没有发现相关题目,感兴趣的可以自己尝试一下

Problem - L - Codeforces

多次询问本质不同公共子串中字典序第k小的子串在第一个串中的出现位置,如有多个优先输出位置下标最小的

由于有多次询问,因此无法直接在sam上预处理(sam并不是树形结构而是DAG,预处理出所有字典序的复杂度是巨大的,last节点的link链上的每一个节点都会向当前新加的节点sam[last][c]连一条边,而link链的长度最大是O(|s|)的,因此遍历sam的复杂度最大为O(|s|^2))

考虑利用sam的集合性质在link树上找字典序第k小

我们考虑反串的link树与字典序的关系:

假如有这么一棵link树

(反串的link树,因此'd'的下标为1)

 那么我们要按字典序从小到大取的话一定是按这个顺序

因此我们可以按节点i与节点link[i]之间第一个差值字符从小到大将每个节点所连的边排序,然后dfs这棵link树即为按字典序从小到大遍历所有串(abacd与acd的第一个差值字符为'b') 

dfs过程中把sam节点按字典序存入pot数组

void dfs(int x)
{
	sort(to[x].begin(), to[x].end()); // 按节点link[i]往节点i分支方向的第一个字符排序  ab -->  dcab dab eab
	for(int i=0; i<to[x].size(); i++)
	{
		pot[++tot]=to[x][i].second; // dfs序即是字典序
		dfs(to[x][i].second);
	}
}

计算按字典序排序的串的数量前缀和

for(int i=1; i<=tot; i++)
	pre[i]=pre[i-1]+(len[pot[i]]-len[link[pot[i]]]); // 字典序小于等于节点i的子串数量

查询时直接在pre数组中二分即可

代码:

#include <iostream> /// Gym k-th Smallest Common Substring 多次询问 本质不同公共子串中字典序第k小的子串在第一个串中的出现位置
#include <cstring>  // 由于要记录公共子串在第一个串中的出现位置 因此不采用最短串建立sam的方式
#include <string>   // 字典序 ==> 后缀树 <==> 反串建立的sam的link树
#include <vector>   // 直接在sam上统计第k小字典序涉及节点太多 复杂度极大(sam并不是树形结构,因此会多次访问同一节点)
#include <algorithm>// 在反串建立的sam的link树上统计每次遍历到一个节点时增加的串的数量有len[i]-len[link[i]]个
using namespace std;

const int N=400005;
int sam_cnt,last,lenlen,n;
int len[N], link[N], sam[N][26];
string s[100005];
int en[N];
int vis[N], vis_cnt[N];
pair<int, int> endpos[N]; // 存储sam上节点对应字符串的编号 以及末尾元素下标
vector<pair<int, int> >to[N];
int pot[N],tot;
long long pre[N];
int cc[N],a[N];

void init()
{
	sam_cnt=last=1;
	tot=0;
	memset(sam,0,sizeof(sam));
	len[1]=0;
	memset(en,0,sizeof(en));
	memset(link,0,sizeof(link));
	memset(vis_cnt,0,sizeof(vis_cnt));
	memset(vis,0,sizeof(vis));
	memset(cc,0,sizeof(cc));
}

void calc()
{
	for(int i=1; i<=sam_cnt; i++)
		cc[len[i]]++;
	for(int i=1; i<=sam_cnt; i++)
		cc[i]+=cc[i-1];
	for(int i=1; i<=sam_cnt; i++) // 按照长度排序(升序)  a[1] : 最短子串所在的节点编号
		a[cc[len[i]]--]=i;
}

void add(int c, pair<int, int> now)
{
	int idx=now.first;
    if(sam[last][c] && len[last]+1==len[sam[last][c]]) // 已经存在完全相同的endpos
    {
        last=sam[last][c];
        for(int p=last; p && vis[p]!=idx; p=link[p])
        {
            vis[p]=idx;
            vis_cnt[p]++;
        }
        return ;
    }
    int p,cur=++sam_cnt;
    if(idx==1)
    	en[cur]=now.second;
    endpos[cur]=now; // 记录所有后缀的末尾元素对应的字符
    len[cur]=len[last]+1;
    for(p=last; p && !sam[p][c]; p=link[p])
        sam[p][c]=cur;
    if(!p)
        link[cur]=1;
    else
    {
        int q=sam[p][c];
        if(len[q]==len[p]+1)
            link[cur]=q;
        else
        {
            int cl;
            if(p==last) // 原本的sam[last][c]需要分裂 即第一个if只满足前半部分 后半部分不满足
                cl=cur;
            else
            {
                cl=++sam_cnt;
                link[cur]=cl;
            }
            len[cl]=len[p]+1;
            vis[cl]=vis[q];
            vis_cnt[cl]=vis_cnt[q];
            endpos[cl]=endpos[q];
            en[cl]=en[q];
            memcpy(sam[cl], sam[q], sizeof(sam[q]));
            link[cl]=link[q];
            while(p && sam[p][c]==q)
            {
                sam[p][c]=cl;
                p=link[p];
            }
            link[q]=cl;
        }
    }
    last=cur;
    for(int p=last; p && vis[p]!=idx; p=link[p])
    {
        vis[p]=idx;
        vis_cnt[p]++;
    }
}

void dfs(int x)
{
	sort(to[x].begin(), to[x].end()); // 按节点link[i]往节点i分支方向的第一个字符排序  ab -->  dcab dab eab
	for(int i=0; i<to[x].size(); i++)
	{
		pot[++tot]=to[x][i].second; // dfs序即是字典序
		dfs(to[x][i].second);
	}
}

int main()
{
	int T;
	scanf("%d", &T);
	while(T--)
	{
		init();
		scanf("%d", &n);
		for(int i=1; i<=n; i++)
		{
			last=1;
			cin>>s[i];
			lenlen=s[i].size();
			s[i]=' '+s[i];
			for(int j=1; j<=lenlen/2; j++) swap(s[i][j], s[i][lenlen-j+1]); // 翻转串
			for(int j=1; j<=lenlen; j++) // 反串建立sam
				add(s[i][j]-'a', {i, j});
		}
		for(int i=1; i<=sam_cnt; i++) // 清空边
			to[i].clear();
		calc();
		for(int i=sam_cnt; i>=1; i--) // 题目要求输出最左边出现的子串的位置 即翻转串最右边的出现位置 拓扑序更新en数组
			en[link[a[i]]]=max(en[link[a[i]]], en[a[i]]);
		for(int i=2; i<=sam_cnt; i++)
			if(vis_cnt[i]==n)
			{
				int idx=endpos[i].first,en_id=endpos[i].second;
				to[link[i]].push_back({s[idx][en_id-len[link[i]]]-'a', i});
			}   // 节点link[i]往节点i分支方向的第一个字符
		dfs(1);
		for(int i=1; i<=tot; i++)
			pre[i]=pre[i-1]+(len[pot[i]]-len[link[pot[i]]]); // 字典序小于等于节点i的子串数量
		int q;
		scanf("%d", &q);
		while(q--)
		{
			int x;
			scanf("%d", &x); // 询问字典序第x小的子串在第一个串中的出现位置
			if(x>pre[tot])
				cout << -1 << '\n';
			else
			{
				int sid=lower_bound(pre+1, pre+1+tot, x)-pre; // 找到第k小的字符串的对应节点编号
				int r=en[pot[sid]];
				int l=r-len[link[pot[sid]]]-(x-pre[sid-1]-1);
				cout << (s[1].size()-1-r) << ' ' << (s[1].size()-1-l)+1 << '\n'; // 翻转后输出即为在原串中的位置(下标从0开始) [l, r)
			}  // 题目要求输出[l, r)
		}
	}
	return 0;
}

Problem - E - Codeforces

给出串s和m个文本串,每次询问s[l,r]在编号在[L,R]的文本串中最多的出现次数及对应的文本串编号

对这m个文本串建立广义sam,用线段树合并维护每个sam节点出现在各个文本串中的出现次数,以及出现次数最大的对应编号

对于询问s[l, r],可以预处理s的每个前缀可以匹配到sam上的哪个节点,然后通过这个节点倍增去找s[l, r]所在的节点即可

#include <iostream> /// Codeforces Round #349 (Div. 1) E 查询s[l,r]在编号在[L,R]的串中最多的出现次数及对应编号 O(n*logn)
#include <cstring>   // 用所有匹配串建立广义sam 预处理主串在sam上的匹配位置rpos 每次查询都倍增找对应结点
using namespace std; // 出现次数及其编号可以用线段树维护再沿link树由子树向当前结点合并
                     // 线段树还可以维护每个结点代表串在母串中的出现位置
const int N=100005,M=3000005;
char ss[500005],s[N];
int sam[N][26],len[N],link[N],sam_cnt,last,a[N],c[N];
int f[N][21],rpos[500005],maxl[500005];
int root[N],ls[M],rs[M],val[M],re[M],cnt,n;// val[i] --> 节点i代表区间中出现最多的权值的出现次数  re[i] --> 节点i代表区间中出现最多的权值
struct pp
{
	int to;
	int old;
}edge[N];
int newly[N],edge_cnt;

//void add_edge(int u, int v)
//{
//	edge[edge_cnt]={v, newly[u]};
//	newly[u]=edge_cnt++;
//}

void push_up(int p)
{
	if(val[ls[p]]>=val[rs[p]])
	{
		val[p]=val[ls[p]];
		re[p]=re[ls[p]]; // val[ls[p]] == val[rs[p]]时 re[ls[p]]<re[rs[p]]
	}
	else
	{
		val[p]=val[rs[p]];
		re[p]=re[rs[p]];
	}
}

void build(int &p, int dl, int dr, int x, int v)
{
	if(!p)
		p=++cnt;
	if(dl==dr) // dl==dr==x
	{
		val[p]+=v;
		re[p]=x; // 记录该位置对应的文本串编号
		return ;
	}
	int mid=(dl+dr)>>1;
	if(x<=mid)
		build(ls[p], dl, mid, x, v);
	else
		build(rs[p], mid+1, dr, x, v);
	push_up(p);
}

int merge(int x, int y, int tl, int tr)
{
	if(!x || !y)
		return x|y; // 此处会产生线段树共用结点
	int now=++cnt;
//	int now=x; // 不能覆盖x的值 --> x可能是与fail子树节点中的线段树结点共用的结点
	if(tl==tr)
	{
		re[now]=re[x];
		val[now]=val[x]+val[y];
		return now;
	}
	int mid=(tl+tr)>>1;
	ls[now]=merge(ls[x], ls[y], tl, mid);
	rs[now]=merge(rs[x], rs[y], mid+1, tr);
	push_up(now);
	return now;
}

int query(int p, int tl, int tr, int dl, int dr)
{
	if(tl>=dl && tr<=dr)
		return p;
	int mid=(tl+tr)>>1;
	int t=0;
	if(dl<=mid)
		t=query(ls[p], tl, mid, dl, dr);
	if(dr>mid)
	{
		int t2=query(rs[p], mid+1, tr, dl, dr);
		if(val[t2]>val[t] || (val[t2]==val[t] && re[t2]<re[t] && re[t2])) // 出现次数相同时优先返回节点编号最小的
			t=t2;
	}
	return t;
}

void init()
{
//	memset(newly,-1,sizeof(newly));
	last=sam_cnt=1;
}

void add(int c, int idx)
{
	if(sam[last][c] && len[last]+1==len[sam[last][c]]) // 已经存在完全相同的endpos
	{
		last=sam[last][c];
		build(root[last], 1, n, idx, 1);
		return ;
	}
	int p,cur=++sam_cnt;
	build(root[cur], 1, n, idx, 1);
	len[cur]=len[last]+1;
	for(p=last; p && !sam[p][c]; p=link[p])
		sam[p][c]=cur;
	if(!p)
		link[cur]=1;
	else
	{
		int q=sam[p][c];
		if(len[q]==len[p]+1)
			link[cur]=q;
		else
		{
			int cl;
			if(p==last) // 原本的sam[last][c]需要分裂 即第一个if只满足前半部分 后半部分不满足
				cl=cur;
			else
			{
				cl=++sam_cnt;
				link[cur]=cl;
			}
			len[cl]=len[p]+1;
			memcpy(sam[cl], sam[q], sizeof(sam[q]));
			link[cl]=link[q];
			while(p && sam[p][c]==q)
			{
				sam[p][c]=cl;
				p=link[p];
			}
			link[q]=cl;
		}
	}
	last=cur;
}

//void dfs(int x, int fa) // 递归处理常数较大 可以calc后通过子串长度遍历
//{
//	f[x][0]=fa;
//	for(int i=1; i<=19; i++)
//		f[x][i]=f[f[x][i-1]][i-1];
//	for(int i=newly[x]; ~i; i=edge[i].old)
//	{
//        dfs(edge[i].to, x);
//		root[x]=merge(root[x], root[edge[i].to], 1, n); // 向link子树合并信息 能匹配到son节点的一定可以匹配link[son] (x)
//	}
//}

void calc()
{
    for(int i=1; i<=sam_cnt; i++)
        c[len[i]]++;
    for(int i=1; i<=sam_cnt; i++)
        c[i]+=c[i-1];
    for(int i=1; i<=sam_cnt; i++)
        a[c[len[i]]--]=i;
}

int main()
{
	init();
	scanf("%s", ss+1);
	scanf("%d", &n);
	for(int i=1; i<=n; i++)
	{
		scanf("%s", s+1);
		last=1;
		int lenlen=strlen(s+1);
		for(int j=1; j<=lenlen; j++)
			add(s[j]-'a', i);
	}

	calc();
	for(int i=2; i<=sam_cnt; i++)// 合并线段树、计算倍增父亲节点
    {
        f[a[i]][0]=link[a[i]];
        for(int j=1; j<=19; j++)
            f[a[i]][j]=f[f[a[i]][j-1]][j-1];
    }
    for(int i=sam_cnt; i>=2; i--)
        root[link[a[i]]]=merge(root[link[a[i]]], root[a[i]], 1, n); // 向link子树合并信息 节点i出现的地方一定出现了link[i]

//	for(int i=1; i<=sam_cnt; i++) // 合并线段树、计算倍增父亲节点
//		add_edge(link[i], i);
//	dfs(1, 1);

	int lenlen=strlen(ss+1); // 计算主串在副串sam上的rpos
	int p=1,now_len=0;
	for(int i=1; i<=lenlen; i++)
	{
		int v=ss[i]-'a';
		while(p && !sam[p][v])
			p=link[p],now_len=len[p];
		if(sam[p][v])
			p=sam[p][v],now_len++;
		else
			p=1,now_len=0;
		rpos[i]=p,maxl[i]=now_len; // 虽然匹配到节点p 但是len[p]可能大于now_len 因此要记录真实的匹配情况
	}

	int q;
	scanf("%d", &q);
	while(q--)
	{
		int l,r,pl,pr;
		scanf("%d%d%d%d", &l, &r, &pl, &pr);
		int p=rpos[pr];
		int aim=pr-pl+1;
		for(int i=19; i>=0; i--) // 倍增找到目标节点
            if(f[p][i] && len[f[p][i]]>=aim)
                p=f[p][i];
		int ans=query(root[p], 1, n, l, r);
		if(aim>maxl[pr] || !val[ans]) // 匹配不到s[pl, pr] || 在[l, r]中的出现次数为0
			cout << l << ' ' << 0 << '\n';
		else
			cout << re[ans] << ' ' << val[ans] << '\n';
	}
	return 0;
}

I-LCS Spanning Tree_第46屆ICPC 東亞洲區域賽(澳門)(正式賽) (nowcoder.com)

每个节点有一个字符串 求i -> j的边权是si和sj的最长公共子串长度的图的最小生成树大小最大为多少

显然是kruskal最小生成树,建立广义sam后按长度从大到小枚举子串,遍历所有以该串为子串的节点放到一个连通块中即可。

#include <iostream> /// 第46届icpc澳门(正式赛) I 每个节点有一个字符串 求i -> j的边权是si和sj的最长公共子串长度的图的最小生成树大小
#include <cstring>   // 对所有s建立广义sam 对串长排序 从大到小枚举所有串 再枚举以该串作为公共子串的点 进行kruskal算法即可
#include <vector>
using namespace std;

const int N=4000005;
int sam[N][26],link[N],len[N],a[N],c[N],last,sam_cnt;
char s[2000005];
int pre[2000005];
vector<int>to[N]; // to[i] --> 有哪些字符串可以匹配到该节点

void add(int c)
{
	if(sam[last][c] && len[last]+1==len[sam[last][c]])
	{
		last=sam[last][c];
		return ;
	}
	int p,cur=++sam_cnt;
	len[cur]=len[last]+1;
	for(p=last; p && !sam[p][c]; p=link[p])
		sam[p][c]=cur;
	if(!p)
		link[cur]=1;
	else
	{
		int q=sam[p][c];
		if(len[q]==len[p]+1)
			link[cur]=q;
		else
		{
			int cl;
			if(p==last)
				cl=cur;
			else
			{
				cl=++sam_cnt;
				link[cur]=cl;
			}
			len[cl]=len[p]+1;
			memcpy(sam[cl], sam[q], sizeof(sam[q]));
			link[cl]=link[q];
			while(p && sam[p][c]==q)
			{
				sam[p][c]=cl;
				p=link[p];
			}
			link[q]=cl;
		}
	}
	last=cur;
}

void calc()
{
	for(int i=1; i<=sam_cnt; i++)
		c[len[i]]++;
	for(int i=1; i<=sam_cnt; i++)
		c[i]+=c[i-1];
	for(int i=1; i<=sam_cnt; i++)
		a[c[len[i]]--]=i;
}

int fin(int x)
{
	if(pre[x]!=x)
		return pre[x]=fin(pre[x]);
	return pre[x];
}

int main()
{
	last=sam_cnt=1;
	int n;
	scanf("%d", &n);
	for(int i=1; i<=n; i++)
	{
		scanf("%s", s+1);
		last=1;
		int lenlen=strlen(s+1);
		for(int j=1; j<=lenlen; j++)
			add(s[j]-'a'),to[last].push_back(i);
	}
	for(int i=1; i<=n; i++)
		pre[i]=i;
	calc();
	long long ans=0;
	for(int i=sam_cnt; i>=2; i--) // kruskal最小生成树 题目要求生成权值最大 从最长的边开始遍历
	{
		for(int j=1; j<to[a[i]].size(); j++) // 将以a[i]为公共子串的节点连接起来
		{
			int fx=fin(to[a[i]][j-1]),fy=fin(to[a[i]][j]);
			if(fx!=fy)
			{
				pre[fy]=fx;
				ans+=len[a[i]];
			}
		}
		to[link[a[i]]].push_back(to[a[i]][0]); // 不需要用染色的方法去处理每个sam节点对应的节点编号
	}                                          // 每次处理完节点a[i]后,其所有对应的节点都连到一个连通块中了
	cout << ans << '\n';                       // 因此只需要把任意一个节点加入到to[link[a[i]]]中即可
	return 0;
}

Problem - G - Codeforces

每次询问模式串在编号为x的文本串中的出现次数 

考虑将每次询问的模式串存在对应编号的文本串节点中,然后遍历文本串的trie树,过程中标记trie树节点对应的sam节点,当访问到带有询问的trie树节点时只需找到询问的模式串对应的sam节点的link子树中的标记数量即为询问的答案

#include <iostream> /// Educational Codeforces Round 71 Div 2 G 每次询问模式串在串x中的出现次数 O(n+sum(模式串长度))
#include <cstring>  // endpos的大小取决取其link子树的大小 在文本串的trie树上dfs 每次将访问到的点的sam对应点权值+1
#include <queue>    // 当有模式串对于当前点的询问时进行线段树子树查询 回溯时将该点对应的sam上的节点-1 保证每次查询都是合法的
#include <string>
#include <vector>
using namespace std;

const int N=800005;
int trie[N][26],pre[N],num[N],pos[N]; // pos[i] --> trie树上的节点i对应于sam上的节点编号
int sam[N][26],link[N],len[N],sam_cnt,k; // pre[i] --> 节点i的父节点  num[i] --> 连向节点i的边的权值
char ch[2];
struct pp
{
	int to;
	int old;
}edge[N];
int newly[N],edge_cnt;
int in[N],out[N],dfn_cnt;
vector<pair<string, int> >to[N];
int match[N],ans[N];
int tree[N<<2];

void add_edge(int u, int v)
{
	edge[edge_cnt]={v, newly[u]};
	newly[u]=edge_cnt++;
}

int add(int c, int last) // 对trie树建立广义sam使用单串插入即可
{
	int p,cur=++sam_cnt;
	len[cur]=len[last]+1;
	for(p=last; p && !sam[p][c]; p=link[p])
		sam[p][c]=cur;
	if(!p)
		link[cur]=1;
	else
	{
		int q=sam[p][c];
		if(len[q]==len[p]+1)
			link[cur]=q;
		else
		{
			int cl=++sam_cnt;
			len[cl]=len[p]+1;
			memcpy(sam[cl], sam[q], sizeof(sam[q]));
			link[cl]=link[q];
			while(p && sam[p][c]==q)
			{
				sam[p][c]=cl;
				p=link[p];
			}
			link[cur]=link[q]=cl;
		}
	}
	return cur; // 返回last
}

void bfs() // 通过trie树建立sam
{
	queue<int>que;
	for(int i=0; i<26; i++)
		if(trie[1][i])
			que.push(trie[1][i]);
	pos[1]=1;
	while(que.size())
	{
		int x=que.front();
		que.pop();
		pos[x]=add(num[x], pos[pre[x]]);
		for(int i=0; i<26; i++)
			if(trie[x][i])
				que.push(trie[x][i]);
	}
}

void __dfs(int x)
{
	in[x]=++dfn_cnt;
	for(int i=newly[x]; ~i; i=edge[i].old)
		__dfs(edge[i].to);
	out[x]=dfn_cnt;
}

void modify(int p, int tl, int tr, int x, int v) // 单点修改 子树求和
{
	if(tl==tr)
	{
		tree[p]+=v;
		return ;
	}
	int mid=(tl+tr)>>1;
	if(x<=mid)
		modify(p<<1, tl, mid, x, v);
	else
		modify(p<<1|1, mid+1, tr, x, v);
	tree[p]=tree[p<<1]+tree[p<<1|1];
}

int query(int p, int tl, int tr, int dl, int dr)
{
	if(!dl || !dr)
		return 0;
	if(tl>=dl && tr<=dr)
		return tree[p];
	int mid=(tl+tr)>>1;
	int t=0;
	if(dl<=mid)
		t+=query(p<<1, tl, mid, dl, dr);
	if(dr>mid)
		t+=query(p<<1|1, mid+1, tr, dl, dr);
	return t;
}

void dfs(int x) // 注意要对trie树dfs而不是sam
{
	modify(1, 1, dfn_cnt, in[pos[x]], 1); // 加上当前节点贡献
	for(int i=0; i<to[pos[x]].size(); i++)
	{
		int p=1;
		for(int j=0; j<to[pos[x]][i].first.size(); j++) // 找到模式串所在节点
			p=sam[p][to[pos[x]][i].first[j]-'a'];
		if(p)
			ans[to[pos[x]][i].second]=query(1, 1, dfn_cnt, in[p], out[p]); // link子树查询
	}
	for(int i=0; i<26; i++)
		if(trie[x][i])
			dfs(trie[x][i]);
	modify(1, 1, dfn_cnt, in[pos[x]], -1); // 减去当前节点贡献
}

int main()
{
	sam_cnt=k=1;
	memset(newly,-1,sizeof(newly));
	int n,m;
	scanf("%d", &n);
	for(int i=1; i<=n; i++)
	{
		int op;
		scanf("%d", &op);
		if(op==1)// 新增一个版本的串
		{
			scanf("%s", ch);
			if(!trie[1][ch[0]-'a'])
			{
				trie[1][ch[0]-'a']=++k;
				pre[k]=1,num[k]=ch[0]-'a';
			}
			match[i]=trie[1][ch[0]-'a']; // i版本的文本串在trie树上对应的结点
		}
		else // 在y版本的串后面添加字符新成新版本的串
		{
			int y;
			scanf("%d %s", &y, ch);
			if(!trie[match[y]][ch[0]-'a'])
			{
				trie[match[y]][ch[0]-'a']=++k;
				pre[k]=match[y],num[k]=ch[0]-'a';
			}
			match[i]=trie[match[y]][ch[0]-'a'];
		}
	}

	bfs(); // 对trie树建立广义sam

	for(int i=2; i<=sam_cnt; i++) // 建立link树 dfs序
		add_edge(link[i], i);
	__dfs(1);

	scanf("%d", &m);
	for(int i=1; i<=m; i++)
	{
		string q;
		int idx;
		scanf("%d", &idx);
		cin>>q;
		to[pos[match[idx]]].push_back({q, i}); // 标记哪些节点带有询问
	}

	dfs(1);
	for(int i=1; i<=m; i++)
		cout << ans[i] << ' ';
	cout << '\n';
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值