【CTSC2016】香山的树(KMP)

传送门

题意

给定 \(n,k\) 和一个长度不超过 \(n\) 的字符串 \(p\)。定义循环串为能表示成 \(q^k(k>1)\) 的串。称一个字符串是 lyndon 串,当且仅当它的最小循环表示是它本身,且它不是循环串。求长度不超过 \(n\) 且字典序大于等于 \(p\) 的所有 lyndon 串中,字典序第 \(k\) 小的那个。

\(n\le50,k\le10^{15},|\Sigma|=26\)。

分析

首先这个题没用到很多 lyndon 的东西,但还是很难。

考虑每次算出以 \(p\) 开头的 lyndon 串个数 \(k'\),讨论 \(k\) 与 \(k'\) 的大小关系:

  • 若 \(k>k'\),则 \(p\) 一定不是答案的前缀。我们把 \(k\) 减去 \(k'\),再删掉 \(p\) 末尾的 z,然后让 \(p\) 的最后一位变大 \(1\) 继续做。
  • 若 \(k\le k'\),则 \(p\) 一定是答案的前缀。我们判一下 \(s\) 是不是答案,若不是就给 \(k\) 减去 \(1\) 并在 \(p\) 后添一个 a 继续做。

这个算法用 \(O(n|\Sigma|)\) 步就能确定答案,现在我们只要求以 \(p\) 开头的 lyndon 串个数。

下文的所有串都是在首尾相接的意义下讨论的。对于一个合法串,若 \(p\) 在其中出现了 \(c\) 次,那么它有 \(c\) 种循环表示的开头是 \(p\)。设 \(s\) 为某个合法串开头为 \(p\) 的某个循环表示,观察到每个 \(s\) 只会对应一个合法串,那就是它的最小循环表示。因此我们可以让每个 \(s\) 都给答案 \(\frac{1}{c}\) 的贡献,最终每个合法串就会被算恰好 \(1\) 次。

现在分析 \(s\) 要满足什么条件:

  • \(s\) 的开头为 \(p\)。
  • \(s\) 中任意长度为 \(m\) 的子串都不小于 \(p\)。
  • \(s\) 不是循环串。

先假设 \(s\) 不是循环串的条件已经满足。我们建出 \(p\) 的 KMP 自动机,再把 \(s\) 重复无限次放到上面跑。注意不是随便跑,为了保证合法,每次失配时都强制 \(s\) 那位比 \(p\) 那位大。发现 \(s\) 唯一对应 KMP 自动机上一个长度为 \(|s|\) 的、经过 \(\operatorname{next}_m\) 的环。设 \(\text{dp}_{i,j,c}\) 为从 \(\operatorname{next}_m\) 开始走 \(i\) 步,当前在 KMP 自动机的 \(j\) 号点,共匹配了 \(c\) 次 \(p\) 的方案数。注意因为是从 \(s_{m+1}\) 开始跑,我们要强制在最后一步匹配到了 \(p\)。于是通过这个数组我们就能算出每个 \(|s|\) 的答案了。

可以搞一个容斥处理循环串。当串长为 \(i\) 时,设 \(g_i\) 表示通过上述算法算出的答案,\(f_i\) 表示实际的答案。那么有:

\[\begin{aligned} g_i &= \sum_{j|i} \frac{jf_j}{i} \\ if_i &= ig_i - \sum_{j|i,j<i} jf_j \end{aligned}\]

由于 \(if_i\) 总是整数,\(ig_i\) 也是整数,可以直接用 long long 存。由于要用到三维 DP 和 KMP 自动机,一次计算的复杂度是 \(O(n^3|\Sigma|)\),总共就是 \(O(n^4|\Sigma|^2)\)。

考虑优化。首先 \(p\) 必须是近似 lyndon 串(lyndon 串循环若干遍再接一个前缀)。这种条件下,由于失配时 \(s\) 中字符必须要更大,失配时一定会从头开始匹配。因此 DP 转移的出边只有 \(2\) 条,总时间就能少一个 \(|\Sigma|\)。

实现

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define per(i,a,b) for(int i=a;i>=b;i--)
#define gcd __gcd
using namespace std;

typedef long long ll;
const ll inf=4e18;
const int maxn=55;

int n,m,nxt[maxn];
char s[maxn];
ll K,f[maxn][maxn][maxn],g[maxn];

inline bool inc(){for(;m&&s[m]=='z';s[m--]=0);return m?s[m]++,1:0;}
inline bool chk(){rep(i,2,m)if(strcmp(s+1,s+i)>0)return 0;return 1;}
inline bool chk2(int j=1){rep(i,2,m){if(s[i]<s[j])return 0;if(s[i]==s[j])j++;else j=1;}return 1;}
inline void upd(ll &x,ll y){x=min(inf,x+y);}
inline ll mul(ll x,ll y){return 1.*x*y>2*inf?inf:min(inf,x*y);}

ll calc(){
	if(!chk2())return 0; 
	for(int i=2,k=0;i<=m;i++){
		for(;k&&s[k+1]!=s[i];k=nxt[k]);
		nxt[i]=(k+=s[k+1]==s[i]);
	}
	memset(f,0,sizeof(f));
	f[0][nxt[m]+1][0]=1;
	rep(i,0,n-1)rep(j,1,m)rep(k,0,i)if(f[i][j][k]){
		upd(f[i+1][j==m?nxt[m]+1:j+1][k+(j==m)],f[i][j][k]);
		upd(f[i+1][1][k],mul(f[i][j][k],'z'-s[j]));
	}
	memset(g,0,sizeof(g));
	rep(i,1,n)rep(k,1,i){
		upd(g[i],mul(f[i-1][m][k-1]/(k/gcd(i,k)),i/gcd(i,k)));
	}
	rep(i,1,n)for(int j=i*2;j<=n;j+=i)if(g[j]!=inf)g[j]-=g[i];
	rep(i,m,n)if(g[i]==inf)return inf;
	ll x=0;rep(i,m,n)upd(x,g[i]/i);return x;
}

int main(){
	scanf("%d%lld%s",&n,&K,s+1);
	m=strlen(s+1);
	while(1){
		ll x=calc();
		if(K>x){
			K-=x;
			if(!inc())puts("-1"),exit(0);
		}else{
			if(chk()&&!--K)puts(s+1),exit(0);
			s[++m]='a';
		}
	}
	return 0;
}
上一篇:数据结构实验:基于改进KMP算法的子串查找与替换


下一篇:java笔记【5.KMP算法】