题解: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;
}

posted @ 2026-06-05 10:19  Wxb2010  阅读(8)  评论(0)    收藏  举报