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

给还没了解过后缀自动机的同学推荐一个入门视频【SDUACM-暑期专题div1】后缀自动机SAM_哔哩哔哩_bilibiliyy

重要的一点是要有集合思想

即每个sam节点都可能代表着>=1个串

后缀自动机的空间复杂度和时间复杂度都为O(n)

下面是练习题

P3804 【模板】后缀自动机 (SAM) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

字符串中出现次数>1的子串的出现次数乘上其长度的最大值

对字符串建立sam后遍历sam节点判断长度就好了

如何统计子串出现次数?

对于子串的出现次数可以每次都对插入新字符时的cur节点加上一个标记,相当于这个节点的maxlen对应的串出现了一次

那么就会出现一个问题: 对于节点u, 有link[cur]=u 但是当前只对cur打上了标记,cur的link链上的点没有被打上这次标记

相当于我在之前已经有了子串"ab"、"b",当前的last对应的子串是"a",此时我再add一个字符'b',那么就只会对"ab"的节点打上标记,其link节点"b"没有被打上这次标记,但实际上"b"串又出现了一次,

这样看其实可以引出一个做法:每次都暴力跳link链打标记,这样做的复杂度是O(n)的会将程序总的复杂度变为O(n^2),不可取。

对于这种情况有几种解决方法

1.树形dp:将每个节点与其link节点作为边建树后做一遍树形dp就可以解决

2.按节点对应的串长从大到小排序:这是最简单也是最实用的方法,排序后从大到小遍历每个节点,只要将其link节点的出现次数加上当前节点的出现次数即可(其实就是扁平化的树形dp)

关于如何排序的问题,优先排除使用sort函数(这样会使程序的复杂度从O(n)变为O(n*logn),从而浪费了使用sam的优势),常用的是拓扑序或计数排序,可以参考【算法】排序算法之计数排序 - 知乎 (zhihu.com)

下面上代码 

#include  /// 洛谷 3804 找出字符串中出现次数>1的子串的出现次数乘上其长度的最大值(SAM后缀自动机)
#include 
#include 
#include 
using namespace std;

const int N=2000005; // 注意开两倍空间
int sam_cnt,last; // 当前自动机中的节点数  上一个添加字符所在的节点
int len[N],link[N],sam[N][26]; // 该集合中的最长的后缀的长度  后缀链边   后缀自动机中每一个节点都代表一个集合
char s[N];                      // maxlen[i]=len[i]   minlen[i]=len[link[i]]+1  每个集合中所代表的字符串数量 == maxlen-minlen+1
int size[N],deg[N];  // size[i] : 集合i的出现次数
void init()
{
	sam_cnt=last=1;
//	memset(len,0,sizeof(len));
//	memset(link,0,sizeof(link));
//	memset(sam,0,sizeof(sam));
}

void add(int c) // 每次最多添加两个sam节点 因此空间是O(n)的
{
	int p,cur=++sam_cnt;
	len[cur]=len[last]+1;
	for(p=last; p && !sam[p][c]; p=link[p]) // 整个程序循环最多执行|s|次
		sam[p][c]=cur;
	if(!p) // 情况1 已经遍历到初始节点了 即该过程中没有已连接的节点
		link[cur]=1;
	else // 情况2
	{
		int q=sam[p][c];
		if(len[q]==len[p]+1) // A类 可以直接转移到cur  len[q] == len[p]+1 <= minlen[cur]-1
			link[cur]=q;
		else // B类 需要分裂节点
		{
			int cl=++sam_cnt; // 将q节点分裂出可连向cur的新节点cl
			len[cl]=len[p]+1;
			memcpy(sam[cl], sam[q], sizeof(sam[q]));  // 原本q指向的节点新节点也要指向
			link[cl]=link[q]; // 用新节点代替节点q的位置
			while(p && sam[p][c]==q) // 将p的link链上原本连向q节点的都连向新节点
			{
				sam[p][c]=cl;
				p=link[p];
			}
			link[cur]=link[q]=cl; // cur和q的link指针都应该指向克隆节点
		}
	}
	size[cur]=1; // 只考虑非克隆节点产生的贡献
	last=cur;
}

int c[N],a[N];

void calc()
{
	queueque;// 在link树上拓扑序求出每个集合的出现次数 即每个子串的出现次数  (相当于将link反向建边再进行树形dp)
	for(int i=1; i<=sam_cnt; i++)
        deg[link[i]]++;
	for(int i=1; i<=sam_cnt; i++)
		if(deg[i]==0) // 入度为0的入队列
			que.push(i);
	while(que.size())
	{
		int u=que.front();
		que.pop();
		size[link[u]]+=size[u];
		if(--deg[link[u]]==0)
			que.push(link[u]);
	}

//	for(int i=1; i<=sam_cnt; i++) // 计数排序 O(sam_cnt)
//		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;   // 按照长度排序(升序)  a[1] : 最短子串所在的节点编号
//    for(int i=sam_cnt; i>=1; i--)
//        size[link[a[i]]]+=size[a[i]];

	long long ans=0;
	for(int i=2; i<=sam_cnt; i++) // 节点1是空串 直接跳过即可
//        ans+=len[i]-len[link[i]];  // 计算所有本质不同子串数
		if(size[i]>1)
			ans=max(ans, 1ll*size[i]*len[i]); // 同一个集合中的字符串出现次数一定是相同的 因此只需找到最长的串的长度即len[i]
	printf("%lld\n", ans);
}

int main()
{
	scanf("%s", s+1);
	int lenlen=strlen(s+1);
	init();
	for(int i=1; i<=lenlen; i++)
		add(s[i]-'a');
	calc();
	return 0;
}

P1368 【模板】最小表示法 - 洛谷 | 计算机科学教育新生态 (luogu.co​​​​​​m.cn)

后缀自动机的经典应用

核心代码:

// 循环同构串 : 将此串首位相连后取其中的原串长度的一端形成的串
// 复制后首位相接建立sam 如下遍历即可
    for(int i=1; i<=n; i++) // 输出原串长度的循环同构串
    {
        map::iterator q=sam[p].begin();
        //解释:q是迭代器
        //(*q).first是这条边的值
        //(*q).second是这条边连着的下一个节点
        p=(*q).second;
        cout<<(*q).first<<' ';
    }

注意sam用的不是数组而是map(题目给的字符集大小太大)

P3975 [TJOI2015]弦论 - 洛谷 | 计算机科学教育新生态 (lu​​​​​​ogu.com.cn)

求字典序第k小的子串是多少

#include  /// 洛谷 3975 字典序第k小的子串是多少
#include  // 计算出 每个子串的出现次数 以及 以s[i]结尾的后缀数量
using namespace std;

const int N=1000005;
int sam_cnt,last,k,t;
int len[N], link[N], sam[N][26];
char s[N];
int dp[N],size[N],c[N],a[N]; // dp[i] : 集合i中字符串的所有后缀的数量  size[i] : 集合i中字符串的出现次数

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

void add(int c)
{
	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) // 情况1 已经遍历到初始节点了 即该过程中没有已连接的节点
		link[cur]=1;
	else // 情况2
	{
		int q=sam[p][c];
		if(len[q]==len[p]+1) // A类 可以直接转移到cur  len[q] == len[p]+1 == minlen[cur]-1
			link[cur]=q;
		else // B类 需要分裂节点
		{
			int cl=++sam_cnt; // 将q节点分裂出可连向cur的新节点cl
			len[cl]=len[p]+1;
			memcpy(sam[cl], sam[q], sizeof(sam[q]));  // 原本q指向的节点克隆节点也要指向
			link[cl]=link[q]; // 用新节点代替节点q的位置
			while(p && sam[p][c]==q) // 将p的link链上原本连向q节点的都连向新节点
			{
				sam[p][c]=cl;
				p=link[p];
			}
			link[cur]=link[q]=cl; // cur和q的link指针都应该指向新节点
		}
	}
	size[cur]=1; // 只考虑非克隆节点产生的贡献
	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[1] : 最短子串所在的节点编号
		a[c[len[i]]--]=i;
	for(int i=sam_cnt; i>=1; i--) // 每个子串的出现次数 短的后缀会在长的后缀中出现 因此要从长的字符串向短的字符串遍历
		size[link[a[i]]]+=size[a[i]];
	if(t) // 相同子串算多个
		for(int i=1; i<=sam_cnt; i++)
			dp[i]=size[i];
	else //  相同子串不算多个
		for(int i=1; i<=sam_cnt; i++)
			dp[i]=size[i]=1;
	size[1]=dp[1]=0; // 1 是根节点 要清0
	for(int i=sam_cnt; i>=1; i--)
		for(int j=0; j<26; j++)
			dp[a[i]]+=dp[sam[a[i]][j]];
}

void dfs(int flag, int k)
{
	if(k<=size[flag]) // 添加该后缀后剩余的步数不足以去添加更多的后缀
		return ;
	k-=size[flag]; // 减去添加当前后缀所带来的贡献
	for(int i=0; i<26; i++)
		if(sam[flag][i])
		{
			if(k>dp[sam[flag][i]]) // 答案串不在该子树内
				k-=dp[sam[flag][i]];
			else
			{
				cout << (char)(i+'a'); // 输出该字符 继续搜索其后缀
				dfs(sam[flag][i], k);
				return ;
			}
		}
}

int main()
{
	scanf("%s", s+1);
	scanf("%d%d", &t, &k);
	int lenlen=strlen(s+1);
	init();
	for(int i=1; i<=lenlen; i++)
		add(s[i]-'a');
	calc();
	if(dp[1]>=k)
		dfs(1, k);
	else
		cout << -1;
	cout << '\n';
	return 0;
}

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

求两个字符串的所有公共子串的数量

对其中一个串建立SAM 统计以i字符为后缀的字符串有多少个 另一个串上去匹配

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[1] : 最短子串所在的节点编号
		a[c[len[i]]--]=i;
	for(int i=sam_cnt; i>=1; i--)
		size[link[a[i]]]+=size[a[i]];
	for(int i=1; i<=sam_cnt; i++)
		fsize[a[i]]+=fsize[link[a[i]]]+1ll*(len[a[i]]-len[link[a[i]]])*size[a[i]];
}

void fun()
{
	int now_len=0,p=1; // 模式串已经匹配的长度  匹配到自动机上的节点
	long long ans=0;
	for(int i=1; i<=lenlen2; i++)
	{
		int v=ch[i]-'a';
		while(p && !sam[p][v])
			p=link[p];
		if(!p) // 自动机中没有能匹配v的节点
			now_len=0,p=1;
		else
		{
			now_len=min(now_len, len[p])+1; // 此时的p还未添加后缀v 代表之前已经匹配了的节点
			p=sam[p][v];
//			ans=max(ans, now_len);  // 最长公共子串长度
			ans+=fsize[link[p]]+1ll*(now_len-len[link[p]])*size[p];
		}
	}
	cout << ans << endl;
}

G-多模式最长公共子串_牛客竞赛字符串专题班SAM(后缀自动机简单应用)习题 (nowcoder.com)

求多个串的最长公共子串

对最短的串建立SAM 其它串上去匹配 统计每个节点被匹配的最大长度 总体取最小值

#include 
#include 
#include 
using namespace std;

const int N=2000005;
int cnt,last,k,t,lenlen,lenlen2;
int len[N], link[N], trie[N][26];
char s[N],ch[N];
int c[N],a[N],match[N],maxl[N];
string ss[100005];

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

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;
}

void add(int c)
{
	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=++cnt; 
			len[cl]=len[p]+1;
			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[cur]=link[q]=cl;
		}
	}
	last=cur;
}

void fun(int id)
{
	int now_len=0,p=1;
	for(int i=1; i<=cnt; i++)
		match[i]=0;
	for(int i=1; i<=lenlen2; i++)
	{
		int v=ss[id][i]-'a';
		while(p && !trie[p][v]) // *
			p=link[p];
		if(!p)
			now_len=0,p=1;
		else
		{
			now_len=min(now_len, len[p])+1;
			p=trie[p][v];
			match[p]=max(match[p], now_len);
		}
	}
	for(int i=cnt; i>=1; i--)
		match[link[a[i]]]=max(match[link[a[i]]], min(match[a[i]], len[link[a[i]]]));
	for(int i=1; i<=cnt; i++)
		maxl[i]=min(maxl[i], match[i]);
}

int main()
{
	int n;
	scanf("%d", &n);
	int minli=-1,minl=0x3f3f3f3f;
	for(int i=1; i<=n; i++)
	{
		cin>>ss[i];
		ss[i]=' '+ss[i];
		if(ss[i].size()

Palindrome (nowcoder.com)

取两个子串相拼是回文串的方案数 子串相同但位置不同即为不同方案

综合能力很强的一道题,需要用到后缀自动机+回文树,没学回文树的可以先跳过

总体思路就是先对翻转串建立sam然后拿原串上去匹配,匹配到的节点就是两两可以拼接成回文串的,然后中间也可以加上一个回文串如图:

后缀自动机 练习题从入门到精通_第1张图片

形象一点就是

后缀自动机 练习题从入门到精通_第2张图片

#include  /// NC19786 取两个子串相拼是回文串的方案数 子串相同但位置不同即为不同方案(pam + sam)
#include  // 将原串翻转 求两串的相同子串数 同时其中间也可拼接任意回文串
using namespace std; //若方案用长度不同的子串去拼接 则较长的串中一定含有回文串 因此枚举拼接后的回文串的两端即可包含所有方案

const int N=400005;
int pam[N][26],fail[N],num[N],len_1[N],pos[N],last_1,p_1,ni;
int sam[N][26],link[N],len[N],sz[N],c[N],a[N],cnt,last_2,lenlen;
__int128 fsize[N],ans;
char s[N],rs[N];

int newnode(int l)
{
	memset(pam[p_1], 0, sizeof(pam[p_1]));
	num[p_1]=0;
	len_1[p_1]=l;
	return p_1++;
}

void init()
{
	last_1=p_1=0; // 回文树初始化
	newnode(0),newnode(-1); // 节点0是偶数长度回文串的根  节点1是奇数长度回文串的根
	s[0]='?',rs[0]='?';
	fail[0]=1;

	last_2=cnt=1; // sam初始化
	memset(sam, 0, sizeof(sam));
	memset(sz, 0, sizeof(sz));
}

int get_fail(int x, int flag)
{
	if(flag)
	{
		while(s[ni-len_1[x]-1]!=s[ni])
			x=fail[x];
	}
	else
	{
		while(rs[ni-len_1[x]-1]!=rs[ni])
			x=fail[x];
	}
	return x;
}

void add_1(int c, int flag) // 回文自动机加点
{
	int old=get_fail(last_1, flag);  // 找到最长的串 cA 使得可以和 c 拼接成 cAc (A可以是空串)
	if(!pam[old][c]) // 出现了一个新的本质不同回文串
	{
		int now=newnode(len_1[old]+2); // 新串的长度是串A长度+2
		fail[now]=pam[get_fail(fail[old], flag)][c];
		pam[old][c]=now;
		num[now]=num[fail[now]]+1;
	}
	last_1=pam[old][c];
	pos[ni]=num[last_1];
}

void add_2(int c) // 后缀自动机加点
{
	int p,cur=++cnt;
	len[cur]=len[last_2]+1;
	for(p=last_2; p && !sam[p][c]; p=link[p])
		sam[p][c]=cur;
	if(!p) // 情况1 已经遍历到初始节点了 即该过程中没有已连接的节点
		p=1,link[cur]=1;
	else // 情况2
	{
		int q=sam[p][c];
		if(len[q]==len[p]+1) // A类 可以直接转移到cur  len[q] == len[p]+1 == minlen[cur]-1
			link[cur]=q;
		else
		{
			int cl=++cnt; // 将q节点分裂出可连向cur的新节点cl
			len[cl]=len[p]+1;
			memcpy(sam[cl], sam[q], sizeof(sam[q]));  // 原本q指向的节点新节点也要指向
			link[cl]=link[q]; // 用新节点代替节点q的位置
			while(p && sam[p][c]==q) // 将p的link链上原本连向q节点的都连向新节点
			{
				sam[p][c]=cl;
				p=link[p];
			}
			link[q]=link[cur]=cl; // cur和q的link指针都应该指向新节点
		}
	}
	sz[cur]=1; // 只考虑非克隆节点产生的贡献
	last_2=cur;
}

void calc()
{
	memset(c, 0, sizeof(c));
	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[1] : 最短子串所在的节点编号
		a[c[len[i]]--]=i;
	for(int i=cnt; i>=1; i--)
		sz[link[a[i]]]+=sz[a[i]];
	for(int i=1; i<=cnt; i++)
		fsize[a[i]]=fsize[link[a[i]]]+1ll*(len[a[i]]-len[link[a[i]]])*sz[a[i]]; // 后缀的全部子后缀数
}                                                                               // 即一个大的后缀中包含了所有小的后缀

void fun(char * ss, int addv)
{
	int p=1,now_len=0;
	for(int i=1; i<=lenlen; i++)
	{
		int c=ss[i]-'a';
		while(p && !sam[p][c])
			p=link[p];
		if(!p)
			now_len=0,p=1;
		else
		{
			now_len=min(now_len, len[p])+1;
			p=sam[p][c];
			ans+=(fsize[link[p]]+1ll*(now_len-len[link[p]])*sz[p])*(pos[i+1]+addv); // 当前匹配到的子串总数 * 中间拼接的回文串数
		}
	}
}

void print(__int128 x)
{
	if(x)
	{
		print(x/10);
		cout << (int)(x%10);
	}
}

int main()
{
	scanf("%d", &lenlen);
	scanf("%s", s+1);
	for(int i=1; i<=lenlen; i++)
		rs[i]=s[lenlen-i+1];

	init();
	for(ni=1; ni<=lenlen; ni++)  // 1 : (左 + 左端点接回文 + 右) + (左 + 右)
		add_1(rs[ni]-'a', 0); // 计算翻转串中以i为右端点的回文串数量
	for(int i=1; i<=lenlen/2; i++)
		swap(pos[i], pos[lenlen-i+1]); // 交换后变成了原串中以i作为左端点的回文串数量
	for(int i=1; i<=lenlen; i++)
		add_2(rs[i]-'a'); // 用翻转串建立sam 原串去匹配
	calc();
	fun(s, 1);

	init();
	for(ni=1; ni<=lenlen; ni++)  // 2 : 左 + 右端点接回文 + 右
		add_1(s[ni]-'a', 1); // 计算原串中以i为左端点的回文串数量
	for(int i=1; i<=lenlen/2; i++)
		swap(pos[i], pos[lenlen-i+1]); // 交换后变成了翻转串中以i作为左端点端点的回文串数量
	for(int i=1; i<=lenlen; i++)
		add_2(s[i]-'a'); // 用原串建立sam 翻转串去匹配
	calc();
	fun(rs, 0);
	print(ans);
	puts("");
	return 0;
}

下面开始进阶

用到一个能很好的和后缀自动机配合来维护endpos集合的工具:线段树合并

不懂的先去学习一下线段树合并

具体做法如下:

对每个sam节点都开一颗权值线段树(动态开点),即在sam的add函数里加这么一句话

build(root[cur], 1, lenlen, idx, 1);

cur是当前新建的节点,lenlen是字符串长度,idx是当前字符的下标

即为在cur所代表的线段树中将idx位置标记为了1 --> 标记了一个以该节点所代表的子串结尾的位置

但是这样也会引出一个和统计子串出现次数相同的问题:

abcab在位置1、7出现过

但是cab却只被标记在位置9出现过

我们先对所有sam节点按长度降序排序

然后遍历的时候调用线段树的merge函数将大的root合并到小的root即可,这样就完成了对sam的endpos集合的维护,我们想知道某个节点所代表的串有没有在某些位置出现只需要在其对应的线段树上查询即可

E-葫芦的考验之定位子串2.0_牛客竞赛字符串专题班SAM(后缀自动机简单应用)习题 (nowcoder.com)

每次给出询问l,r,L,R,问s[l,r]在s[L,R]中出现了多少次

非常经典的与线段树合并配合的应用

先把link树建出来,预处理倍增,然后线段树合并维护好endpos,对于每次查询先倍增跳到对应的节点即s[l,r]所在的节点,然后在对应的线段树上查询出现在[L+r-l, R]中的标记数量。

#include  
#include    
using namespace std;

const int N=400005,M=20000005;
char s[N];
int sam[N][26],len[N],link[N],sam_cnt,last,lenlen;
int f[N][21],rpos[500005];
int root[N],ls[M],rs[M],val[M],cnt,n;
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)
{
	val[p]=val[ls[p]]+val[rs[p]];
}

void build(int &p, int dl, int dr, int x, int v)
{
	if(!p)
		p=++cnt;
	if(dl==dr)
	{
		val[p]+=v; 
		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;
	if(tl==tr)
	{
		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 val[p];
	int mid=(tl+tr)>>1;
	int t=0;
	if(dl<=mid)
		t+=query(ls[p], tl, mid, dl, dr);
	if(dr>mid)
		t+=query(rs[p], mid+1, tr, dl, dr);
	return t;
}

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

void add(int c, int idx)
{
	int p,cur=++sam_cnt;
	build(root[cur], 1, lenlen, 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=++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)
{
	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, lenlen);
	}
}

int main()
{
	init();
	scanf("%s", s+1);
	lenlen=strlen(s+1);
	for(int i=1; i<=lenlen; i++)
		add(s[i]-'a', i),rpos[i]=last;

	for(int i=1; i<=sam_cnt; i++)
		add_edge(link[i], i);
	dfs(1, 1);
	
	int q;
	scanf("%d", &q);
	while(q--)
	{
		int l,r,L,R;
		scanf("%d%d%d%d", &l, &r, &L, &R);
		int p=rpos[r];
		int aim=r-l+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, lenlen, L+r-l, R);
		cout << ans << '\n';
	}
	return 0;
}

 

D-Typewriter_牛客竞赛字符串专题班SAM(后缀自动机简单应用)习题 (nowcoder.com)

 添加一个字符需要p的消耗,添加一个前面已打印的子串需要q消耗,求最少消耗

sam+dp的好题

#include  /// 牛客sam专题 Typewriter 打印单个字符需要p消耗 复制前面已打印的子串需要q消耗 求打印出指定字符串的最少消耗 O(n)
#include    // 维护最小的j使得s[j+1,i]在s[1,j]中出现过
#define ll long long
using namespace std;

const int N=400005;
int sam[N][26],len[N],link[N];
int cnt,last;
char s[N];
ll dp[N];

void add(int c)
{
	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=++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;
		}
	}
	last=cur;
}

int main()
{
	last=cnt=1;
	int p,q;
    scanf("%s", s+1);
    scanf("%d %d", &p, &q);
	int n=strlen(s+1);
	int now=1,l=1;
	dp[1]=p;
	add(s[now]-'a');
	for(int i=2; i<=n; i++)
	{
		int c=s[i]-'a';
		dp[i]=dp[i-1]+p;
		while((!sam[now][c] || i-l>l ) && i>l) // 当前字符无法转移 || 需要打印的字符比当前sam中的字符还多
		{
			add(s[++l]-'a');
			while(now>1 && len[link[now]]+1>=i-l) // 同下 时刻保持now节点的最优性
				now=link[now];
		}
		now=sam[now][c];
		while(now>1 && len[link[now]]>=i-l) // 若link[now]也能满足打印出s[l+1,i] 将now转移到link[now] 保证在后续判断sam[now][c]时是最优策略
			now=link[now];
		dp[i]=min(dp[i], dp[l]+q);
	}
	cout << dp[n] << '\n';
	return 0;
}

https://codeforces.com/contest/235/problem/C

查询t的所有循环同构串在文本串中的出现次数

代码:(删掉了add函数部分)

#include  /// Codeforces Round #146 (Div. 1) C 查询t的所有循环同构串在文本串中的出现次数
#include    // 循环同构 --> 复制后首尾相连  在文本串构建的sam上匹配即可
#include        // 每完成一个匹配就记录答案并将now_len-- 然后将当前遍历的节点跳link链到正确的节点
using namespace std;

const int N=2000005;
int sam[N][26],link[N],len[N],sz[N],a[N],c[N],sam_cnt,last;
char s[N];
maphah;

// 构建sam 统计节点出现次数

int main()
{
	last=sam_cnt=1;
	scanf("%s", s+1); // 输入文本串
	int lenlen=strlen(s+1);
	for(int i=1; i<=lenlen; i++)
		add(s[i]-'a');
	calc();
	int n;
	scanf("%d", &n);
	while(n--)
	{
		hah.clear();
		scanf("%s", s+1);
		lenlen=strlen(s+1);
		int aim=lenlen;
		for(int i=lenlen+1; i<=lenlen*2-1; i++)
			s[i]=s[i-lenlen];
		lenlen=lenlen*2-1;
		long long ans=0;
		int p=1,now_len=0;
		for(int i=1; i<=lenlen; i++)
		{
			int c=s[i]-'a';
			while(p && !sam[p][c])
				p=link[p],now_len=len[p];
			if(sam[p][c])
				p=sam[p][c],now_len++;
			else
				p=1,now_len=0;
			if(now_len>=aim)
			{
				while(len[link[p]]>=aim) // 此时集合p中的最短长度可能大于aim
					p=link[p];
				if(hah.find(p)==hah.end()) // 循环同构串可能相同 标记防止重复计数
					hah[p]=1,ans+=sz[p];
				now_len=aim-1;
				while(now_len && len[link[p]]>=now_len) // 删除首字符后为其找到正确的endpos集合
					p=link[p];
			}
		}
		cout << ans << '\n';
	}
	return 0;
}

https://codeforces.com/contest/700/problem/E

s[i]是原串的子串 求最长的满足s[i+1]在s[i]中出现至少两次的序列的长度 子串允许重叠

也是sam+dp

推理后可以发现答案是一条link链的形式

即在link链上做dp即可,其实这道题已经十分接近区域赛金牌题的难度了

#include  /// Codeforces Round #349 (Div. 1) E 查询s[l,r]在编号为[L,R]的串中出现最多次数及对应编号 O(n*logn)
#include    // 用所有匹配串建立广义sam 预处理主串在sam上的匹配位置rpos 每次查询都倍增找对应结点
using namespace std; // 出现次数及其编号可以用线段树维护再沿link树由子树向当前结点合并

const int N=400005,M=20000005;
char s[200005];
int sam[N][26],len[N],link[N],sam_cnt,last,a[N],c[N];
int rpos[N];
int root[N],ls[M],rs[M],val[M],cnt,n;// val[i] --> 节点i代表区间中出现最多的权值的出现次数
int dp[N],pre[N],f[N][21];

void push_up(int p)
{
	val[p]=val[ls[p]]+val[rs[p]];
}

void build(int &p, int dl, int dr, int x, int v)
{
	if(!p)
		p=++cnt;
	if(dl==dr)
	{
		val[p]|=v;
		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;
	if(tl==tr)
	{
		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 val[p];
	int mid=(tl+tr)>>1;
	int t=0;
	if(dl<=mid)
		t+=query(ls[p], tl, mid, dl, dr);
	if(dr>mid)
		t+=query(rs[p], mid+1, tr, dl, dr);
	return t;
}

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

void add(int c, int idx)
{
	int p,cur=++sam_cnt;
	rpos[cur]=idx;
	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=++sam_cnt;
			rpos[cl]=rpos[cur];
			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()
{
	init();
	scanf("%d", &n);
	scanf("%s", s+1);
	for(int i=1; i<=n; i++)
		add(s[i]-'a', i);
	calc();
	
    for(int i=sam_cnt; i>=2; i--)
        root[link[a[i]]]=merge(root[link[a[i]]], root[a[i]], 1, n);
        
    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];
	}
	int ans=1;
	for(int i=2; i<=sam_cnt; i++)
	{
		int u=link[a[i]],v=a[i];
		if(u==1)
		{
			dp[v]=1;
			pre[v]=v;
			continue;
		}
		if(query(root[pre[u]], 1, n, rpos[v]-len[v]+len[pre[u]], rpos[v])>=2)
		{
			dp[v]=dp[pre[u]]+1;
			pre[v]=v;
		}
		else
			pre[v]=pre[u];
		ans=max(ans, dp[v]);
	}
	cout << ans << '\n'; 
	return 0;
}

https://www.luogu.com.cn/problem/P4770

洛谷黑题(难度在区域赛金牌题左右)

多次询问t,l,r  计算串t不在s[l,r]中出现的本质不同子串数

思路:对文本串s建立sam并用线段树合并维护好endpos,询问串t时对串t遍历,同时在s的sam上匹配,维护好串t的每个前缀在s[l,r]中出现的最长后缀长度maxl(可能有点绕,要仔细理解),最后对t建sam,遍历每个sam节点计算其在s[l,r]中出现的本质不同子串数,注意细节问题你就解决了一道洛谷黑题。

#include  /// 洛谷 4770 多次询问t,l,r 计算串t不在s[l,r]中出现的本质不同子串数 O(|s| + sum(|t|))
#include    // 先处理串t在s[l,r]中出现的部分有哪些
using namespace std; // 求t的本质不同子串 --> 对t建sam

const int N=2000005,M=40000005;
char ss[N],s[N];
int maxl[N],rpos[N]; // maxl[i] --> t[1, i]能在s[l, r]的sam上匹配的最长后缀长度
int ls[M],rs[M],val[M],root[N],cnt;
int lenlen;

struct sam_tree
{
	int sam[N][26],len[N],link[N],sam_cnt,last;
	int a[N],c[N];
	int newnode()
	{
		++sam_cnt;
		memset(sam[sam_cnt],0,sizeof(sam[sam_cnt]));
		return sam_cnt;
	}

	void init()
	{
		memset(sam[1],0,sizeof(sam[0]));
		last=sam_cnt=1;
	}

	void add(int c, int idx)
	{
		int p,cur=newnode();
		len[cur]=len[last]+1;
		rpos[cur]=idx;
		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) // wrong 3: 模板也写错了
				link[cur]=q;
			else
			{
				int cl=++sam_cnt;
				memcpy(sam[cl],sam[q],sizeof(sam[q]));
				rpos[cl]=rpos[cur];
				len[cl]=len[p]+1;
				link[cl]=link[q];
				while(p && sam[p][c]==q)
				{
					sam[p][c]=cl;
					p=link[p];
				}
				link[q]=link[cur]=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;
	}
}S,T;

void push_up(int p) // 处理sam节点所有endpos的时间复杂度是O(n^2) 因此只能采用线段树O(logn)维护
{
	val[p]=val[ls[p]]|val[rs[p]];
}

void build(int &p, int dl, int dr, int x, int v)
{
	if(!p)
		p=++cnt;
	if(dl==dr)
	{
		val[p]|=v;
		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;
	if(tl==tr)
	{
		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); // wrong 1: rs[x], rs[x] ???
	push_up(now);
	return now;
}

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

bool check(int p, int l, int r, int dd) // 判断目标结点是否在l,r的区间内
{
	if(!p)
		return false;
	return query(root[p], 1, lenlen, l+dd-1, r);
}

int main()
{
	scanf("%s", s+1);
	lenlen=strlen(s+1);
	S.init();
	for(int i=1; i<=lenlen; i++)
		S.add(s[i]-'a', i),build(root[S.last], 1, lenlen, i, 1);
	S.calc();
	for(int i=S.sam_cnt; i>=2; i--) // 每次合并O(logn) 总时间复杂度O(n*logn)
		root[S.link[S.a[i]]]=merge(root[S.link[S.a[i]]], root[S.a[i]], 1, lenlen); // wrong 2: 合并后的结点没有给赋值给root[link[a[i]]]
	int q;
	scanf("%d", &q);
	while(q--)
	{
		int l,r;
		scanf("%s", ss+1);
		scanf("%d%d", &l, &r);
		int sn=strlen(ss+1);
		int now_len=0,p=1;
		for(int i=1; i<=sn; i++)
		{
			int c=ss[i]-'a';
			while(p && !check(S.sam[p][c], l, r, now_len+1)) // 能匹配的最大长度可能介于len[link[p]]~len[p]之间
			{                //                                 l<------------>r
				now_len--;   //                                      *       *             '*'为endpos出现的位置
				if(now_len<=S.len[S.link[p]]) //                [<-  len  ->][ ]
					p=S.link[p],now_len=S.len[S.link[p]]; //    [<-llen->][    ]
//				p=S.link[p],now_len=S.len[p]; //                [<- maxl ->][  ]
			}
			if(check(S.sam[p][c], l, r, now_len+1))
				p=S.sam[p][c],now_len++;
			else
				p=1,now_len=0;
			maxl[i]=now_len;
		}
		T.init();
		for(int i=1; i<=sn; i++)
			T.add(ss[i]-'a', i);
		long long ans=0;
//		for(int i=2; i<=T.sam_cnt; i++) // 直接计算不在s[l,r]中出现的子串数
//			ans+=max(0, T.len[i]-max(T.len[T.link[i]], maxl[rpos[i]]));
		long long res=0;
		for(int i=2; i<=T.sam_cnt; i++)
			res+=T.len[i]-T.len[T.link[i]];
		for(int i=2; i<=T.sam_cnt; i++) // 计算本质不同公共子串数
			ans+=max(0, min(T.len[i], maxl[rpos[i]])-T.len[T.link[i]]);
		cout << res-ans << '\n';
	}
	return 0;
}

https://codeforces.com/contest/1063/problem/F

求满足s[i]是s[i-1]的真子串的有序串组的最大长度(最大子串个数)

有序串组 : s[1],s[2]...s[k] 是原串中的不相交有序序列

真子串 : t是s的子串但t!=s

例如:abcdeabcbcc --> abcd(e) abc bc c --> ans==4

熟悉的sam+dp,与上上道题有点像,但此处要求的是真子串,因此做法也完全不同

考虑将原串翻转,题目转变为求s[i-1]是s[i]的真子串的最大长度

观察可得最优选择下有序串组的长度一定可以是1,2,3,...k

令dp[i]为以s[i+dp[i]-1, i]作为有序串组的最后一个子串的最大有序串组长度

那么 dp[i]=max(dp[j]+1) (s[j+dp[j]-1, j]==s[i+dp[i]+2, i] 或 dp[j+dp[j]-1, j]==dp[i+dp[i]+1, i-1])

如果我们要判断一个dp[i]的值是否合法,就需要在s[1, i-dp[i]]里找一个dp[j]==dp[i]-1且满足上述条件

令字符串p1=s[i+dp[i]+2, i],p2=s[i+dp[i]+1, i-1]

其实就是要找到以p1或p2为后缀的子串的endpos(结尾下标)集合中的最大dp值

思考一下为什么,我们要找的是能转移给dp[i]的dp[j],其中dp[j]=dp[i]-1,也就是j位置的子串长度为i位置的子串长度-1,因此只能是p1或p2,其次考虑dp[j]的值是否满足条件,即我们需要p1或者p2能作为长度为dp[i]-1的有序串组的结尾串,因此每次我们把dp[r]的值赋给s[1,r]对应串所在的sam节点,对于p1,p2的dp值我们只需要在link树上查询将其作为后缀的节点的最大dp值,若这个最大值满足>=dp[i]-1,那么我们就一定可以找到一个方案使得dp[i]成立,这里我们用到线段树维护一下子树最大值即可。

那么现在已经有了判断dp[i]的值是否合法的方法,我们来考虑如何给dp[i]定值

首先dp[i]<=dp[i-1]+1,考虑逐渐缩小dp[i]值的大小来使其合法,由于dp值的总增量<=n,因此这一步的时间复杂度是均摊O(1)的,接着来考虑当前子串的左端点i-dp[i]

由于dp[i]<=dp[i-1]+1 --> dp[i+1]<=dp[i]+1 --> (i+1)-dp[i+1]=i-dp[i+1]+1>=i-(dp[i]+1)+1=i-dp[i]

可以得i-dp[i]随i的增大单调不减,令r=i-dp[i]-1,在每次扩大r(即当前dp[i]非法需要减小)的时候都把dp[r]拿去更新s[1, r]对应的sam节点即可,这样就保证了每次dp转移的合法性。

总复杂度O(n * logn)

难度为区域赛金牌

细节写在注释里了

#include  /// Codeforces Round #516 F 满足s[i]是s[i-1]的真子串的有序串组的最大长度 O(n*logn)
#include    // 有序串组 : s[1],s[2]...s[k] 是原串中的不相交有序序列  真子串 : t是s的子串但t!=s
using namespace std; // abcdeabcbcc --> abcd(e) abc bc c --> ans==4
                     // 每个子串的出现位置可能有多处 因此无法只靠枚举sam节点来转移dp
const int N=1000005; // 最优选择下的有序串组的长度一定是 1 2 3...k
int sam[N][26],link[N],len[N],rpos[N],last,sam_cnt,n;
char s[N];
int dp[N]; // dp[i] --> 以s[i-dp[i]+1, i]结尾的有序串组的最大长度
int f[N][21];
int tree[N<<2];
struct pp
{
	int to;
	int old;
}edge[N];
int newly[N],cnt;
int in[N],out[N],dfn_cnt;

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

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

void add(int c, int idx)
{
	int p,cur=++sam_cnt;
	len[cur]=len[last]+1;
	rpos[idx]=cur;
	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[q]=link[cur]=cl;
		}
	}
	last=cur;
}

void dfs(int x, int fa)
{
	f[x][0]=fa;
	for(int i=1; i<=19; i++)
		f[x][i]=f[f[x][i-1]][i-1];

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

void modify(int p, int dl, int dr, int x, int v) // 单点修改 子树查询
{
	if(dl==dr)
	{
		tree[p]=v;
		return ;
	}
	int mid=(dl+dr)>>1;
	if(x<=mid)
		modify(p<<1, dl, mid, x, v);
	else
		modify(p<<1|1, mid+1, dr, x, v);
	tree[p]=max(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=(tr+tl)>>1;
	int t=0;
	if(dl<=mid)
		t=query(p<<1, tl, mid, dl, dr);
	if(dr>mid)
		t=max(t, query(p<<1|1, mid+1, tr, dl, dr));
	return t;
}

int get(int x, int aim) // 得到s[x-aim+1, x]所在的节点
{
	int p=rpos[x];
	for(int i=19; i>=0; i--)
		if(f[p][i] && len[f[p][i]]>=aim)
			p=f[p][i];
	return p;
}

bool check(int x) // * 节点p的dp值一定是由p代表的子串删去首字符或尾字符对应串的dp值转移而来
{   // --> 转移状态为以处理后的子串节点作为后缀的串的endpos集合
	int p1=get(x, dp[x]-1),p2=get(x-1, dp[x]-1); // 倍增找到去掉首字符/尾字符的节点
	return max(query(1, 1, sam_cnt, in[p1], out[p1]), query(1, 1, sam_cnt, in[p2], out[p2]))>=dp[x]-1;
} // 判断[1,r]中是否有p1或p2出现过且dp[endpos(p1/p2)]>=dp[x] 即可以转移到x --> 查询p1或p2的link子树中的dp最大值

int main()
{
	init();
	scanf("%d", &n);
	scanf("%s", s+1);
	for(int i=1; i<=n/2; i++) // 转化为子串长度递增的有序串组
		swap(s[i], s[n-i+1]);
	for(int i=1; i<=n; i++)
		add(s[i]-'a', i);

	for(int i=2; i<=sam_cnt; i++) // 处理倍增和dfs序
		add_edge(link[i], i);
	dfs(1, 1);

	int r=0; // 可转移区间的右端点  r = i-dp[i]
	int ans=0;
	for(int i=1; i<=n; i++) // 原本要对dp[i]的值进行二分处理 但考虑到 i-dp[i]具有单调性 dp[1~n]的总缩减次数不超过n 因此可对其采用线性优化
	{
		dp[i]=dp[i-1]+1;  // 给定dp[i]的最大值 判断缩小dp[i]的值使其合法  dp[i]<=dp[i-1]+1 <==> dp[i+1]<=dp[i]+1
		while(!check(i))  // 判断是否有[1, i-dp[i]]中的状态能转移到 dp[i] (dp[j]+1 == dp[i])
		{                 // 显然在当前状态 i-dp[i] 是单调不减的 且下一个状态 (i+1)-dp[i+1]>=(i+1)-(dp[i]+1)==i-dp[i]
			dp[i]--;      // 即 下一状态的r >= 当前状态的r
			r++;          // 因此维护r为当前状态可选范围的右端点即可
			modify(1, 1, sam_cnt, in[rpos[r]], dp[r]); // 将新的右端点dp值加入可选择范围
		}                                              // 注意节点rpos[r]代表的串是s[1,r] 因此查询时只需通过子树查询即可
		ans=max(ans, dp[i]);
	}
	cout << ans << '\n';
	return 0;
}

https://codeforces.com/contest/1037/problem/H

每次询问从s[l,r]中找到字典序比t串大的子串 要求答案串的字典序尽可能小

后缀自动机+线段树合并常规题,但是cf上Rating竟然标到了3200。

我们可以尝试从sam中构造串t同时记录能否找到更大的字符代替当前位置 线段树合并维护endpos判断是否在[l,r]即可。

#include 
#include 
using namespace std;

const int N=400005,M=20000005;
int sam[N][26],link[N],len[N],a[N],c[N],lenlen,last,sam_cnt;
int root[N],ls[M],rs[M],val[M],cnt;
char s[N];
int yy;

void add(int c)
{
	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;
			link[cl]=link[q];
			memcpy(sam[cl], sam[q], sizeof(sam[q]));
			while(p && sam[p][c]==q)
			{
				sam[p][c]=cl;
				p=link[p];
			}
			link[cur]=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;
}

void push_up(int p)
{
	val[p]=val[ls[p]]|val[rs[p]];
}

void build(int &p, int dl, int dr, int x, int v)
{
	if(!p)
		p=++cnt;
	if(dl==dr)
	{
		val[p]|=v;
		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;
	if(tl==tr)
	{
		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 val[p];
	int mid=(tl+tr)>>1;
	int t=0;
	if(dl<=mid)
		t|=query(ls[p], tl, mid, dl, dr);
	if(dr>mid)
		t|=query(rs[p], mid+1, tr, dl, dr);
	return t;
}

bool check(int p, int l, int r, int vv)
{
	if(!p)
		return false;
	return query(root[p], 1, lenlen, l+vv-1, r);
}

int main()
{
	sam_cnt=last=1;
	scanf("%s", s+1);
	lenlen=strlen(s+1);
	for(int i=1; i<=lenlen; i++)
		add(s[i]-'a'),build(root[last], 1, lenlen, i, 1);
	calc();
	for(int i=sam_cnt; i>=2; i--)
		root[link[a[i]]]=merge(root[link[a[i]]], root[a[i]], 1, lenlen);
	
	int q;
	scanf("%d", &q);
	while(q--)
	{
		int l,r;
		scanf("%d%d", &l, &r);
		scanf("%s", s+1);
		int sn=strlen(s+1),p=1,pre=-1,preid=-1;
		int yy=1;
		while(yy<=sn)
		{
			int c=s[yy]-'a';
			if(check(sam[p][c], l, r, yy)) // 存在该字符 
			{
				for(int j=c+1; j<26; j++)
				{
					if(check(sam[p][j], l, r, yy))
					{
						pre=j;
						preid=yy;
						break;
					}
				}
				p=sam[p][c];
			}
			else // 没有该字符 
			{
				for(int j=c+1; j<26; j++) // 判断是否能找到更大的 
					if(check(sam[p][j], l, r, yy))
					{
						pre=j;
						preid=yy;
						break;
					}
				break;
			}
			yy++;
		}
		if(yy>sn) // 全部字符都找到
		{
			for(int j=0; j<26; j++)
				if(check(sam[p][j], l, r, yy))
				{
					pre=j;
					preid=sn+1;
					break;
				}
			if(~pre)
			{
				for(int i=1; i

https://ac.nowcoder.com/acm/contest/32708/J

第四十六届icpc昆明站J题,这道题在赛上是属于金牌题的行列

集合T由两两不为后缀的s的子串组成 每次询问s[1, len]的所有集合中最大的 前k大串长 的权值和

两两不为后缀

--> 在link树中其中一个不是另一个的祖先

--> 由于给出的权值是按长度递增的,因此优先考虑串长最大的节点

--> 只考虑link树叶子节点,所有的叶子节点都不互为后缀,且长度是最大的  

每次询问s[1,L]直接按L排序离线即可,具体过程就是线段树维护前k大的len对应的权值和

#include 
#include 
#include 
using namespace std;

const int N=200005;
int v[N],sam[N*2][26],link[N*2],len[N*2],sam_cnt,last,n;
int ee[N*2];
char s[N];
long long tree[N*4],gg;
int cnt[N*4]; // cnt[i] --> 存储对应长度的字符串的数量
long long res;
struct pp
{
	int l;
	int k;
	int idx;
}qq[N];
long long ans[N];

void push_up(int p)
{
	tree[p]=tree[p<<1]+tree[p<<1|1];
	cnt[p]=cnt[p<<1]+cnt[p<<1|1];
}

void modify(int p, int tl, int tr, int x, int vv)
{
	if(tl==tr)
	{
		cnt[p]+=vv; 
		tree[p]+=v[x]*vv;
		return ;
	}
	int mid=(tl+tr)>>1;
	if(x<=mid)
		modify(p<<1, tl, mid, x, vv);
	else
		modify(p<<1|1, mid+1, tr, x, vv);
	push_up(p);
}

int query(int p, int tl, int tr, int k)
{
	if(tl==tr)
	{
		gg+=tree[p];
		return tl;
	}
	int val=cnt[p<<1|1];
	int mid=(tl+tr)>>1;
	if(val>=k)
		return query(p<<1|1, mid+1, tr, k);
	gg+=tree[p<<1|1];
	return query(p<<1, tl, mid, k-val);
}

void init()
{
	last=sam_cnt=1;
	memset(tree,0,sizeof(tree));
	memset(cnt,0,sizeof(cnt));
}

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

bool cmp(struct pp a, struct pp b)
{
	return a.l

Problem - F - Codefor​​​​​​ces​​​​​​

计算有一个位置变为特殊字符后该串的本质不同子串数,对每个位置计算答案总和

令f(x) --> x的本质不同子串数

对于串"abcd", ans=  f(abcd)+f(*bcd)+ f(a*cd)+ f(ab*d)+ f(abc*)

即为计算 S, *T, S*, S*T 形式的本质不同子串数

对于前三种情况,我们在对其建sam的时候直接统计本质不同子串数即可

对于 S*T: 考虑枚举串集合S,对于答案的贡献即为(len[s]-len[link[s]])*(以S的结尾位置+2 作为开头的本质不同子串数),当集合S在原串中只出现过一次时后者的数值就等于以该位置开头的后缀长度

当集合S出现多次时我们需要考虑去除掉这些后缀中的重复串,一个后缀所贡献的子串都是以同一个位置作为开头的,因此我们可以对原串的翻转串再建一个sam,问题就转变为了一个正串的sam节点对应了反串的sam的link树上的多条链,然后要将这些链取一个链并,最后这些链并的节点的本质不同子串数之和就是后者的数值

考虑用线段树维护sam上每一个节点能够拼接上的本质不同T串数

那么就需要维护多条链的并集,其实也就是树并,在处理树并时常用的方法就是先找出节点dfs序最大值较小的那棵树的最大dfs序对应节点x,以及另一棵树最小的dfs序对应节点y,只需要去除lca(x, y)到根节点的这条链的贡献即可(如果这条链有过贡献的话)

用线段树合并去维护每一个sam节点对应的反串的link树的dfs序

合并时长这样

void push_up(int p)// 合并节点 减去中间重合的子串
{
	val[p]=val[ls[p]]+val[rs[p]]-RS.len[lca(pr[ls[p]], pl[rs[p]])]; // 两个区间中dfs序相近的节点的lca到根所代表的串就是公共串
	pl[p]=pl[ls[p] ? ls[p]:rs[p]];
	pr[p]=pr[rs[p] ? rs[p]:ls[p]];
}

代码:

#include  /// Codeforces Round #606 F
#include 
using namespace std;
         
const int N=200005,M=6000005;
char s[N];
int root[N],ls[M],rs[M],pl[M],pr[M],cnt; // pl[i] --> 当前区间dfs序最小的sam编号  pr[i] --> dfs序最大的sam编号
long long val[M];         // val[i] --> 节点i代表的本质不同T串数 
struct pp
{
	int to;
	int old;
}edge[N];
int newly[N],edge_cnt;
int dfn[N],dfn_cnt;
int f[N][22],dep[N],lg[N];

struct sam
{
	int sam[N][26],link[N],len[N],a[N],c[N],sam_cnt,last;
	int pos[N]; // pos[i] --> s[1, i]对应的sam节点
	void init()
	{
		sam_cnt=last=1;
	}
	void add(int c)
	{
		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;
				link[cl]=link[q];
				memcpy(sam[cl], sam[q], sizeof(sam[q]));
				while(p && sam[p][c]==q)
				{
					sam[p][c]=cl;
					p=link[p];
				}
				link[cur]=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;
	}
}S,RS;

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

void dfs(int x, int fa)
{
	f[x][0]=fa,dep[x]=dep[fa]+1;
	for(int i=1; i<=lg[dep[x]]; i++)
		f[x][i]=f[f[x][i-1]][i-1];
	dfn[x]=++dfn_cnt;
	for(int i=newly[x]; ~i; i=edge[i].old)
		dfs(edge[i].to, x);
}

int lca(int x, int y)
{
	if(dep[x]dep[y])
		x=f[x][lg[dep[x]-dep[y]]-1];
	if(x==y)
		return x;
	for(int k=lg[dep[x]]-1; k>=0; k--)
		if(f[x][k]!=f[y][k])
			x=f[x][k],y=f[y][k];
	return f[x][0];
}

void push_up(int p)// 合并节点 减去中间重合的子串
{
	val[p]=val[ls[p]]+val[rs[p]]-RS.len[lca(pr[ls[p]], pl[rs[p]])]; // 两个区间中dfs序相近的节点的lca到根所代表的串就是公共串
	pl[p]=pl[ls[p] ? ls[p]:rs[p]];
	pr[p]=pr[rs[p] ? rs[p]:ls[p]];
}

void build(int &p, int dl, int dr, int x)
{
	if(!p)
		p=++cnt;
	if(dl==dr)
	{
		val[p]=RS.len[x];
		pl[p]=x,pr[p]=x;
		return ;
	}
	int mid=(dl+dr)>>1;
	if(dfn[x]<=mid)
		build(ls[p], dl, mid, x);
	else
		build(rs[p], mid+1, dr, x);
	push_up(p);
}

int merge(int x, int y, int tl, int tr)
{
	if(!x || !y)
		return x|y;
	int now=++cnt;
	if(tl==tr)
	{
		val[now]=val[x] ? val[x] : val[y];
		pl[now]=pl[x] ? pl[x] : pl[y];
		pr[now]=pr[x] ? pr[x] : pr[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 main()
{
	S.init(),RS.init();
	memset(newly,-1,sizeof(newly));
	for(int i=1; i>1]+1;
	long long ans=0;
	scanf("%s", s+1);
	int n=strlen(s+1);

	for(int i=1; i1; i--) // 反串sam
		RS.add(s[i]-'a'),RS.pos[i]=RS.last;
	for(int i=1; i<=RS.sam_cnt; i++) // *T
		ans+=RS.len[i]-RS.len[RS.link[i]];
	RS.add(s[1]-'a'),RS.pos[1]=RS.last;

	for(int i=2; i<=RS.sam_cnt; i++) // 建立RS的link树  计算dfs序 倍增lca
		add_edge(RS.link[i], i);
	dfs(1, 0);

	for(int i=1; i<=n-2; i++) // 节点pos[i]所能匹配的本质不同T串数  对于每个前缀s[1, i]都匹配了一条链 最终结果就是求链并
		build(root[S.pos[i]], 1, dfn_cnt, RS.pos[i+2]);
	S.calc(); // 计数排序
	for(int i=S.sam_cnt; i>1; i--) // 按长度从大到小合并线段树
		root[S.link[S.a[i]]]=merge(root[S.link[S.a[i]]], root[S.a[i]], 1, dfn_cnt);

	for(int i=2; i<=S.sam_cnt; i++) // S*T
		ans+=(S.len[i]-S.len[S.link[i]])*val[root[i]]; // 每一个本质不同的S * 可以接上的本质不同的T的数量
	cout << ans+2 << '\n'; // 加上 * 和空串
	return 0;
}

P6292 区间本质不同子串个数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

每次询问s[l,r]中的本质不同子串数

首先离线询问按r升序,然后依次添加字符,询问左端点落在[l,r]里的本质不同子串数量之和

如何计算?

考虑当前区间是[1,x],此时添加进来字符s[x+1],其实就相当于加入了一条link链进来

我们先不考虑本质不同这个条件,那么新产生的贡献就是将区间[1,x+1]都加上1

其中发生重复的部分其实就是在当前sam节点的link链中在之前已经出现过的节点所代表的串

我们每次都对新产生的链打上标记,对于之前存在旧标记的节点直接删除其贡献,最后加上新链的贡献,然后我们就解决了这道题

对于这一步的复杂度每次都是O(n)的

此时我们考虑一种可以很好地维护树链信息的数据结构--LCT(Linked-Cut-Tree)

LCT可以每次都在O(log n)的时间复杂度内帮我们维护好树链信息

因此加上LCT我们这道题就写完了

#include  
#include 
#include  
using namespace std;

const int N=1000005;
int sam[N][26],link[N],len[N],pos[N],sam_cnt,last;
char s[N];
struct pp
{
	int l,r,idx;
}a[N];
long long ans[N];
int n;

bool cmp(struct pp a, struct pp b)
{
	return a.r=dl && r<=dr) // 当前区间被目标区间完全包含
	{
		tree[i].add+=dis;
		tree[i].sum+=dis*(r-l+1);
		return ;
	}
	push_down(i, r-l+1); // 向下传递标记
	int mid=(l+r)>>1;
	if(dl<=mid)
		modify(i<<1, l, mid, dl, dr, dis);
	if(dr>mid)
		modify(i<<1|1, mid+1, r, dl, dr, dis);
	push_up(i);
}

long long query(int i, int l, int r, int dl, int dr)
{
	if(l>=dl && r<=dr) // 被目标区间包含 立即返回答案
		return tree[i].sum;
	push_down(i, r-l+1);
	int mid=(l+r)>>1;
	long long t=0;
	if(dl<=mid)
		t+=query(i<<1, l, mid, dl, dr);
	if(dr>mid)
		t+=query(i<<1|1, mid+1, r, dl, dr);
	return t;
}

void add(int c)
{
	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[q]=link[cur]=cl;
		}
	}
	last=cur;
}

struct LCT
{
    struct node // 原树跟辅助树的结构并不相同(原树是虚树结构)
    {
        int s[2], fa; // 所有节点都有可能"儿子认爹,爹不认儿子"
        int sum; // 这一条链(splay)上的本质不同子串数 
        int val;
        int tag;
        int col;
    }tr[N];

    int ss[N], top;

    void pushup(int u)  // 维护每一个splay中的信息
    {
        tr[u].sum=tr[tr[u].s[0]].sum + tr[u].val + tr[tr[u].s[1]].sum;
    }

    void pushdown(int u)
    {
        if(tr[u].tag)
        {
            swap(tr[u].s[1], tr[u].s[0]);
            tr[u].tag=0;
            tr[tr[u].s[1]].tag^=1;
            tr[tr[u].s[0]].tag^=1;
        }
        if(tr[u].col)
        {
        	tr[tr[u].s[0]].col=tr[u].col;
			tr[tr[u].s[1]].col=tr[u].col;
		}
    }

    bool isroot(int u) // 判断u是不是当前伸展树?原树的根节点
	{
        return u != tr[tr[u].fa].s[0] && u != tr[tr[u].fa].s[1];
    }

    void rotate(int x) // 旋转x
	{
        int y=tr[x].fa, z=tr[y].fa;
	    int t1=(x == tr[y].s[1]);
	    int t2=(y == tr[z].s[1]);
        int ch=tr[x].s[t1^1];
	    tr[ch].fa=y;
	    if(!isroot(y))
			tr[z].s[t2^0]=x;
	    tr[x].fa=z;
        tr[x].s[t1^1]=y;
        tr[y].fa=x;
        tr[y].s[t1^0]=ch;
	    pushup(y);
    }

    void splay(int x) // 将x作为当前伸展树的根节点
	{
        ss[++top]=x;
        for(int i=x; !isroot(i); i=tr[i].fa)
            ss[++top]=tr[i].fa;
        while(top)
            pushdown(ss[top--]);
        while(!isroot(x))
		{
            int y=tr[x].fa, z=tr[y].fa;
            if(!isroot(y))
			{
                if((x == tr[y].s[0]) ^ (y == tr[z].s[0]))
					rotate(x);
                else
					rotate(y);
            }
            rotate(x);
        }
        pushup(x);
    }

    int access(int x, int color)  // x与原来的splay断开 连接到根节点所在的splay上(打通到根节点的路径)
    {                 // 两次返回值就是LCA(x1,x2)
        int t=0;
        while(x)
        {
            splay(x); // 先将x提到当前splay的根节点
            tr[x].s[1]=t;
            pushup(x);

            if(tr[x].col) // 在前面出现过
                modify(1, 1, n, tr[x].col-len[x]+1, tr[x].col-len[tr[x].fa], -1); // 去除这段链的贡献  注意tr[x].fa而不是link[x]
                                                                              // 因为link[x]可能已经被统计到tr[x]的左子树里了
                               // 注意此处为-len[x]不会与其link节点产生覆盖
            t=x,x=tr[x].fa;        // 是因为当前节点所代表的splay中包含了其link链上所有被染成同色的部分 因此不会重复枚举
        }
        modify(1, 1, n, 1, color, 1);  // 记录贡献 以1~color为左端点的本质不同子串数+1
        tr[t].col=color; // color为当前添加的idx
        return t;
    } // 注意本质不同子串数不是O(n)的 只有sam节点数是O(n) 统计答案要用long long
    
    int access2(int x)  // x与原来的splay断开 连接到根节点所在的splay上(打通到根节点的路径)
	{                 // 两次返回值就是LCA(x1,x2)
        int t=0;
        while(x)
		{
            splay(x); // 先将x提到当前splay的根节点
            tr[x].s[1]=t;
            pushup(x);
            t=x;
            x=tr[x].fa;
        }
        return t;
    }
    
    void makeroot(int x) // 使当前x成为原树的根
    {
        access2(x); // 注意这里使用的是正常的access函数
        splay(x);
        tr[x].tag^=1; // 翻转后调换了splay的深度关系 这样x就成为了原树的根节点
    }	

    void link(int u, int v) // u --> v
    {
        makeroot(v);
        tr[v].fa=u;
    }
}T;

signed main()
{
	last=sam_cnt=1;
	int q;
	scanf("%s", s+1);
	n=strlen(s+1);
	for(int i=1; i<=n; i++)
		add(s[i]-'a'),pos[i]=last;
	for(int i=2; i<=sam_cnt; i++)
	{
		T.tr[i].val=len[i]-len[link[i]];
        T.link(link[i], i);
	}
	scanf("%d", &q);
	for(int i=1; i<=q; i++)
		scanf("%d%d",  &a[i].l, &a[i].r),a[i].idx=i;
	sort(a+1, a+1+q, cmp);
	int now=1;
	for(int i=1; i<=q; i++)
	{
		while(now<=a[i].r)
		{
			T.access(pos[now], now);
			now++;
		}
		ans[a[i].idx]=query(1, 1, n, a[i].l, a[i].r);
	}
	for(int i=1; i<=q; i++)
		cout << ans[i] << '\n';
	return 0;
}

刷完这些题大概初步具备了解决后缀自动机区域赛金牌/银牌题的能力

你可能感兴趣的:(字符串算法,算法)