1. 问题背景与核心挑战
今天要讨论的是LeetCode第902题"最大为N的数字组合",这是一道典型的中等难度动态规划问题。题目要求我们给定一个数字字符串数组digits和一个整数n,返回可以用digits中的数字组成的、小于等于n的正整数的个数。
这道题的难点在于如何高效处理数字组合与比较的逻辑。直接暴力枚举所有可能的组合显然不现实,特别是当n很大时(比如n是10^9量级),这种方法的计算量会呈指数级增长。我们需要找到一种更聪明的计算方法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题思路分析
2.1 问题分解
首先,我们可以将问题分解为两部分:
- 计算所有位数比n少的数字组合
- 计算位数与n相同但数值不超过n的数字组合
对于第一部分,因为位数少,所以这些数字肯定小于n,可以直接计算组合数。第二部分则需要更细致的比较。
2.2 动态规划方法
我们可以使用动态规划来解决这个问题。定义dp[i]表示从最高位开始处理到第i位时,满足条件的数字组合数。这里需要考虑两种情况:
- 前i-1位已经严格小于n的前i-1位
- 前i-1位等于n的前i-1位,第i位需要小于等于n的第i位
2.3 具体实现步骤
- 将n转换为字符串形式,方便逐位比较
- 预处理digits数组,确保它是排序的
- 计算位数比n少的数字组合总数
- 使用动态规划计算位数与n相同的有效组合数
- 将两部分结果相加得到最终答案
3. 代码实现详解
java复制public int atMostNGivenDigitSet(String[] digits, int n) {
String s = String.valueOf(n);
int m = digits.length;
int k = s.length();
int[] dp = new int[k + 1];
dp[k] = 1;
// 处理位数与n相同的数字
for (int i = k - 1; i >= 0; i--) {
int si = s.charAt(i) - '0';
for (String d : digits) {
int num = Integer.parseInt(d);
if (num < si) {
dp[i] += Math.pow(m, k - i - 1);
} else if (num == si) {
dp[i] += dp[i + 1];
}
}
}
// 处理位数比n少的数字
for (int i = 1; i < k; i++) {
dp[0] += Math.pow(m, i);
}
return dp[0];
}
4. 代码解析与优化
4.1 关键点解析
- 字符串转换:将n转换为字符串s,方便逐位比较
- 动态规划数组:dp数组从后向前计算,dp[i]表示处理到第i位时的有效组合数
- 两种情况处理:
- 当前数字小于n的对应位:后面的位可以任意选择
- 当前数字等于n的对应位:需要看后面的位是否符合条件
4.2 时间复杂度分析
该算法的时间复杂度为O(k*m),其中k是n的位数,m是digits数组的长度。这比暴力枚举的O(m^k)要高效得多。
4.3 空间优化
我们可以将dp数组优化为只使用O(1)的额外空间,因为每次计算只需要前一个状态的值。不过为了代码清晰,这里保留了完整的dp数组。
5. 边界条件与特殊情况
5.1 边界条件处理
- n为0:根据题意,n是正整数,所以不需要处理
- digits为空:直接返回0
- digits包含0:根据题意,数字组合必须是正整数,所以0不应该出现在digits中
5.2 测试用例示例
java复制// 测试用例1
String[] digits1 = {"1","3","5","7"};
int n1 = 100;
// 预期输出: 20
// 测试用例2
String[] digits2 = {"1","4","9"};
int n2 = 1000000000;
// 预期输出: 29523
// 测试用例3
String[] digits3 = {"7"};
int n3 = 8;
// 预期输出: 1
6. 算法优化与变种
6.1 记忆化搜索方法
除了动态规划,我们还可以使用记忆化搜索的方法来解决这个问题。这种方法可能更直观,但实现起来会稍微复杂一些。
6.2 数学组合方法
对于位数比n少的数字组合,我们可以直接使用排列组合公式计算,而不需要逐个累加。这可以稍微提高一些效率。
6.3 支持更大范围的n
如果需要处理更大的n(比如超过long的范围),我们可以将n作为字符串直接输入,避免数值转换的问题。
7. 实际应用场景
这类数字组合问题在实际中有多种应用场景:
- 密码组合计算:计算符合特定规则的密码组合数
- 产品编码生成:生成符合特定规则的序列号
- 数据验证:验证输入是否符合特定的数字格式要求
8. 常见错误与调试技巧
8.1 常见错误
- 位数计算错误:忘记处理位数比n少的情况
- 边界条件遗漏:没有处理n的每一位都大于digits中所有数字的情况
- 重复计算:在动态规划过程中重复计算相同的子问题
8.2 调试技巧
- 打印中间结果:在计算过程中打印dp数组的值,验证计算逻辑
- 小规模测试:先用小的n值测试,确保基本逻辑正确
- 对比暴力解法:对于小规模问题,可以用暴力解法验证结果的正确性
9. 性能优化建议
- 预处理digits:将digits转换为整数数组,避免在循环中重复转换
- 预计算幂次:预先计算并存储不同位数的组合数,避免重复计算
- 提前终止:当发现后续无论如何组合都无法满足条件时,可以提前终止计算
10. 扩展思考
这道题可以有多种变种,比如:
- 严格小于n的数字组合
- 包含特定数字模式的组合
- 支持数字重复使用的组合
每种变种都需要对基本算法进行相应的调整,但核心思路仍然是动态规划或记忆化搜索。
