SPOJ Substrings 题解

\(\text {SAM}\) 入门好题。

首先我们需要知道几个关于 \(\text{SAM}\) 的结论。

结论 1:题目中的 \(f(x)\) 单调下降。

显然,对于长度为 \(x\) 的子串,其必存在一个 \(x-1\) 的后缀,这个后缀的 \(\text {endpos}\) 集合肯定包含子串的 \(\text {endpos}\) 集合,所以必有 \(f(x-1)\leq f(x)\)

结论 2:两个 \(\text {endpos}\) 集合要么是包含关系,要么是非交的,且取决于一个字符串是否是另一个字符串的后缀。

如果字符串 \(u\) 包含 \(v\),则 \(u\) 可匹配的位置 \(v\) 一定可以匹配。

否则,两者结尾不同有字符,故对于 \(u\) 可以匹配的每一个终点\(v\) 都不可以匹配。

结论 3:一个节点有一个终点当且仅当它的子树包含这个终点对应的主链的节点,一个子串在字符串中的出现次数为其子树中主链的节点的数量。

在这里,主链表示字符串前缀所在的终点等价类所代表的节点。

每个终点都有一个对应的主链的节点。

必要性:如果这个节点包含这个终点,根据结论 2,其必然包含主链上节点的 \(\text {endpos}\) 集合,故包含这个节点。

充分性:如果这个节点包含这个终点,但是不包含主链上的对应节点,则两者集合有交,与结论 2 矛盾,所以如果这个节点包含这个终点则必然包含主链上的对应节点。


那么知道了这些结论,我们该怎么做题呢?

发现了没有,对于一个终点等价类的子串,它们显然是有共同的出现次数的,而这一次数可以利用结论 4 求出。

所以我们考虑用一个拓扑来做一个树形 DP,求出之后,我们在节点的 \(\text {len}\) 对应的位置打标记记录答案,求一个后缀最大值即可。

为什么可以直接后缀最大值呢?参见结论 1。

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const LL N=1e6+5;
struct node
{
	LL len,fa,num,du;
	map<char,LL>nxt;
}t[N];
LL n,a[N],lst,sz,f[N];
char c[N];
queue<LL>q;
void ins(char x)
{
	LL now=++sz;
	t[now].len=t[lst].len+1;
	t[now].num=1;
	LL p=lst;
	lst=now;
	while(p!=-1&&!t[p].nxt.count(x))
	{
		t[p].nxt[x]=now;
		p=t[p].fa;
	}
	if(p==-1)return;
	LL q=t[p].nxt[x];
	if(t[p].len+1==t[q].len)
	{
		t[now].fa=q;
		return;
	}
	LL cl=++sz;
	t[cl]=t[q],t[cl].num=0,t[cl].len=t[p].len+1;
	while(p!=-1&&t[p].nxt[x]==q)
	{
		t[p].nxt[x]=cl; 
		p=t[p].fa;
	}
	t[q].fa=t[now].fa=cl;
}
vector<char>v;
int main()
{
	t[0].fa=-1;
	scanf("%s",c+1);
	n=strlen(c+1);
	for(int i=1;i<=n;i++)ins(c[i]);
	for(int i=1;i<=sz;i++)
	{
		t[t[i].fa].du++;
	}
	for(int i=1;i<=sz;i++)
	{
		if(!t[i].du)q.push(i);
	}
	while(!q.empty())
	{
		LL k=q.front();
		q.pop();
		t[t[k].fa].num+=t[k].num;
		t[t[k].fa].du--;
		if(!t[t[k].fa].du)
		{
			q.push(t[k].fa);
		}
	}
	for(int i=1;i<=sz;i++)
	{
		f[t[i].len]=max(f[t[i].len],t[i].num);
	}
	for(int i=n-1;i>=1;i--)
	{
		f[i]=max(f[i],f[i+1]);
	}
	for(int i=1;i<=n;i++)
	{
		printf("%lld\n",f[i]);
	}
	return 0;
}
posted @ 2023-07-05 10:57  DengDuck  阅读(16)  评论(0编辑  收藏  举报