LeetCode探索(154):902-最大为 N 的数字组合

146 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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 <= 9
  • digits[i].length == 1
  • digits[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)。

参考