算法--最大为 N 的数字组合

354 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第10天,点击查看活动详情

题目

leetcode 902. 最大为 N 的数字组合 难度:困难

给定一个按 非递减顺序 排列的数字数组 digits 。你可以用任意次数 digits[i] 来写的数字。例如,如果 digits = ['1','3','5'],我们可以写数字,如 '13', '551', 和 '1351315'。

返回 可以生成的小于或等于给定整数 n 的正整数的个数 。

 

示例 1:

输入:digits = ["1","3","5","7"], n = 100
输出:20
解释:
可写出的 20 个数字是:
1, 3, 5, 7, 11, 13, 15, 17, 31, 33, 35, 37, 51, 53, 55, 57, 71, 73, 75, 77.

示例 2:

输入:digits = ["1","4","9"], n = 1000000000
输出:29523
解释:
我们可以写 3 个一位数字,9 个两位数字,27 个三位数字,
81 个四位数字,243 个五位数字,729 个六位数字,
2187 个七位数字,6561 个八位数字和 19683 个九位数字。
总共,可以使用D中的数字写出 29523 个整数。

示例 3:

输入:digits = ["7"], n = 8
输出:1

提示:

1 <= digits.length <= 9

digits[i].length == 1

digits[i] 是从 '1' 到 '9' 的数

digits 中的所有值都 不同 

digits 按 非递减顺序 排列

1 <= n <= 109

题解

首先,根据题意,我们要计算出通过digits数组中数字的组合,可以生成的小于或等于给定整数n的正整数的个数。那么,我们可以根据两种情况进行分析:

【情况1】n的最高位不等于digits数组中任意数字。

【情况2】n的最高位等于digits数组中的某个数字。

针对于【情况1】,我们以digits = ["1","3","5","7"], n = 8321为例。由于n的最高位是8,它不在digits数组中,并且比任意一个digits数组中的数字都大,那么我们可以得出如下结论:

第1位(8) 可以拼装出的组合数量为:4^4种

第2位(3) 可以拼装出的组合数量为:4^3种

第3位(2) 可以拼装出的组合数量为:4^2种

第4位(1) 可以拼装出的组合数量为:4^1种

针对于【情况2】,我们以digits = ["1","3","5","7"], n = 5321为例。由于n的第1位是5,它并没有大于digits中的所有数字,所以,最高位的拼装数量就一定小于4^4。但是,对于后3位数字的拼装,是没有影响的,所以我们分为两个步骤进行统计:

【步骤1】首先:计算后3位拼装组合,即:4^3 + 4^2 + 4^1 = 84种

【步骤2】然后:计算第1位拼装组合,我们下面再具体分析计算。

由于n的第1位是5,它大于digits数组里的“1”和“3”,所以,针对第1位如果是“1”或“3”的话,最高位可以拼装的组合都是4^3种。

由于n的第1位是5,它等于digits数组里的“5”,针对这种情况,我们就需要再继续看它的下一位数字,即:n的第2位——数字3。

由于n的第2位是3,它大于digits数组里的“1”,所以,针对第2位如果是“1”的话,可以拼装的组合都是4^2种。

由于n的第2位是3,它等于digits数组里的“3”,针对这种情况,我们就需要再继续看它的下一位数字,即:n的第3位——数字2。

/**
 * @param {string[]} digits
 * @param {number} n
 * @return {number}
 */
var atMostNGivenDigitSet = function(digits, n) {
    const s = '' + n;
    const m = digits.length, k = s.length;
    const dp = new Array(k + 1).fill(0).map(() => new Array(2).fill(0));
    dp[0][1] = 1;
    for (let i = 1; i <= k; i++) {
        for (let j = 0; j < m; j++) {
            if (digits[j][0] === s[i - 1]) {
                dp[i][1] = dp[i - 1][1];
            } else if (digits[j][0] < s[i - 1]) {
                dp[i][0] += dp[i - 1][1];
            } else {
                break;
            }
        }
        if (i > 1) {
            dp[i][0] += m + dp[i - 1][0] * m;
        }
    }
    return dp[k][0] + dp[k][1];
};

代码详解

题目说了 digits 是从 1 到 9 的树,没有 0 就好

就是求 digits 中取数合成的数值范围在 [1,n]之间的有多少个

注意合成的数值重复是只算一个的

设 dp[x]表示返回的合法数的个数,那么区间(l,r)的合法数个数就是 ans(l-r) = dp(r) - dp(l-r)

要注意的是,0 是最高位,len-1 是最低位(len 为 n 的长度)

从样例 2 的解释中,我们可以了解到,合法数大概分为这么几类:

a: 位数和 n 相同的,最高位小于 x 最高位 b: 位数相同,最高位相同的 c: 位数小于的 第一个和第三个很简单,主要是中间那个不好说

设 cur 为 n 中第 k 位的数字,要想合成的数字小于 n,那就必须第 k 位在[1,cur-1]这个区间取一个数字。那从 digits 中抽取就好了,找满足这个区间,也就是小于 cur 的最大下标,就设它为 t 吧

题目说了 digits 是有序的,所以上一步中可以使用二分查找来进行优化(其实就 9 个数字,也优化不了多少,啊还增加代码长度,我懒得写了)

digits[t] < cur : 之后的每个位置都有 m 个选择(m 为 digits 长度),m^q个选择, digits[t] == cur :实际上已经算过了,就是前一位时的小于 cur 情况嘛,直接从前一位的 dp[i+1]里面拿就好了,累加一下 大于的情况,直接不合法了,0 个,不算就好了