`算法题解` `LeetCode` 2376. 统计特殊整数

catalog

  • 题解
    • 更新
  • 注意点
  • 代码

题目链接

题解

问在[1, x]之间的(所有数字) 的某类性质, 这是数位DP的问题;

按照数位DP的规定, Prefix( r)表示: 在[0 - r]之间的所有数字
所以, 将其转换为: Prefix( x) - Prefix( 1 - 1),

数位DP会将[0 - r]的所有数字, 划分为3大类: 以r = 2345
(1, 第一类: 非对齐数字, 为所有< 1000的数, 即[0 - 999]这些)
(2, 第二类: 对齐数字, 为所有[1000, 2344]这些数, 可以发现, 这些数字的长度是相同的)
(3, 第三类: r = 2345)

一般, 优先考虑, (第二类数字) 是否有解; … 因为, 有时第一类数字的问题, 可以用与 (第二类数字) 相同的方法来解决

具体的, 第二类数字, 还会继续划分成:
([1000, 1999])
([2000, 2099] [2100, 2199] [2200, 2299])
([2300, 2309] [2310, 2319] [2320, 2329] [2330, 2339])
([2340, 2340] [2341, 2341] [2342, 2342] [2343, 2343] [2344, 2345])
… 具体规则可以回顾(数位DP)知识.

我们称, 上面这13个区间, 我们对(第二类数字)的处理过程, 就是 对这13个区间的处理; 即, 一个区间, 是我们最小的处理单元;
这每个区间, 他们都是可以表示为: abcd, 且某个前缀是固定的 后缀是0-9的任意
比如, 对于区间[2100, 2199], 他为 (固定前缀为21, 后缀任意), 所有区间都是这样的形式

因此, Aligned( vector< int> pre, int k)函数, 就是针对每个区间, 求解该区间的答案;
pre为固定前缀(没有前导零), k为后缀长度. 该区间有10^k个数; [2100, 2199]对应为Aligned( {2, 1}, 2)
那么, 比如对于[abc] [000 - 999]区间, 他的答案是多少呢?
(1, 如果abc是非法的 即, 存在两位相同, 则, 整个区间全为非法的; ans为0)
(2, 否则, abc互不相同, 则后缀有: [10-3种选择, 10-4种选择, 10-5种选择], 即ans = 7 * 6 * 5, 因为所有位都不同, 就类似于(排列问题).

至此, (第二类数字) 我们已解决;

看第一类数字: [0, 999], 他一般有两种处理方法:
(1, 尝试用Aligned (即第二类数字的处理方法)来解决, 通过将(第一类数字) 添加前导零, 使得 (第一类数字 与 第二类数字 ) 具有相同长度
… 即, 将[0, 999] 变成 [0000, 0999], 即Aligned( {0}, 3)的答案 是否等于 [0, 999]的答案呢?
… 答案是否定的! 0001Aligned里 会按照非法的来处理, 而0001他在[0, 999]里对应为1 他是合法的!!!
… 因此, 不可以添加前导零;
(2, 拆分集合; 将[0, 999]继续划分为: ([0, 0] [1, 9] [10, 99], [100, 999])
… 这样划分的依据在于: (每个区间, 都是等长的)
… 每个区间的长度为: 1, 9, 90, 900, ...
… 除了第一个区间[0, 0], 其他区间 都是: [1 后面全是0, 9 后面全是9]的形式;
… 对于每个区间, 继续进行划分: 比如[100, 999], 划分为: [100, 199] [200, 299] [300, 399] …, [900, 999] (共9个)
… 这样做的好处是: 对于每个小区间[200, 299], 他符合Aligned的形式, 直接调用Aligned( {2}, 2)即可
… … 而这里, 其实对于[1000, 9999], 也可以自己直接算: [9种, 9种, 8, 7, ..], ans = 9 * 9 * 8 * 7 ... (注意, 第一位是[1-9], 其他位都是[0-9])

更新

题目要求的 (特殊数 所有位不同), 那么, 数位DP 是选择计算(特殊数) 还是计算(非特殊数)呢?
… 这要看 [a b c] [000 - 999], 谁能在线性时间 处理这个集合的答案;
… 这里的(非特殊数), 并不是连续的, 比如: 12341是(非特殊数), 它是跳跃的; 这不是DP能处理的
… 而(特殊数), 每个位 都不同, 联系到(排列数)

注意点

  • 这是一个非DP的数位DP, 不需要Build_dp记忆化; 直接暴力即可

代码

bool Is_valid( int _a){
    vector< bool> vis( 10, false);
    for( auto i = _a; i > 0; i /= 10){
        if( vis[ i % 10]){
            return false;
        }
        vis[ i % 10] = true;
    }
    return true;
}
int Aligned( const vector< int> & _pre, int _k){ //< _pre无前导零, _pre不会为空, _k可能为0
    { //* check whether `_pre` is valid
        vector< bool> vis( 10, false);
        for( auto i : _pre){
            if( vis[ i]){
                return false;
            }
            vis[ i] = true;
        }
    }
    int ans = 1;
    int f = 10 - _pre.size();
    FOR_( i, 1, _k, 1){
        ans *= f;
        -- f;
    }
    return ans;
}
int Prefix( int _r){
    if( _r < 0){ return 0;}
    //--
    vector< int> bits;
    for( auto i = _r; i > 0; i /= 10){
        bits.push_back( i % 10);
    }
    reverse( bits.begin(), bits.end());
    if( bits.empty()){ bits.push_back( 0);}
    //--
    int n = bits.size();
    int ans = 0;
    FOR_( i, 0, n - 1, 1){
        int k = n - 1 - i;
        FOR_( h, 0, bits[ i] - 1, 1){
            if( ( i == 0) && ( h == 0)){
                FOR_( j, 0, k, 1){
                    if( j == 0){
                        ans += Is_valid( 0);
//< 此时代表[0, 0]这个区间, 即`0`这个数;   这里不能瞎写! 
//< 虽然题目是[1, r]区间 不涉及0, 但我们的数位DP就是从`0`开始的
//< 而且`Prefix( 0)`会调用(他会调用`Is_valid( 0)`, 不会进入这里, 不会执行(数位二叉树)
                    }
                    else{
                        FOR_( hh, 1, 9, 1){
                            ans += Aligned( { hh}, j - 1);
                        }
                    }
                }
            }
            else{
                vector< int> pre( bits.begin(), bits.begin() + i);
                pre.push_back( h);
                ans += Aligned( pre, k);
            }
        }
    }
    if( Is_valid( _r)){
        ++ ans;
    }
    return ans;
}
int countSpecialNumbers(int n) {
    return Prefix( n) - Prefix( 0);
}

你可能感兴趣的:(算法题解,算法)