LeetCode 902. 最大为 N 的数字组合
题目描述


题意分析
digits中的数字互不相同、已经升序排列,并且都在1到9之间。每个数字可以重复使用,要求统计能组成多少个不超过n的正整数。由于没有
0,不需要处理前导零。设n有L位、可选数字有D个:不足L位的数一定合法,恰好L位的数需要逐位比较,更多位的数一定超过n。
解法:按位计数
核心思路
[!blue]
长度为
k时,每一位都有D种选择,共有 $D^k$ 个数。因此先把长度1到L - 1的数量相加。处理等长数字时,从高位向低位走,并始终让已经选定的前缀与
n相同。两个等长整数的大小由第一个不同的数位决定:当前位选得比n[i]小,整个数就一定更小,剩下的L - i - 1位可以任意填,贡献 $D^{L-i-1}$;当前位选得更大则不合法。只有选中与
n[i]相同的数字,才需要继续比较下一位。如果集合中没有这个数字,更小的分支已经计数,更大的分支都不合法,可以直接结束。若所有位都能相等,说明n本身也能组成,最后再加1。每个小于
n的等长数字都有唯一的“首次变小位置”,所以逐位累加既不会漏计,也不会重复。
解题步骤
- 将
n转成字符串,得到位数L;预计算power[i] = D^i。power[0] = 1表示后面没有数位时,当前选择仍对应一个数。- 累加
power[1]到power[L - 1],计入所有位数更短的数。- 从左到右扫描
n。在第i位枚举可选数字,每遇到一个小于n[i]的数字,就加上power[L - i - 1]。- 用
equal记录能否选择n[i]。数字已经升序,遇到更大的候选值可以停止内层枚举;若equal仍为假,返回当前答案。- 全部数位匹配成功后,返回
ans + 1。
代码实现
class Solution {
public int atMostNGivenDigitSet(String[] digits, int n) {
String value = String.valueOf(n);
int m = digits.length;
int length = value.length();
int[] power = new int[length];
power[0] = 1;
for (int i = 1; i < length; i++) {
power[i] = power[i - 1] * m;
}
int ans = 0;
for (int len = 1; len < length; len++) {
ans += power[len];
}
for (int i = 0; i < length; i++) {
char cur = value.charAt(i);
boolean equal = false;
for (String digit : digits) {
char ch = digit.charAt(0);
if (ch < cur) {
// 当前位首次小于上限后,剩余每一位都可自由选择。
ans += power[length - i - 1];
} else if (ch == cur) {
equal = true;
} else {
break;
}
}
// 无法延续相等前缀时,这条受上限约束的分支已经结束。
if (!equal) {
return ans;
}
}
// 所有位都能与上限相同,再补上上限自身。
return ans + 1;
}
}
import "strconv"
func atMostNGivenDigitSet(digits []string, n int) int {
value := strconv.Itoa(n)
m := len(digits)
length := len(value)
power := make([]int, length)
power[0] = 1
for i := 1; i < length; i++ {
power[i] = power[i-1] * m
}
ans := 0
for digitsCount := 1; digitsCount < length; digitsCount++ {
ans += power[digitsCount]
}
for i := 0; i < length; i++ {
cur := value[i]
equal := false
for _, digit := range digits {
ch := digit[0]
if ch < cur {
// 当前位首次小于上限后,剩余每一位都可自由选择。
ans += power[length-i-1]
} else if ch == cur {
equal = true
} else {
break
}
}
// 无法延续相等前缀时,这条受上限约束的分支已经结束。
if !equal {
return ans
}
}
// 所有位都能与上限相同,再补上上限自身。
return ans + 1
}
复杂度分析
- 时间复杂度:$O(L \cdot D)$,其中
L是n的位数,D是可用数字个数。幂表预计算为 $O(L)$。- 空间复杂度:$O(L)$,用于保存
n的字符串表示和幂表。
关键点总结
[!green]
- 按位数拆分后,只有与
n等长的部分需要受上限约束。- 逐位过程只延续相等前缀;一旦选小,整段后缀直接用幂表计数。
- 小于
n的数按首次变小的位置归类,等于n的情况单独补上。
易错点总结
[!yellow]
- 自由计数只到
L - 1位;长度L必须逐位比较,否则会把大于n的数也算进去。- 当前位已经选定,剩余位数是
L - i - 1;最后一位选小后应当贡献power[0] = 1。- 无法延续相等前缀时要结束整个过程,继续处理后续位就会为不存在的前缀计数。
- 当
n比所有可选数字都小时,答案自然为0;全部位匹配成功时则必须计入n本身。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 357. 统计各位数字都不同的数字个数 | 中等 | 同样逐数位计数,本题限制可用数字集合,原题限制数码不能重复。 |
| 233. 数字 1 的个数 | 困难 | 同样利用高位前缀对候选分块,本题计合法整数个数,原题累计数码出现次数。 |
| 补充题 162. 指定数字组成的小于 N 的最大整数 | 中等 | 都从高位维持与 N 相同的前缀,首次选更小数字后处理后缀;本题计数,补充题求最大可行值。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!