数位DP,统计特殊整数

统计区间内的特殊整数

题目描述

如果一个正整数每一个数位上的数字都是 互不相同 的,我们称它为 特殊整数

给定一个正整数 n,请你返回区间 [1, n] 之间特殊整数的数目。

示例

示例 1:

输入:n = 20
输出:19
解释:1 到 20 之间所有整数除了 11 以外都是特殊整数。所以总共有 19 个特殊整数。

示例 2:

输入:n = 5
输出:5
解释:1 到 5 所有整数都是特殊整数。

示例 3:

输入:n = 135
输出:110
解释:从 1 到 135 总共有 110 个整数是特殊整数。
不特殊的部分数字为:22 ,114 和 131 。

数据范围

1 <= n <= 2 * 10^9

解决方案

为了统计区间 [1, n] 内的特殊整数数量,我们需要确保每个数字的各个数位上的数字都是互不相同的。由于 n 的范围可以达到 2 * 10^9,直接遍历所有数字并检查每个数字是否为特殊整数的效率过低。因此,我们需要一种高效的方法来计算符合条件的数字数量。

思路解析

我们可以使用 数字动态规划(Digit DP) 的方法来解决这个问题。数字 DP 是一种专门用于处理与数字的各个数位相关问题的动态规划技巧,特别适用于范围计数问题。

以下是具体的思路步骤:

  1. 将数字转换为字符串:将 n 转换为字符串 s,方便逐位处理。

  2. 定义状态

    • i:当前处理的位数(从左到右)。
    • is_limit:一个布尔值,表示当前位是否受到上界 n 的限制。如果 is_limittrue,则当前位的数字不能超过 s[i];否则,可以自由选择 09
    • mask:一个位掩码,用于记录当前已经使用过的数字。mask 的第 c 位为 1 表示数字 c 已经被使用。
    • is_num:一个布尔值,表示当前是否已经开始构建数字。如果 is_numfalse,则表示还没有选择任何非零数字,此时可以选择跳过当前位(即选择前导零)。
  3. 递归函数 dfs

    • 终止条件:当处理到第 m 位(即所有位都处理完)时,如果已经构建了一个有效数字(is_numtrue),则返回 1,否则返回 0
    • 记忆化:为了避免重复计算,当 is_limitfalse 且已经开始构建数字时,如果当前状态已经计算过,则直接返回记忆化的结果。
    • 选择跳过当前位:如果还没有开始构建数字(is_numfalse),可以选择跳过当前位,继续处理下一位。
    • 选择当前位的数字:根据 is_limit 的状态,确定当前位可以选择的数字范围。如果选择了某个数字 c,需要检查 c 是否已经被使用过(通过 mask 判断)。如果没有被使用,则更新 mask 并递归处理下一位。
  4. 记忆化表:使用一个二维数组 memo 来存储中间结果,避免重复计算。

代码解析

以下是基于上述思路实现的代码:

#include <vector>
#include <string>
#include <functional>
using namespace std;

class Solution {
public:
    int countSpecialNumbers(int n) {
        string s = to_string(n); // 将数字转换为字符串
        int m = s.size(); // 数字的位数
        // 初始化记忆化表,-1 表示未计算
        vector<vector<int>> memo(m, vector<int>(1 << 10, -1));
        
        // 定义递归函数
        function<int(int, bool, int, bool)> dfs = [&](int i, bool is_limit, int mask, bool is_num) -> int {
            if(i == m) return is_num ? 1 : 0; // 终止条件
            if(!is_limit && is_num && memo[i][mask] != -1) return memo[i][mask]; // 记忆化
            
            int res = 0;
            if(!is_num) {
                // 选择跳过当前位
                res += dfs(i + 1, false, mask, false);
            }
            
            int up = is_limit ? s[i] - '0' : 9; // 当前位的上限
            for(int c = is_num ? 0 : 1; c <= up; c++) { // 如果已经开始构建数字,允许选择 0;否则,从 1 开始
                if(!((mask >> c) & 1)) { // 检查数字 c 是否已经被使用
                    res += dfs(i + 1, is_limit && (c == up), mask | (1 << c), true);
                }
            }
            
            if(!is_limit && is_num) {
                memo[i][mask] = res; // 更新记忆化表
            }
            return res;
        };
        
        return dfs(0, true, 0, false); // 从第 0 位开始递归
    }
};

代码细节说明

  1. 字符串转换

    string s = to_string(n);
    int m = s.size();
    

    将输入数字 n 转换为字符串 s,便于逐位处理。同时,获取数字的总位数 m

  2. 记忆化表

    vector<vector<int>> memo(m, vector<int>(1 << 10, -1));
    

    使用一个二维数组 memo 来存储中间结果。memo[i][mask] 表示在第 i 位,当前位数掩码为 mask 时,所能构成的特殊整数数量。初始化为 -1,表示尚未计算。

  3. 递归函数 dfs

    function<int(int, bool, int, bool)> dfs = [&](int i, bool is_limit, int mask, bool is_num) -> int { /* ... */ };
    

    使用 lambda 表达式定义递归函数 dfs,并捕获所有必要的变量。

  4. 终止条件

    if(i == m) return is_num ? 1 : 0;
    

    当处理到第 m 位时,如果已经开始构建数字(is_numtrue),则返回 1,表示找到一个特殊整数;否则返回 0

  5. 记忆化查找

    if(!is_limit && is_num && memo[i][mask] != -1) return memo[i][mask];
    

    如果当前状态 i, mask 已经被计算过,且不受上界限制,并且已经开始构建数字,则直接返回记忆化的结果,避免重复计算。

  6. 选择跳过当前位

    if(!is_num) {
        res += dfs(i + 1, false, mask, false);
    }
    

    如果还没有开始构建数字,可以选择跳过当前位,继续处理下一位。

  7. 确定当前位的数字范围

    int up = is_limit ? s[i] - '0' : 9;
    for(int c = is_num ? 0 : 1; c <= up; c++) { /* ... */ }
    

    如果当前位受到上界 n 的限制,则最大可以选择的数字为 s[i] - '0';否则,可以自由选择 09。如果已经开始构建数字,允许选择 0;否则,从 1 开始,避免前导零。

  8. 检查数字是否重复

    if(!((mask >> c) & 1)) { /* ... */ }
    

    使用位运算检查数字 c 是否已经被使用过。如果未被使用,则继续递归处理下一位,同时更新 mask

  9. 更新记忆化表

    if(!is_limit && is_num) {
        memo[i][mask] = res;
    }
    

    当当前状态不受上界限制,并且已经开始构建数字时,将结果存入记忆化表。

  10. 递归调用

    return dfs(0, true, 0, false);
    

    从第 0 位开始,初始状态为受上界限制 (is_limit = true)、未使用任何数字 (mask = 0)、且尚未开始构建数字 (is_num = false)。

时间复杂度分析

由于数字 n 的位数最多为 10(因为 n <= 2 * 10^9),递归的状态数为 位数 * 上界限制 * mask * 是否开始构建数字。具体来说,最多为 10 * 2 * 1024 * 2 = 40960,远小于 10^7,因此算法的时间复杂度是可接受的。

总结

通过使用数字动态规划的方法,我们能够高效地计算出区间 [1, n] 内的特殊整数数量。关键在于合理地定义状态并使用记忆化技术避免重复计算。此方法不仅适用于本题,也适用于其他涉及数位限制和组合的计数问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值