【数位 DP / 记忆化搜索】蓝桥云课 - 光轨区的 AI 数 题解

【数位 DP / 记忆化搜索】蓝桥云课 - 光轨区的 AI 数 题解

1. 题目概述

  • 题目大意 :求区间 l,rl, rl,r 内满足"任意相邻数位上的数字之差的绝对值均不超过 kkk"的整数个数。
  • 数据规模 :1≤l≤r≤10181 \le l \le r \le 10^{18}1≤l≤r≤1018,0≤k≤80 \le k \le 80≤k≤8

2. 解题思路

由于 r≤1018r \le 10^{18}r≤1018,直接暴力遍历会超时。本题是标准的数位 DP(数位动态规划)问题,采用记忆化搜索的方式最为简便。

  1. 区间差分

    l,rl, rl,r 区间内满足条件的个数,转化为求前缀和:ans = solve(r) - solve(l - 1)

  2. 状态定义

    设计 DFS 函数 dfs(pos, pre, is_limit, is_num)

    • pos:当前处理到从高到低的第几位。
    • pre:上一个数位选择的数字(0∼90 \sim 90∼9)。
    • is_limit:当前位填数是否受上限限制(若为 true,则最高只能填到当前位的数字限制)。
    • is_num:前面是否已经填入了有效的数字(用于处理前导 0)。
  3. 转移条件

    • 填写当前位数字 ddd 时,若 is_numtrue,必须满足 ∣d−pre∣≤k|d - pre| \le k∣d−pre∣≤k;
    • is_numfalse 且 d=0d = 0d=0,表示依然是前导 0,转移时 is_num 仍为 false
    • is_numfalse 且 d>0d > 0d>0,则 ddd 作为最高有效位,转移时 pre 设为 ddd,is_num 设为 true
  4. 记忆化

    只有当 !is_limit && is_num 时(即不受上限约束且已经填了有效数字),状态才具有通用性,可以保存在 dp[pos][pre] 数组中进行复用。


3. C++ 源码

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

LL l, r;
int k;
int digits[20];
LL dp[20][10];

/**
 * @brief 数位 DP 记忆化搜索
 * @param pos 当前搜索的数位索引(从高位到低位)
 * @param pre 上一位填写的数字
 * @param is_limit 是否受到高位限制
 * @param is_num 前面是否已经填入了有效数字(处理前导 0)
 */
LL dfs(int pos, int pre, bool is_limit, bool is_num) {
    // 递归基:搜索完所有数位
    if (pos < 0) return is_num ? 1 : 0;

    // 记忆化检索
    if (!is_limit && is_num && dp[pos][pre] != -1) {
        return dp[pos][pre];
    }

    LL res = 0;
    int up = is_limit ? digits[pos] : 9; // 当前位填数的上限

    for (int d = 0; d <= up; d++) {
        if (!is_num) {
            if (d == 0) {
                // 仍为前导 0
                res += dfs(pos - 1, pre, is_limit && (d == up), false);
            } else {
                // 开始填入最高有效位
                res += dfs(pos - 1, d, is_limit && (d == up), true);
            }
        } else {
            // 已有有效前缀,校验相邻数位差绝对值是否 <= k
            if (abs(d - pre) <= k) {
                res += dfs(pos - 1, d, is_limit && (d == up), true);
            }
        }
    }

    // 记忆化保存
    if (!is_limit && is_num) {
        dp[pos][pre] = res;
    }

    return res;
}

// 计算 [1, n] 内满足条件的 AI 数个数
LL solve(LL n) {
    if (n <= 0) return 0;

    int len = 0;
    while (n > 0) {
        digits[len++] = n % 10;
        n /= 10;
    }

    memset(dp, -1, sizeof(dp));
    return dfs(len - 1, 0, true, false);
}

int main() {
    // 优化标准输入输出
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    if (cin >> l >> r >> k) {
        cout << solve(r) - solve(l - 1) << "\n";
    }

    return 0;
}

4. 复杂度分析

  • 时间复杂度 :O(log⁡10(r)×10×10)O(\log_{10}(r) \times 10 \times 10)O(log10(r)×10×10)。数字 rrr 最多有 191919 位,状态数为 19×10=19019 \times 10 = 19019×10=190 个,每个状态遍历 0∼90 \sim 90∼9 转移,计算量在 10310^3103 级别,运行时间在 1ms 以内。
  • 空间复杂度 :O(log⁡10(r)×10)O(\log_{10}(r) \times 10)O(log10(r)×10)。仅需要占用极小的 dp[20][10] 数组与递归栈空间,内存消耗小于 1MB。

5.疑难解惑

1. 第一维 20(代表 pos:当前处理到的数位索引)

  • 原因 :题目中的数据范围通常为 1≤l≤r≤10181 \le l \le r \le 10^{18}1≤l≤r≤1018。101810^{18}1018 在十进制下是一个 19 位的数字。
  • 作用 :数组的第一维需要存储从第 000 位(个位)到最高位(第 181818 位)的状态。开到 20 可以保证完全容纳最多 19 位的数字索引(0∼180 \sim 180∼18),防止数组越界,属于竞赛和算法题中留有余量的安全写法(即"最大值 + 余裕")。

2. 第二维 10(代表 pre:上一位填入的十进制数字)

  • 原因 :在十进制下,任意数位上的数字只可能是 0,1,2,3,4,5,6,7,8,90, 1, 2, 3, 4, 5, 6, 7, 8, 90,1,2,3,4,5,6,7,8,9 这 10 种可能。
  • 作用 :数组的第二维需要记录上一位具体填的是哪个数字,以便检查相邻数位的差值绝对值(abs(d - pre) <= k)。因为数字取值范围是 0∼90 \sim 90∼9,所以大小开到 10 正好能够覆盖所有可能的数字情况。

3. 代码中 if (pos < 0) return is_num ? 1 : 0; 的含义

这是递归搜索的递归基(终止条件) 。当 pos < 0 时,说明从高位到低位(从 len - 1 一路减到 -1)的所有数位都已填完。此时通过 is_num 判断填入的数字是否合法。

以下以最大为 3 位数(pos 初始为 2)为例,演示不同数字的具体执行逻辑:


示例 1:构成数字 352(常规合法数字)
  • pos = 2 (百位) :填数字 3 →\rightarrow→ 开始填入有效数字,is_num 变为 true
  • pos = 1 (十位) :填数字 5 →\rightarrow→ is_num 仍为 true
  • pos = 0 (个位) :填数字 2 →\rightarrow→ is_num 仍为 true
  • pos = -1 :递归终止,此时 is_numtrue返回 1 (代表数字 352 是 1 个合法方案)。

示例 2:构成数字 7(带前导 0 的一位数)
  • pos = 2 (百位) :填数字 0 →\rightarrow→ 仍为前导 0,is_numfalse
  • pos = 1 (十位) :填数字 0 →\rightarrow→ 仍为前导 0,is_numfalse
  • pos = 0 (个位) :填数字 7 →\rightarrow→ 开始填入有效数字,is_num 变为 true
  • pos = -1 :递归终止,此时 is_numtrue返回 1 (代表数字 7 是 1 个合法方案)。

示例 3:全填前导 0 的情况(非法/空数字 0)
  • pos = 2 (百位) :填数字 0 →\rightarrow→ is_numfalse
  • pos = 1 (十位) :填数字 0 →\rightarrow→ is_numfalse
  • pos = 0 (个位) :填数字 0 →\rightarrow→ is_numfalse
  • pos = -1 :递归终止,此时 is_numfalse返回 0(说明这是无效的纯前导 0 路径,不计入最终结果)。

4. 代码中 if (!is_limit && is_num && dp[pos][pre] != -1) return dp[pos][pre]; 的含义

这段代码是数位 DP 中最关键的**"记忆化剪枝"**逻辑:如果在之前的搜索中已经计算并保存了"当前状态",且当前状态不受限制,就直接返回历史计算好的结果,不再重复递归。


一、三个条件的拆解
  1. !is_limit(不受最高位限制)

    • 含义 :当前能够填入的数字范围是完整的 0∼90 \sim 90∼9,而不是受到原始数字上限(不能超过 digits[pos])的约束。
    • 作用 :如果受到限制(is_limit == true),后面能填的数字组合会变少,计算出的方案数是特定/残缺的,不能作为通用的状态存起来给后续分支复用。
  2. is_num(已经填入了有效数字)

    • 含义:前面已经填过非 0 数字,当前处理的不再是前导 0。
    • 作用 :如果当前还是前导 0(is_num == false),pre(上一位数字)其实没有实际意义(例如 000 中的前导 0 并不是真正的数值 0)。只有确定进入有效数字阶段,dp[pos][pre] 的记录才是准确的。
  3. dp[pos][pre] != -1(已经记忆化存过值)

    • 含义 :初始时 dp 数组都被赋值为 -1。如果不等于 -1,说明之前有其他搜索分支已经经过 (pos, pre) 这种状态,并把计算好的合法方案数存入了 dp[pos][pre] 中。

二、具体数字举例(以 n=538,k=2n = 538, k = 2n=538,k=2 为例)

假设我们要计算 1∼5381 \sim 5381∼538 中满足相邻数字差值绝对值 ≤2\le 2≤2 的数字个数。

  • 场景 1:第一次计算 pos = 0(个位),pre = 3 的情况

    • 当搜索树走到 13x (百位填 1,十位填 3)时:
      • 递归进入 pos = 0(个位),此时 pre = 3(十位是 3)。
      • 因为百位填的是 1(小于上限 5),此时 is_limit = false
      • 已经填入了有效数字 13,所以 is_num = true
      • 检查 dp[0][3],发现是初始值 -1未命中记忆化
      • 程序继续向下执行循环,依次尝试个位填 1, 2, 3, 4, 5(满足 ∣d−3∣≤2|d - 3| \le 2∣d−3∣≤2),算出后续共有 5 种合法方案。
      • 返回前将结果写入记忆化数组:dp[0][3] = 5
  • 场景 2:第二次遇到 pos = 0(个位),pre = 3 的情况(触发记忆化)

    • 当搜索树走到 23x (百位填 2,十位填 3)时:
      • 递归再次进入 pos = 0(个位),此时 pre 同样是 3
      • 百位填的是 2(仍小于上限 5),is_limit = false
      • 已经填入了有效数字 23is_num = true
      • 执行判断:if (!is_limit && is_num && dp[0][3] != -1),此时 dp[0][3] 在上一步已经被赋值为 5
      • 触发条件!直接返回 5 。程序无需再次跑 for 循环校验条件,瞬间完成该分支计算。
相关推荐
地平线开发者2 小时前
Horizon J6m 部署 YOLOv8s INT8 精度下降问题排查记录
算法
橘子汽水1682 小时前
Leetcode 200,994岛屿数量,腐烂的橘子
数据结构·算法·leetcode
欧特克_Glodon2 小时前
OpenCV计算机视觉开发入门与实践<三十七>:OpenCV中的机器学习
c++·opencv·机器学习·计算机视觉
老赵的博客3 小时前
c++ 之观察者模式
c++·qt
坚定学代码3 小时前
static在c++中的使用
开发语言·c++
hansang_IR3 小时前
【题解】P5364 [SNOI2017] 礼物
c++·线性代数·算法
Tisfy3 小时前
LeetCode 3871.统计范围内的逗号 II:每次算一个逗号
数学·算法·leetcode·题解
无小道3 小时前
C/C++——tuple小记
c++·tuple
小O的算法实验室3 小时前
IEEE TEVC,基于K-means与DQN辅助元启发式算法的多目标两栖无人艇调度模型
算法·kmeans·启发式算法