题解:P5770 [JSOI2016] 无界单词
前言
本文同步自洛谷专栏,题目传送门。
做法:kmp,容斥。复杂度 \(O(n^3)\)。
分析
题意比较简单:求长为 \(n\) 仅由 \(a,b\) 组成的无 border 的字符串个数与其中字典序第 \(k\) 大的字符串。
第一问
先做第一问,令 \(dp_i\) 表示长为 \(i\) 时的子问题,所求即为 \(dp_n\)。
考虑到:无 border 的个数 \(=\) 总个数 \(-\) 有 border。使用容斥,使每个有 border 的字符串仅在最短 border 处统计一次,则统计的要求为该 border 无 border。
初始时,\(dp_i=2^i\),然后枚举 border 长度 \(j\),则减去 \(dp_j\times 2^{i-2j}\),注意指数为 \(i-2j\) 而非 \(i-j\),因为长度为 \(j\) 的 border 已经在 \(dp_j\) 中枚举了。
注意 \(2\times j\le i\),一个字符串如果有 border,那么其最短 border 长度 \(\le \frac{n}{2}\),证明可以利用周期与最长 border 的关系,画图理解。
第二问
第二问较为麻烦,考虑到答案很大,不能枚举,故考虑从前向后逐位确定答案:当前位填 \(a\) 的方案数 \(\ge K\) 则表明可行,否则填 \(b\)。
首先可以利用 \(dp_n\) 确定第一位:根据对称性,由 \(a\) 开头的和由 \(b\) 开头的各有一半。(或许可以避免一些 corner)
假设当前正在确定第 \(i\) 位的结果,先定为 \(a\)(下文的已确定部分包括这暂定的一位),对当前的答案序列做一遍 kmp,得到 fail 数组。
沿用第一问的方法定义 \(g_j\) 为:已经确定了 \(i\) 位、字符串长度为 \(j\) 时无 border 的个数。
\(j\le i\) 时,若 \(fail_j=0\) 则 \(g_j=1\),否则为 \(0\)。
\(j>i\) 时,初始有 \(g_j=2^{j-i}\) 表示有 \(j-i\) 位可以填。
枚举 border 长度 \(k\),则减去的值为一个权值 \(\times g_k\),下面讨论这个权值。
-
\(k\ge i\),此时 border 覆盖所有确定的数,则有 \(j-2k\) 位可以任意填写,权值为 \(2^{j-2k}\),\(g_k\) 中包含了确定前 \(k\) 位的方案。
-
\(k\le j-i\),此时 border 不能(或恰好)覆盖未确定的部分,权值为 \(2^{j-i-k}\)。
-
其它情况,此时后缀上的 border 会和已确定的部分重叠,如果已确定的部分存在一个长为 \(k-(j-i)\) 的 border,则不会矛盾,权值为 \(1\),否则为 \(0\)。(\(k-j+i\) 画图理解较为简洁)
对于其它情况中查询是否存在一个长为 \(k-j+i\) 的 border,可以用桶存下当前已确定部分所有 border 的长度,使确定一位的复杂度从 \(O(n^3)\) 降低至 \(O(n^2)\)。
实现
注释算是比较多的了。
$\red{\text{code}}$
#include<bits/stdc++.h>
using namespace std;
int n,fail[70],cnt[70];
unsigned long long dp[70],K,g[70];
char s[70];
void solve(){
memset(cnt,0,sizeof(cnt)),fail[1]=0;
scanf("%d%llu",&n,&K),s[n+1]=0,g[1]=1;
if(K>(dp[n]>>1)) s[1]='b',K-=(dp[n]>>1);
else s[1]='a';//可以直接判定第一位
for(int i=2,fj;i<=n;i++){//正在考虑第 i 位
s[i]='a',fj=0;
for(int fi=2;fi<=i;fi++){//建议重新匹配,因为可能会更改答案序列
while(fj&&s[fj+1]!=s[fi]) fj=fail[fj];
if(s[fj+1]==s[fi]) fj++;
fail[fi]=fj,g[fi]=!fj;//有 border 则 g=0
}
for(;fj;fj=fail[fj]) cnt[fj]=i;//减少 memset 次数
for(int j=i+1;j<=n;j++){//求解长度为 j 的无 border 的串串个数
g[j]=1ull<<(j-i);//总长度为 j 时的初始方案数
for(int k=1;(k<<1)<=j;k++){//枚举 border 长度,类似 main
if(k>=i) g[j]-=g[k]*(1ull<<(j-k-k));//可以确定前后 k 位
else if(k<=j-i) g[j]-=g[k]*(1ull<<(j-i-k));//前 i 位和后 k 位确定
else if(cnt[k-(j-i)]==i) g[j]-=g[k];//前 i 位要有一个长度为 k-(j-i) 的 border
}
}
if(g[n]<K) s[i]='b',K-=g[n];
}
printf("%llu\n%s\n",dp[n],s+1);
}
int main(){
for(int i=1;i<=64;i++){//用最小的 border 统计
if(i!=64) dp[i]=1ull<<i;//据讨论区,1ull<<64 会出现神秘错误
for(int j=1;(j<<1)<=i;j++){
dp[i]-=dp[j]*(1ull<<(i-j-j));
}
}
int Tcnt;scanf("%d",&Tcnt);
while(Tcnt--) solve();
return 0;
}

浙公网安备 33010602011771号