持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第19天,点击查看活动详情
题目
给定一个按 非递减顺序 排列的数字数组 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 <= 9digits[i].length == 1digits[i]是从'1'到'9'的数digits中的所有值都 不同digits按 非递减顺序 排列1 <= n <= 10^9
思考
本题难度困难。
首先是读懂题意。给定一个按 非递减顺序 排列的数字数组 digits ,可以用任意次数 digits[i] 来组合数字。最终返回 可以生成的小于或等于给定整数 n 的正整数的个数。
本题确实有点难,没有什么优雅的解题方法。这里使用数位动态规划和二分查找的方法。首先将 digits 数组转为数字数组,方便后续的比较。对于数字 n,获取对应的数位数组 list,比如100 -> [0, 0, 1]。
考虑以下两种情况:
- i. 位数和 x 相同。
我们需要考虑如何求得数组 digits 的区间[l, r]范围内合法数字的个数。这里借助二分查找的方法。
- ii. 位数比 x 少的。
从数字长度1开始,直至 n - 1,我们需要统计这几种情况中的数字个数。比如 list 为 100 时, 位数1有4个数字,位数2有4*4个数字。
最后,把以上结果累加并返回即可。
解答
方法一:数位动态规划 + 二分查找
/**
* @param {string[]} digits
* @param {number} n
* @return {number}
*/
var atMostNGivenDigitSet = function(digits, n) {
const nums = digits.map(num => parseInt(num))
return dp(nums, n)
}
const dp = (nums, x) => {
const list = []
while (x != 0) {
list.push(x % 10)
x = parseInt(x / 10)
}
// console.log(list) // 100 -> [0, 0, 1]
let n = list.length, m = nums.length, ans = 0
// i.位数和 x 相同
for (let i = n - 1, p = 1; i >= 0; i--, p++) {
let cur = list[i]
// 考虑如何求得区间[l, r]范围内合法数字的个数
// 二分查找
let l = 0, r = m - 1
while (l < r) {
let mid = (l + r + 1) >> 1
if (nums[mid] <= cur) l = mid
else r = mid - 1
}
// 分情况讨论
if (nums[r] > cur) {
// 不满足
break
} else if (nums[r] === cur) {
// 当前位置共有 r 种选择,而后面共有 n - p 个位置,每个位置都有 m 种选择
ans += r * Math.pow(m, (n - p))
if (i === 0) ans++
} else if (nums[r] < cur) {
// 当前位置共有 r + 1 种选择
ans += (r + 1) * Math.pow(m, (n - p))
break
}
}
// ii.位数比 x 少的
// 比如 list 为 100 时, 位数1有4个数字,位数2有4*4个数字
for (let i = 1, last = 1; i < n; i++) {
let cur = last * m
ans += cur
last = cur
}
return ans
}
复杂度分析:
- 时间复杂度:O(logn),由于 digits 最多存在 9 个元素,因此二分查找的复杂度可以忽略。需要遍历 n 的所有数位的数字,n 含有的数字个数为 log10n,因此整体复杂度为 O(logn)。
- 空间复杂度:O(1)。