从⌈AcWing3752⌋看数位dp

题目描述

给定一个整数 K K K 和一个长度为 N N N 的字符串 S S S。已知,字符串 S S S 是由前 K K K 个小写字母组成。

现在,请你找出满足下列条件的回文字符串的数量:

  • 长度为 N N N
  • 字典序上小于 S S S
  • 由前 K K K 个小写字母组成。

由于满足条件的字符串数量可能很大,所以输出对 10 e 9 + 7 10e9+7 10e9+7 取模后的答案。

输入格式

第一行包含整数 T T T,表示共有 T T T 组测试数据。
每组数据第一行包含两个整数 N N N K K K
第二行包含一个长度为 N N N 的由小写字母组成的字符串 S S S

输出格式

每组数据输出一个结果,每个结果占一行。
结果表示为 Case #x: y,其中 x x x 为组别编号(从 1 开始), y y y 为对 109 + 7 109+7 109+7 取模后的答案。

数据范围

1 ≤ T ≤ 100 , 1≤T≤100, 1T100,
1 ≤ N ≤ 105 , 1≤N≤105, 1N105,
1 ≤ K ≤ 26 1≤K≤26 1K26

输入样例:
3
2 3
bc
5 5
abcdd
1 5
d
输出样例:
Case #1: 2
Case #2: 8
Case #3: 3
样例解释

对于样例 1,满足条件的回文串为 aabb

对于样例 2,满足条件的回文串为 aaaaaaabaaaacaaaadaaaaeaaababaabbbaabcba

对于样例 3,满足条件的回文串为 abc

算法求解

本题是经典的数位DP问题,按照题目要求,给定字符串 S S S,需要找的是相同长度、字典序小、前 k k k 个字母构成的回文字符串。根据数据量,只有 O ( N ) O(N) O(N) 的时间复杂度满足要求。

这里所求字符串的回文性质其实带来了一定程度的便利——如果已经明确字符串前一半(长度 ⌊ ( N + 1 ) / 2 ⌋ \lfloor (N+1)/2 \rfloor ⌊(N+1)/2 ⌈ N / 2 ⌉ \lceil N/2\rceil N/2 ),那么后一半也就可以明确,如果前一半已经满足字典序要求,只需要确定后一半符合字典序要求,通过一个 O ( N ) O(N) O(N) 的遍历检验即可(然而后续论证可以说明这种检验只在一种情况下需要用到)。

两个等长字符串的字典序比较有很好的性质——对应位相等,则继续往后比较,直到某一位不相等,可以直接由该位判断两个字符串整体的字典序大小(也就是说和后面的位都无关,后面可以任意取)。

解决该问题主要就在于如何确定前一半的序列,数位统计中可以通过构建一棵树来进行:

假设字符串 S = b d c a e S=bdcae S=bdcae k = 5 k=5 k=5,所求回文字符串记为 L L L

可以定义字符串一半长为变量 mid=(n+1)/2

那么对于 L L L 的第一个字符 L 1 L_1 L1,在满足字典序的情况下可以选择 a a a b b b

a
b
L1=
任意
考虑下一个字符
  • 如果 L 1 = a L_1=a L1=a,由于 S 1 = b S_1=b S1=b,故已经满足了字典序的要求,根据字典序比较性质, L 1 L_1 L1 ~ L m i d L_{mid} Lmid 可以任意取前 k k k 小的所有字符的组合(由于回文,最多只能决定到 m i d mid mid ),所以当 L 1 = a L_1=a L1=a 时可能成立的情况有 k m i d − 1 k^{mid-1} kmid1 种。
  • 如果 L 1 = b L_1=b L1=b,那么决定权就给到 L 2 L_2 L2 S 2 S_2 S2 的比较,可以继续构建:
a
b
a
b
c
d
L1=
任意
L2=
任意
任意
任意
考虑下一个字符

同样,在 L 2 < S 2 L_2<S_2 L2<S2 的情况下,都已经满足了字典序要求,故没种都可以有 k m i d − 2 k^{mid-2} kmid2 种情况,也就是说这里可以取到 3 ∗ k m i d − 2 3*k^{mid-2} 3kmid2 种情况。

如果归纳到更一般的情况,对于 L i L_i Li i i i 从1开始 ),共有 Si-'a' 种情况会满足“任意”的条件,故可以新增 ( S i − ′ a ′ ) ∗ k m i d − i (S_i-'a')*k^{mid-i} (Sia)kmidi 种成立的情况。如果从 0 0 0 开始计数(更符合代码书写的情况),代码表示为:

res += (LL)(s[i] - 'a') * p[mid - i - 1]		//p[x]表示p的x次方

此外,最后还需要考虑前半部分都相等的唯一一种情况(即从1~ m i d mid mid ),都满足 L i = S i L_i = S_i Li=Si,那么就需要遍历回文获得的后半部分,检查是否满足字典序,如果满足则需要加上这一种情况。

另外关于 k k k 的次方运算,可以先通过一个 O ( n ) O(n) O(n) 的运算存储在数组 p 中,后续只需要调用即可:

p[0] = 1;
for (int i = 1; i <= n; i++ )
		p[i] = (LL)p[i - 1] * k % MOD;  //k的i次方计算

完整代码如下:

//
//  main.cpp
//  3752-更小的字符串
//
//  Created by MacBook Pro on 2023/8/8.
//

/* 算法:数位统计 */
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 100010, MOD = 1e9 + 7;

int n, m;
char s[N];
int p[N];

int main()
{
    int T;
    scanf("%d", &T);
    for (int cases = 1; cases <= T; cases++ )
    {
        scanf("%d%d", &n, &m); scanf("%s", s);

        p[0] = 1;
        for (int i = 1; i <= n; i++ )
            p[i] = (LL)p[i - 1] * m % MOD;  //m的i次方计算

        int res = 0, mid = (n + 1) / 2;     //自由度计算
        for (int i = 0; i < mid; i++ )      //计算每一位 - 非相等情况的自由数
        {
            res += (LL)(s[i] - 'a') * p[mid - i - 1] % MOD; //中间值到本位的最高自由数
            res %= MOD;
        }

        int t = 0;
        for (int i = mid - 1, j = n - 1 - i; i >= 0; i --, j ++ )
            if (s[i] != s[j])
            {
                if (s[i] < s[j]) t = 1;
                break;
            }

        res = (res + t) % MOD;
        
        printf("Case #%d: %d\n", cases, res);
    }

    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zhqi HUA

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值