问题描述
给定一个数n(如23121)和一组数字A(如{2,4,9}),求由A中元素组成的、小于n的最大数。
例如:
- n = 23121, A = {2,4,9} → 结果为 22999
- n = 2533, A = {1,2,4,9} → 结果为 2499
- n = 988822, A = {4,2,9,8} → 结果为 988499
- n = 9, A = {9,8} → 结果为 8
- n = 56449, A = {9,6,3,5} → 结果为 56399
解题思路
这道题本质上是一个数字组合问题,需要在给定数字集合中选出数字组成一个小于目标数n的最大数。关键点在于:
- 数字可重复使用:题目没有说明不能重复使用,从示例22999可以看出是可以重复的
- 需要小于n:不能等于n
- 位数可以不同:当无法组成与n相同位数的数时,可以退而求其次,组成位数少一位的最大数(全部用最大数字填充)
算法设计
采用回溯法(Backtracking)来解决:
- 将目标数n转为字符串,方便逐位处理
- 将可用数字排序,便于选择
- 从最高位开始,逐位尝试放置数字
- 维护一个标志位表示之前的所有位是否都与n的前缀相等
- 如果之前的前缀已经小于n,后面可以直接放最大数字
- 如果之前的前缀等于n,当前位需要选择小于等于当前位的数字
- 如果无法找到合适的数字,回溯到上一位
代码实现
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
string res;
bool backtrack(const string &n_str, const vector<int> &digits, int index, bool equalPrefix)
{
// 到达末尾,返回是否找到了小于n的数
if (index == n_str.length())
{
return !equalPrefix;
}
// 如果前缀已经小于n,后面直接放最大数字
if (!equalPrefix)
{
res += char(digits.back() + '0');
backtrack(n_str, digits, index + 1, false);
return true;
}
else
{
int cur_digit = n_str[index] - '0';
// 从大到小尝试可能的数字
for (int i = digits.size() - 1; i >= 0; i--)
{
if (digits[i] <= cur_digit)
{
res += char(digits[i] + '0');
bool nextEqual = (digits[i] == cur_digit);
if (backtrack(n_str, digits, index + 1, nextEqual))
{
return true;
}
res.pop_back(); // 回溯
}
}
return false;
}
}
int maxLessThanN(int n, vector<int> digits)
{
string n_str = to_string(n);
sort(digits.begin(), digits.end());
res = "";
backtrack(n_str, digits, 0, true);
// 如果没找到,构造位数少一位的最大数
if (res.empty())
{
if (n_str.length() > 1)
{
string result(n_str.length() - 1, char(digits.back() + '0'));
return stoi(result);
}
return -1; // n为个位数且没有合适的数字
}
return stoi(res);
}
测试用例
int main()
{
vector<pair<int, vector<int>>> test_cases = {
{23121, {2, 4, 9}},
{2533, {1, 2, 4, 9}},
{988822, {4, 2, 9, 8}},
{11, {9, 8}},
{333, {9, 8}},
{56449, {9, 6, 3, 5}}};
for (const auto &test : test_cases)
{
int n = test.first;
const vector<int> &digits = test.second;
int result = maxLessThanN(n, digits);
cout << "n = " << n << ", A = {";
for (size_t i = 0; i < digits.size(); i++) {
cout << digits[i];
if (i < digits.size() - 1) cout << ", ";
}
cout << "} → " << result << endl;
}
return 0;
}
运行结果
n = 23121, A = {2, 4, 9} → 22999
n = 2533, A = {1, 2, 4, 9} → 2499
n = 988822, A = {4, 2, 9, 8} → 988499
n = 11, A = {9, 8} → 9
n = 333, A = {9, 8} → 99
n = 56449, A = {9, 6, 3, 5} → 56399
算法分析
- 时间复杂度:O(m * k),其中m是n的位数,k是可用数字的个数。最坏情况下需要回溯,但每个位置最多尝试k次。
- 空间复杂度:O(m),递归栈的深度。
关键点总结
- 回溯策略:当遇到无法继续的情况时,需要回退到上一位尝试更小的数字
- 前缀相等标志:通过equalPrefix标志来跟踪当前构造的数与目标数的前缀关系
- 位数降级:当无法构造出与n相同位数的数时,自动降级到少一位的最大数
- 贪心选择:在每个位置上,优先尝试最大的可能数字,这样能保证找到的数最大
扩展思考
- 如果数字集合中的数字不能重复使用,该如何修改?
- 如果需要找到大于n的最小数,算法该如何调整?