用给定集合中的数字组成小于 n 的最大数,数字可以重复使用。
未标记
考查重点:严格上界与数位回退。从高位选可用数字;完整结果等于 n 或后续无解时,回退到能改选更小数字的位置,余位填最大数字;必要时缩短位数,数字可重复使用并处理前导零
本题目录
参考答案
核心回答
按题目示例,集合中的每个元素都是一位十进制数字,允许重复使用,结果必须严格小于 n。例如 n=23121、A={2,4,9},答案是 22999,不能按“每个数字最多使用一次”的排列题处理。
可以从高位构造:尽量保留与上界相同的前缀;某位选得更小时,后面全部填集合中的最大数字;如果当前位没有合法选择,就向左回退,把最近一个能减小的位置减小,再把后缀填满。等长无解时,考虑少一位的最大数。
下面约定 n 是非负整数,结果不允许前导零;只有集合含 0 且 0 < n 时,才允许单独返回 0。无解返回 None。如果题目只接受正整数,将返回值 0 视为无解即可。
关键思路:先处理严格小于,再维护前缀
对整数,把“小于 n”转成“不超过 n−1”,可以统一处理答案刚好等于上界的情况。例如 n=222、A={2},不能返回 222,而应返回 22。
令新的上界为 upper=n−1,从左到右逐位构造:
- 如果能选与当前上界数位相等的数字,先选相等,继续保持前缀约束。
- 如果只能选更小的数字,选其中最大的。此时整个数已经小于上界,剩下各位全部填集合中的最大数字,不再受上界后缀限制。
- 如果这一位没有合法数字,保留当前已选前缀并向左回退。在最近一个可减小的位置,换成集合中严格小于原数字的最大值,再把它右侧全部填最大数字。
- 如果整个前缀都不能减小,等长无解。集合存在非零数字时,用最大数字重复组成少一位的数;否则只可能有单独的
0,或无解。
多位数首位不能选 0,但后面的 0 可以正常使用。选过一个数字后也不从集合中删除,因此始终允许重复取用。
Python 实现
bisect_right(...)-1 查找不超过当前数位的最大数字,bisect_left(...)-1 查找严格更小的最大数字。实现使用迭代和前缀列表,不依赖递归深度。
from bisect import bisect_left, bisect_right
def max_below(n: int, allowed) -> int | None:
if type(n) is not int or n < 0:
raise ValueError("n must be a nonnegative integer")
raw = list(allowed)
if any(type(d) is not int or not 0 <= d <= 9 for d in raw):
raise ValueError("allowed must contain decimal digits")
digits = sorted(set(raw))
if n == 0 or not digits:
return None
upper = str(n - 1)
length = len(upper)
largest = str(digits[-1])
prefix = []
for i, char in enumerate(upper):
limit = int(char)
j = bisect_right(digits, limit) - 1
if j < 0 or (i == 0 and length > 1 and digits[j] == 0):
break
digit = digits[j]
prefix.append(str(digit))
if digit < limit:
return int("".join(prefix) + largest * (length - i - 1))
else:
return int("".join(prefix))
while prefix:
i = len(prefix) - 1
old = int(prefix.pop())
j = bisect_left(digits, old) - 1
if j >= 0 and not (i == 0 and length > 1 and digits[j] == 0):
result = "".join(prefix) + str(digits[j])
return int(result + largest * (length - i - 1))
if length > 1 and digits[-1] != 0:
return int(largest * (length - 1))
return 0 if 0 in digits else None
assert max_below(23121, [2, 4, 9]) == 22999
assert max_below(24132, [2, 4, 9]) == 22999
assert max_below(222, [2]) == 22
assert max_below(1000, [0, 1]) == 111
assert max_below(1, [0]) == 0
assert max_below(8, [9]) is None
assert max_below(0, [0]) is None
assert max_below(100, []) is None
代码中的 for ... else 表示所有数位都成功走完,没有因为无候选而 break。此时构造结果恰好等于 n−1,符合严格小于 n 的要求。
为什么回退后可以直接填最大数字
没有前导零的等长十进制数,其大小由第一个不同的数位决定。更高位的差异,后面所有数位都无法弥补。
所以,保持更长的相同前缀优于更早减小;不得不减小时,应优先在最靠右的可行位置减小,并选择该位置能用的最大较小数字。这个位置一旦减小,后缀就可以全部取最大数字。题目允许重复取数,且没有相邻关系、数位和等额外限制,因此这样的后缀一定合法。
若等长无解,任何合法的少一位正数都小于上界,而少一位的最大数又大于所有更短的正数,所以不必枚举全部短长度。
例如 n=24132,上界为 24131。先保留 24,第三位 1 没有可用数字;退回前缀的第二位,把 4 改成 2,于是得到 22,后缀全填 9,答案为 22999。
复杂度
设输入数字列表长度为 m,去重后有 d 个数字,L 为上界位数。忽略任意精度整数与十进制字符串转换的成本,预处理与数位构造共需 O(m + d log d + L log(d+1)) 时间,额外空间 O(m + d + L)。每个前缀位置最多前进、回退各一次,不会枚举所有数字组合。十进制中 d≤10,数位构造部分可以视为 O(L)。
常见追问
为什么不能当前位放不下,就直接填最大数字?
如果前缀仍与上界相等,这样可能超过上界;必须先找到一个能变小的位置。例如 24131 的前缀已是 24,第三位不能放 2 或 9,需要先退回第二位。
集合含 0 时有什么变化?
多位数的首位不能是零,其他位置可以。全是零的集合只能组成数值 0,不能用 000 冒充一个三位数。无解也不能无条件返回 0,因为 0 可能根本不在集合里。
如果数字不能重复,或还要求相邻数位不同呢?
那就不能再直接用最大数字填满后缀。需要增加已使用数字、上一位等状态,判断后缀是否存在合法完成方式;可以采用带上界约束的回溯或数位 DP,再按从大到小的顺序恢复最大解。这是另一组约束,不能直接沿用当前算法的正确性证明。
参考资料
- 国内:za0429:字节面试题,小于 n 的最大数:借用原帖 23121 与 {2,4,9} 的同题示例和高位优先搜索思路,确定正文的可重复取数题意;严格比较、短位数和零按本文输入约定处理。
- 国内:OI Wiki:数位 DP:依据贴上界、脱离上界与前导零状态的区分,解释正文中保留相等前缀及后缀直接填最大值的构造。
- 国内:Hello 算法:回溯算法:参考撤销选择与约束剪枝,说明正文中回退前缀的必要性,以及可重复取数、后缀无额外约束时能够直接填满的原因。
- 国外:Python 3.14:bisect:依据左右插入点和 find_lt、find_le 的比较契约,说明正文中两个前驱查找分别采用 bisect_left 与 bisect_right 的原因。
- 国外:USACO Guide:Digit DP:借鉴数位位置、under 与 started 状态,说明正文中前缀约束,以及增加数位条件后需要扩展为数位 DP 的原因。
相关面经
4 篇字节跳动
Agent给定一个n和集合A,找到A组合的数字中小于n的最大值,可以重复选用。
字节跳动
Agent 开发工程师给定一个数n,如23121;给定一组数字A如{2,4,9},求由A中元素组成的小于n的最大数,如小于23121的最大数是22999。
字节跳动
Agent 开发实习生小于n的最大数 数组A中给定可以使用的1~9的数,返回由A数组中的元素组成的小于n的最大数。例如A={1, 2, 4, 9},n=2533,返回2499
字节跳动
后端开发(Agent 方向)手撕:给定一个数n,如23121,给定一组数字a,如{2,4,9},求a中元素组成的小于n的最大值,如小于23121的最大值为22999
