1. 程式人生 > 其它 >【洛谷】P3975 [TJOI2015]弦論(SAM)

【洛谷】P3975 [TJOI2015]弦論(SAM)

原題連結

題意

對於一個給定長度為 \(N\) 的字串,求它的第 \(K\) 小子串。

資料範圍

\(1\leq n \leq 10^5\)\(0\leq t \leq 1\)\(1\leq k \leq 10^9\)

\(t\)\(0\) 則表示不同位置的相同子串算作一個,\(t\)\(1\) 則表示不同位置的相同子串算作多個。

思路

考慮字尾自動機(下文預設已經知曉 SAM 的基本性質)。

根據題意,當 \(t=0\) 時,SAM 上的每個節點的 \(Endpos\) 集合大小(該集合代表該節點對應的所有子串在原串中所有出現的位置)都為 \(1\)。反之則可以根據 parent 樹的性質從下往上遞推。定義 \(i\)

點的 \(Endpos\) 集合大小為 \(siz[i]\)

定義 \(sum[i]\) 表示經過 \(i\) 節點的子串數目(嚴格來說並不是,往後看就明白了)。即當前節點的 \(siz\) 加上以當它為字首的所有節點的 \(siz\)。可以在 SAM 的 DAG 上逆序遞推。需要注意的是,這裡不需要考慮 \(i\) 節點對應的所有子串,因為接下來查詢的時候根據字典序的性質是從前往後推的,也就是一個字首不斷往後擴充套件,而不是字尾往前擴充套件(舉個簡單的例子,當前節點對應的子串集合為 {\(aabb,abb,bb\)},那麼後面查詢到這裡時可能是從 \(aab\) 過來的,也可能是從 \(b\)

過來的,此時就與對應的其他子串無關了)。

查詢的時候就是從根節點往後推,根據當前節點的 \(siz\)\(sum\) 判斷是否要繼續往後推,具體實現可以見程式碼,這部分放在程式碼裡將更清晰。

code

#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e6+10;
#define LL long long
struct SAM{int ch[26],len,fa;}node[N];
int c[N],last=1,tot=1,t,k,num[N];LL siz[N],sum[N]; char s[N];
void extend(int c)
{
	int p=last,np=last=++tot;node[np].len=node[p].len+1;siz[np]=1;
	while(p&&!node[p].ch[c]) node[p].ch[c]=np,p=node[p].fa;
	if(!p) node[np].fa=1;
	else
	{
		int q=node[p].ch[c];
		if(node[p].len+1==node[q].len) node[np].fa=q;
		else
		{
			int nq=++tot;node[nq]=node[q];node[nq].len=node[p].len+1;
			node[q].fa=node[np].fa=nq;
			while(p&&node[p].ch[c]==q) node[p].ch[c]=nq,p=node[p].fa;
		}
	}
}
void print(int now,int k) //k表示當前還要查詢的第k小子串 
{
	if(k<=siz[now]) return ; //如果當前節點出現次數不小於k,就代表查詢子串就是當前已經走到的子串,不需要往後推 
	k-=siz[now];
	for(int i=0;i<26;i++) //根據字典序的性質, 從小到大列舉當前位字元 
	{
		int t=node[now].ch[i];if(!t) continue;
		if(sum[t]<k) k-=sum[t];//有點類似於splay查詢右子樹時減去左子樹的size 
		else {printf("%c",i+'a');print(t,k);return ;}
	}
}
int main()
{
	scanf("%s%d%d",s+1,&t,&k);for(int i=1;s[i];i++) extend(s[i]-'a');
	for(int i=1;i<=tot;i++) c[node[i].len]++;//求siz和sum陣列可以按照這裡的基數排序的方式,也可以直接建圖dfs 
	for(int i=1;i<=tot;i++) c[i]+=c[i-1];
	for(int i=1;i<=tot;i++) num[c[node[i].len]--]=i;
	for(int i=tot;i>=1;i--) siz[node[num[i]].fa]+=siz[num[i]];
	for(int i=1;i<=tot;i++) t?sum[i]=siz[i]:siz[i]=sum[i]=1;
	siz[1]=sum[1]=0;//需要注意,根節點沒有任何資訊,下面的sum[1]是判斷無解 
	for(int i=tot;i>=1;i--)
	    for(int j=0;j<26;j++) 
	        sum[num[i]]+=sum[node[num[i]].ch[j]];
	if(sum[1]<k) puts("-1");
	else print(1,k),puts("");
	return 0;
}```