《数位DP》Numehehaaaaaa(解题报告)

零、题目描述

题目链接:Numehehaaaaaa
题目要求
输入 t 组测试用例,每组给定一个整数 n,请输出大于等于 n 的最小整数,使得该整数的十进制表示中包含子串“114514”(即“114514”作为连续字符出现)。

示例(假设):

  • 若输入 n = 114513,输出 114514(直接包含模式串);
  • 若输入 n = 114515,输出 1000000 + 114514 = 1114514(需找下一个包含模式串的数)。

一、算法概述

本题采用 “数位动态规划(数位DP)+ 状态机 + 二分查找” 的组合算法,核心思路如下:

  1. 数位DP + 状态机:设计数位DP统计“0到x中包含子串‘114514’的数的个数”。其中,状态机用于追踪当前数位匹配模式串的进度,确保高效识别模式串;
  2. 二分查找:利用“0到x中符合条件的数的个数”的单调性(x越大,个数越多),通过二分查找找到“大于等于n的最小x”,使得x是第一个包含“114514”的数。

该算法的优势在于:

  • 数位DP解决了“大数字范围计数”的问题(避免直接枚举导致的超时);
  • 状态机精准追踪模式串匹配进度,确保不遗漏符合条件的数;
  • 二分查找将“找最小数”转化为“验证计数差”,效率极高。

二、算法思路

2.1 核心问题拆解

本题的核心是两个子问题:

  • 子问题1:如何快速计算 f(x) = 0到x中包含“114514”的数的个数?
  • 子问题2:如何基于 f(x) 找到大于等于n的最小x,使得 f(x) - f(n-1) = 1(即x是n之后第一个包含模式串的数)?

2.2 子问题1:数位DP + 状态机实现 f(x)

2.2.1 状态机设计(模式串匹配核心)

目标是匹配子串“114514”,需设计状态机追踪当前匹配进度:

  • 状态定义:用 data0 表示当前匹配到模式串的第 k 个字符(k ∈ 0~6):
    • 0:初始状态(未匹配任何字符);
    • 1:匹配到第1个字符“1”;
    • 2:匹配到第2个字符“1”(即“11”);
    • 3:匹配到第3个字符“4”(即“114”);
    • 4:匹配到第4个字符“5”(即“1145”);
    • 5:匹配到第5个字符“1”(即“11451”);
    • 6:完全匹配(即“114514”,终止状态)。
  • 转移规则:用二维数组 mat[7][10] 存储状态转移,mat[cur_state][digit] 表示“当前状态为 cur_state 时,输入数字 digit 后转移到的新状态”。例如:
    • mat[0][1] = 1(初始状态遇“1”,匹配第1个字符);
    • mat[5][4] = 6(匹配到“11451”后遇“4”,完全匹配);
    • mat[6][d] = 1(完全匹配后,遇任何数字均回到“匹配第1个‘1’”的状态,不影响后续计数)。
2.2.2 数位DP状态设计

数位DP的递归状态需包含以下信息:

  • depth:当前枚举到的数位位置(从高位到低位);
  • is_leadingzero:是否存在前导零(如“00123”的前导零不影响实际数值);
  • is_limit:当前数位是否受原数 x 的限制(如 x=123,枚举到百位为1时,十位最多为2;若百位为0,十位无限制);
  • dpdata:自定义状态(含 data0 状态机进度、data1 是否已找到模式串)。
2.2.3 数位DP流程
  1. 初始化dpdata 初始化为 data0=0(未匹配)、data1=0(未找到模式串);
  2. 递归出口:当枚举完所有数位(depth == num.size()),返回 data1(1表示找到模式串,0表示未找到);
  3. 记忆化剪枝:若当前状态已计算过,直接返回缓存结果(避免重复计算);
  4. 枚举数位:根据 is_limit 确定当前数位的最大值,遍历所有可能的数字;
  5. 状态转移:根据当前数字更新 is_leadingzerois_limitdpdata(通过 getNextDpData 函数更新状态机进度和 data1);
  6. 递归累加:累加所有子状态的结果,存入缓存并返回。

2.3 子问题2:二分查找找最小x

由于 f(x)单调递增函数(x越大,包含模式串的数越多),可通过二分查找定位最小x:

  • 二分范围
    • 左边界 l = n(从n开始找);
    • 右边界 r = (n/1e6 + 1)*1e6 + 114514(保守上界,确保包含至少一个符合条件的数);
  • 二分逻辑
    1. 计算 lans = f(n)(0到n中符合条件的数的个数);
    2. 取中点 mid = (l + r) / 2,计算 cnt = f(mid) - lans
    3. cnt >= 1:说明 mid 及左侧存在符合条件的数,调整 r = mid
    4. cnt < 1:说明 mid 左侧无符合条件的数,调整 l = mid
    5. l + 1 < r 时重复,最终 r 即为最小x。

三、代码实现

代码分为 模板框架自定义逻辑 两部分,核心自定义逻辑集中在状态机和状态转移。

3.1 宏定义与结构体

#include <iostream>
#include <map>
#include <cstring>
#include <string>
using namespace std;

#define ll long long
#define maxd 65          // 最大数位(十进制65位足够覆盖大整数)
#define MEMORY_USE_MAP   // 启用map记忆化(灵活处理状态,避免数组大小限制)

// 自定义DP状态:存储状态机进度和是否找到模式串
struct DpData {
    ll data0;  // 状态机进度(0~6,对应匹配模式串的0~6个字符)
    ll data1;  // 是否已找到模式串(1=是,0=否)
    static ll K;    // 预留参数(本题未用)
    static ll base; // 进制(十进制=10)

    // 构造函数
    DpData(): data0(0), data1(0) { init(); }
    DpData(ll d0, ll d1): data0(d0), data1(d1) {}

    // map排序需要的比较运算符
    bool operator<(const DpData& d) const {
        return data0 != d.data0 ? data0 < d.data0 : data1 < d.data1;
    }

    // 自定义逻辑:初始化DP状态
    void init() { data0 = 0; data1 = 0; }
    // 自定义逻辑:递归出口返回值
    ll dfsReturn(bool is_leadingzero) const {
        return is_leadingzero ? 0 : data1; // 前导零视为无意义,返回0
    }
    // 自定义逻辑:状态转移
    DpData getNextDpData(bool is_leadingzero, int digit) const;
};

// 静态成员初始化
ll DpData::K = 0;
ll DpData::base = 10;

3.2 状态机转移表与核心函数

// 状态机转移表:mat[当前状态][输入数字] = 下一状态
int mat[7][10] = {
    {0,1,0,0,0,0,0,0,0,0}, // 状态0(初始):遇1→1,其他→0
    {0,2,0,0,0,0,0,0,0,0}, // 状态1(匹配"1"):遇1→2,其他→0/1
    {0,2,0,0,3,0,0,0,0,0}, // 状态2(匹配"11"):遇4→3,其他→0/1
    {0,1,0,0,0,4,0,0,0,0}, // 状态3(匹配"114"):遇5→4,其他→0/1
    {0,5,0,0,0,0,0,0,0,0}, // 状态4(匹配"1145"):遇1→5,其他→0/1
    {0,2,0,0,6,0,0,0,0,0}, // 状态5(匹配"11451"):遇4→6,其他→0/1
    {0,1,0,0,0,0,0,0,0,0}  // 状态6(匹配"114514"):遇任何→1(已找到,不影响)
};

// 状态转移实现:根据当前状态和数字更新DP状态
DpData DpData::getNextDpData(bool is_leadingzero, int digit) const {
    DpData ret = *this;
    if (is_leadingzero) {
        if (digit == 0) {
            // 前导零+当前为0:保持前导零,状态机不变
            ret.data0 = 0;
        } else {
            // 前导零+当前非0:取消前导零,更新状态机
            ret.data0 = mat[0][digit];
        }
    } else {
        // 无中导零:直接按状态机转移
        ret.data0 = mat[ret.data0][digit];
    }
    // 若已找到模式串(data1=1),则保持1;否则检查是否刚匹配成功
    ret.data1 = ret.data1 || (ret.data0 == 6);
    return ret;
}

3.3 数位DP模板(dfs与getans)

// 记忆化数组/Map:dp[深度][前导零][限制][DP状态] = 计数
#ifdef MEMORY_USE_MAP
map<DpData, ll> dp[maxd][2][2];
#else
#define data0_max 7  // 状态机最多7个状态(0~6)
#define data1_max 2  // data1只有0/1
ll dp[maxd][2][2][data0_max][data1_max];
#endif

// 数位DP递归函数
ll dfs(const string& num, int depth, bool is_leadingzero, bool is_limit, DpData dpdata) {
    // 1. 递归出口:枚举完所有数位
    if (depth == num.size()) {
        return dpdata.dfsReturn(is_leadingzero);
    }

    // 2. 记忆化返回
#ifdef MEMORY_USE_MAP
    map<DpData, ll>& memo = dp[depth][is_leadingzero][is_limit];
    auto it = memo.find(dpdata);
    if (it != memo.end()) return it->second;
#else
    ll& ans = dp[depth][is_leadingzero][is_limit][dpdata.data0][dpdata.data1];
    if (ans != -1) return ans;
#endif

    // 3. 枚举当前数位的可能值
    ll ans = 0;
    int max_digit = is_limit ? (num[depth] - '0') : (DpData::base - 1); // 数位上限
    for (int digit = 0; digit <= max_digit; ++digit) {
        // 更新子状态
        bool new_leadingzero = is_leadingzero && (digit == 0);
        bool new_limit = is_limit && (digit == max_digit);
        DpData new_dpdata = dpdata.getNextDpData(is_leadingzero, digit);

        // 递归累加子状态结果
        ans += dfs(num, depth + 1, new_leadingzero, new_limit, new_dpdata);
    }

    // 4. 记忆化存储
#ifdef MEMORY_USE_MAP
    memo[dpdata] = ans;
#endif
    return ans;
}

// 计算[0, n]中包含模式串的数的个数
ll getans(ll n) {
    // 初始化记忆化
#ifdef MEMORY_USE_MAP
    for (int i = 0; i < maxd; ++i)
        for (int j = 0; j < 2; ++j)
            for (int k = 0; k < 2; ++k)
                dp[i][j][k].clear();
#else
    memset(dp, -1, sizeof(dp));
#endif

    // 数字转字符串(高位在前)
    string s;
    if (n == 0) s = "0";
    else {
        int a[maxd], asize = 0;
        while (n) { a[asize++] = n % 10; n /= 10; }
        for (int i = asize - 1; i >= 0; --i) s.push_back('0' + a[i]);
    }

    // 启动数位DP
    DpData dpd;
    return dfs(s, 0, true, false, dpd);
}

// 计算[l, r]中包含模式串的数的个数
ll getans(ll l, ll r) {
    return getans(r) - getans(l - 1);
}

3.4 主函数(二分查找逻辑)

int main() {
    int t;
    cin >> t;
    while (t--) {
        ll n;
        scanf("%lld", &n);

        // 二分查找边界:右边界确保包含答案
        ll lans = getans(n);
        ll l = n, r = (n / 1000000 + 1) * 1000000 + 114514;

        // 二分查找最小x
        while (l + 1 < r) {
            ll mid = (l + r) >> 1;
            if (getans(mid) - lans >= 1) {
                r = mid; // mid左侧可能有答案
            } else {
                l = mid; // mid左侧无答案
            }
        }

        printf("%lld\n", r);
    }
    return 0;
}

四、算法解释

4.1 状态机核心逻辑

状态机是本题识别模式串的关键,mat 数组的设计直接决定匹配准确性:

  • 例如,当处于状态5(已匹配“11451”)时,若当前数字是4,mat[5][4] = 6data1 设为1(标记找到模式串);
  • 若已找到模式串(data1=1),后续状态转移不改变 data1,确保即使后续数位不匹配,也不会“忘记”已找到的模式串。

4.2 前导零处理

前导零不影响实际数值(如“00114514”等价于“114514”),因此:

  • 若存在前导零且当前数字为0,is_leadingzero 保持为true,data0 仍为0(未开始匹配);
  • 若当前数字非0,is_leadingzero 设为false,开始按状态机匹配模式串。

4.3 记忆化的作用

数位DP的状态空间较小(maxd×2×2×7×2 ≈ 3.6×10³),记忆化可避免重复计算相同状态,将时间复杂度从“指数级”降至“多项式级”,确保处理大数字时不超时。

4.4 二分查找的合理性

f(x) 单调递增的证明:对任意 x1 < x2[0, x1][0, x2] 的子集,因此 f(x1) ≤ f(x2)。基于单调性,二分查找可高效定位最小x。

五、复杂度分析

5.1 时间复杂度

  1. 数位DP单次计算(getans(x))

    • 状态总数:depth × is_leadingzero × is_limit × data0 × data1 = 65 × 2 × 2 × 7 × 2 ≈ 3.6×10³
    • 每个状态枚举10个数字(十进制),因此单次 getans(x) 时间为 O(3.6×10³ × 10) = O(3.6×10⁴)
  2. 二分查找

    • 二分次数:log2(r - l),其中 r 最多为 n + 1e6,因此次数约为 20
    • 单次测试用例时间:O(20 × 3.6×10⁴) = O(7.2×10⁵)
  3. 多组测试用例:若 t ≤ 100,总时间为 O(100 × 7.2×10⁵) = O(7.2×10⁷),远低于时间限制。

5.2 空间复杂度

  • 记忆化空间
    • 若用Map:存储所有状态,空间为 O(3.6×10³)(每个状态的Map条目);
    • 若用数组:dp[65][2][2][7][2],总大小约 65×2×2×7×2 = 3640ll,仅占 3640×8 ≈ 29KB,空间开销极小。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

英雄哪里出来

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

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

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

打赏作者

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

抵扣说明:

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

余额充值