GDOI2016模拟4.22 无界单词 字符串上的动态规划

本文介绍了一种算法,用于解决给定长度下无界单词的数量计算及字典序第K个无界单词的确定问题。通过递归计算所有可能的单词组合,排除有界单词,最终找到符合条件的无界单词。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目大意

给定一个NK,问所有长度为L的仅包含字符ab的单词中有多少个无界单词,以及字典序第K的无界单词是什么?
有界单词:对于一个单词S,如果存在一个长度L,满足0<L<length(S),并且使得S长度为L的前缀与S长度为L的后缀相同,则称S是有界的。
无界单词:对于一个单词S,如果不存在一个L使之成为有界单词,即它为无界单词。
题目有T组测试数据。

L<=64T<=4

解题思路

相比第二问来说,肯定是第一问比较好处理,那我们先来考虑一下第一问。
我们可以令Fi表示长度为i的单词有多少个是无序的,计算时无界单词考虑起来比较繁琐,所以可以用所有单词的数量减去有界单词的数量来得到Fi的值。那么现在就需要考虑长度为i的有界单词的数量。

假如当前的有界单词长度为L的前缀和后缀相等,我们对每个单词统计数量是只考虑最小的L来保证不会算重。一个显然的性质就是当L>i2时,肯定存在一个更小的L使之满足要求,所以就只需考虑小于等于i2L。考虑最小的L时我们可以直接把之前统计出的长度小于i2的无界单词复制一遍放在头和尾,中间剩下字符任选,这样就可以推出Fi。总的来说就可以写出递推式Fi=2ii2j=1Fj2i2jFN就是第一问的答案,并且与第二问也有关联。

对于第二问,容易想到的是我们可以一位一位的判断能不能放a,现在的问题就变成了如何统计一个确定前Len个字符的字符串能构成无界单词的个数。其实跟第一问差不多,也要维护一个Fi表示前i个字符组成的字符串(包括未确定的)组成的无序单词的数量。但是递推时需要分几种情况讨论一下。
假设当前已经确定了前Len个字符,现在递推到第i位,枚举到长度为j的无序前缀(这里的ij和上面的含义一样)

  1. iLen
    那么当前要求的前Len个字符都是已经确定。所以直接用KMP判断一下已经确定的前i个字符组成的单词是否是无序单词就可以了。

  2. Lenj
    那么对当前的Fi造成影响的Fj都在前面的转移中特殊考虑过了,所以第一问一样直接转移就可以了。

  3. Len>jLenij
    这种情况跟第二种差不多,但是相同的前后缀中有些任选的位置已经在被确定了,所以只需把中间任选的方案(2i2j),改成除了确定的位置任选(2i2j(Lenj)

  4. Len>ij
    当出现这种情况时不仅中间任选字符已经确定,而且我们把前缀复制到后缀时,放后缀的有些位置的字符也已经确定。所以我们只需用Hash判断一下当前超出的位置是不是当前单词的前缀。而由于可以任选字符的位置已经确定,所以如果匹配成功,只需把有序单词的个数加+E

程序

//YxuanwKeith
#include <cstring>
#include <cstdio>
#include <algorithm>

using namespace std;
typedef unsigned long long LL;

const int MAXN = 65, HNum = 15731;

LL K, Pow[MAXN], F[MAXN], Has[MAXN], H[MAXN];
int Len, Last, Next[MAXN], N, Pj, Ans[MAXN];

void Prepare() {
    Pow[0] = Has[0] = 1;
    for (int i = 1; i <= 64; i ++) Pow[i] = Pow[i - 1] * 2;
    for (int i = 1; i <= 64; i ++) Has[i] = Has[i - 1] * HNum;
}

bool Judge(int Len, int r) {
    int l = r - Len + 1;
    return H[Len] - H[0] * Has[Len] == H[r] - H[l - 1] * Has[r - l + 1]; 
}

LL Solve() {
    memset(F, 0, sizeof F);
    for (int i = 1; i <= N; i ++) {
        if (i < Len && !Next[i]) F[i] = 1;
        if (i < Len) continue;
        F[i] = Pow[i - Len];
        for (int j = 1; j <= i / 2; j ++) {
            LL Cnt;
            if (Len <= j) Cnt = Pow[i - 2 * j];
            if (Len > j && Len <= i - j) Cnt = Pow[i - 2 * j - (Len - j)];
            if (Len > i - j) Cnt = Judge(Len - (i - j), Len);
            F[i] -= F[j] * Cnt;
        }
    }
    return F[N];
}

void PushIn(int c) {
    Ans[++ Len] = c, H[Len] = H[Len - 1] * HNum + c;
    if (Len == 1) return;
    for (; Pj && Ans[Pj + 1] != Ans[Len]; Pj = Next[Pj]);
    if (Ans[Pj + 1] == Ans[Len]) Pj ++;
    Next[Len] = Pj;
}

void Work() {
    Len = 0, Pj = 0;
    scanf("%d%lld", &N, &K);
    printf("%lld\n", Solve());
    for (int i = 1; i <= N; i ++) {
        Last = Pj;
        PushIn(0);
        LL Num = Solve();
        if (Num < K) {
            Pj = Last, Len --;
            K -= Num;
            PushIn(1);
        }
    }
    for (int i = 1; i <= N; i ++) printf("%c", Ans[i] + 'a');
    printf("\n");
}

int main() {
    Prepare();
    int Test;
    scanf("%d", &Test);
    for (; Test; Test --) Work();
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值