暴力搜索 复杂度O(NlogN)

暴力法,需要一个函数判断一个数是否是“好数”。
好数可简化为不含3,4,7的且至少含一个2或5或6或9的数。
对每个数,统计每个数字的出现数量再做判断即可。
时间复杂度O(NlogN),因为要取出每个数的每个数位。

数位DP 复杂度O(logN)

好数的个数 = (只含018,25,69的数字数量) 减去 (只含018)的数字数量
于是需要一个函数快速求1到N中,只含0182569和只含018的数字数量。

定义:F018(n)为区间[0, n)中只含018这3个数字的数的数量。
为了方便计算,需要做以下定义。
1.区间包含0,且左闭右开。
2.该计算方法会为每个数加上前导零,本题中前导零不影响数字合法性。其他题目中若如此分解,可能需要对前导零做特殊处理。

以n=5123为例,该方法将[0, 5123)分解成[0000, 5000),[5000, 5100), [5100, 5120) 和 [5120, 5123)。递归调用F018函数约logN次(每个数位一次)。
于是时间复杂度是O(logN)。函数具体实现见代码。

代码

暴力

class Solution_BruteForce{
    public:
    int nValidNumbers(int n){
        int ANS = 0;
        for(int i = 1 ; i <= n ; ++i){
            string num = to_string(i);
            int C[10] = {};
            for(auto ch : num)
                C[ch-'0']++;
            ANS += C[3] + C[4] + C[7] == 0 && 
                    C[2] + C[5] + C[6] + C[9] > 0;
        }
        return ANS;
    }
};

数位DP

class Solution {
    vector<int> Mask1, Sum1;
    vector<int> Mask2, Sum2;
    void Initialize(){
        // Mask[k]表示k是否属于合法数字
        // Sum[k]表示小于k的合法数字有多少个

        // 合法数字为018,25,69
        //                   0 1 2 3 4 5 6 7 8 9
        Mask1 = vector<int>({1,1,1,0,0,1,1,0,1,1});
        Sum1  = vector<int>({0,1,2,3,3,3,4,5,5,6});

        // 合法数字为018
        //                   0 1 2 3 4 5 6 7 8 9
        Mask2 = vector<int>({1,1,0,0,0,0,0,0,1,0});
        Sum2  = vector<int>({0,1,2,2,2,2,2,2,2,3});
    }
    // 求a的k次方
    int pow(int a, int k){
        int ANS = 1;
        for(int i = 0 ; i < k ; ++i)
            ANS *= a;
        return ANS;
    }
    // 区间[0, num)中有多少个只包含0182569的数
    int F0182569(string &num, int k, int len){
        // 递归终点
        if(k == len)
            return 0;
        // 取出下标k的数字
        int cntNum = num[k] - '0';
        int ANS = 0;
        // 计算数位k上的数小于cntNum时,有多少个合法的数
        // 以 num = 5123 为例
        // k = 0 时计算[0000, 5000) 中合法数量
        // k = 1 时计算[5000, 5100) 中合法数量
        // k = 2 时计算[5100, 5120) 中合法数量
        // k = 3 时计算[5120, 5123)中合法数量

        // 合法数量 = 小于cntNum的数字数量 * 7^(剩余数位个数)
        ANS += Sum1[cntNum] * pow(7, len - k - 1);

        // 计算数位上的数等于cntNum时,有多少个合法的数
        // 以 num = 5123 为例
        // k = 0 时计算[5000, 5123) 中合法数量
        // k = 1 时计算[5100, 5123) 中合法数量
        // k = 2 时计算[5120, 5123) 中合法数量

        // 当前数字合法时,递归求合法数量
        if(Mask1[cntNum])
            ANS += F0182569(num, k+1, len);
        return ANS;
    }
    // 区间[0, num)中有多少个只包含018的数
    // 注释同上面函数
    int F018(string &num, int k, int len){
        if(k == len)
            return 0;
        int cntNum = num[k] - '0';
        int ANS = 0;
        ANS += Sum2[cntNum] * pow(3, len - k - 1);

        if(Mask2[cntNum])
            ANS += F018(num, k+1, len);
        return ANS;
    }
public:
    Solution(){
        Initialize();
    }
    int nValidNumbers(int n){
        // 将n+1转化为字符串
        string num = to_string(n+1);
        // 答案等于 (只含0182569的数字数量) 减去 (只含018的数字数量)
        int ANS = F0182569(num, 0, num.length()) -
                  F018(num, 0, num.length());
        return ANS;
    }
};