【字节面试题】小于n的最大数:C++&测试类、回溯+贪心
题目描述
给定一个数字n,如23121,给定一组数字A,如{2,4,9},求由A中元素组成的,小于n的最大数字,如小于23121的最大数字为22999。
算法思路
使用递归回溯算法并结合贪心策略找出小于N的、由集合A中的数字组成的最大数,算法思路:
- 降序排序数字集合,优先使用较大的数字
- 使用DFS递归回溯尝试构建答案,结合贪心剪枝
- 当发现前面位已经小于n对应位时,后续直接填入最大数字
- 如果找不到解,则返回比n少一位且所有位都用最大数字组成的数【1000, {1, 9}, “999”】
题目分析
对于测试用例 {23121, {2, 4, 9}},
初始化阶段
- 排序数字集合:将A降序排序得到 {9, 4, 2}
- 最大/最小数字:max_digit = 9, min_digit = 2
- 转换n为字符串,便于按位处理,避免大数溢出:n_str = “23121”, n_len = 5
递归回溯过程
第一步:处理第一位(pos=0,对应n的第一位"2")
- 开始递归,preIsEqual=true
- 尝试数字9:9 > 2,不符合条件,跳过
- 尝试数字4:4>2,不符合条件,跳过
- 尝试数字2:2 == 2,符合条件,path=[2]
- 相等,则继续递归,preIsEqual=true (因为2==2)
第二步:处理第二位 (pos=1,对应n的第二位"3")
- 尝试数字9:9 > 3,不符合条件,跳过
- 尝试数字4:4 >3,不符合条件,跳过
- 尝试数字2:2 < 3,符合条件 这时候触发贪心优化:
- 当前位小于n对应位,后续所有位填充最大数字 path=[2,
2] - 后续填充:path_str = “2” + “2” + “999” = “22999”
- 设置ans ="22999"并返回true,不再继续搜索,算法结束
如果没有贪心优化
如果没有贪心优化,算法会继续递归处理后面的位:
- 将第三位填入9,path=[2, 2, 9]
- 将第四位填入9,path=[2, 2, 9, 9]
- 将第五位填入9,path=[2, 2, 9, 9, 9] 形成数字22999,然后验证22999 < 23121,返回22999
贪心优化的效果
- 贪心优化大大减少了递归深度和分支数量:
- 早期剪枝:一旦确定前缀比原数小,立即构造结果 避免多余递归:原本需要递归到第5位,现在只需要递归到第2位
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
/**
* 算法说明:
* 给定一个正整数n和一个包含数字0-9的集合A,找出小于n且只包含A中数字的最大整数。
*
* 例如:
* 对于测试用例 {54321, {5, 4, 3, 2, 1}},
* 当处理54321时,我们从左往右查找每一位,尝试找到一个位置使得可以构造出小于n的数
* 分析最后一位1时,集合A中没有小于1的数字
* 分析倒数第二位2时,可以用小于2的最大数字1来替换
* 替换后,将最后一位填充为集合A中的最大数字5
* 因此结果是54315
*/
/**
* 递归辅助函数
*
* @param n_str 目标数字的字符串表示
* @param n_len 目标数字的长度
* @param sorted_A 已排序(降序)的数字集合
* @param max_digit 数字集合中的最大数字
* @param path 当前构建的路径
* @param preIsEqual 前面位是否与n对应位相等
* @param result 用于存储结果的引用
* @return 是否找到解
*/
bool dfsHelper(const string& n_str, int n_len, const vector<int>& sorted_A,
int max_digit, vector<int>& path, bool preIsEqual, string& result) {
// 提前返回:如果已经找到结果
if (!result.empty()) return true;
// 递归终止条件:找到一个完整长度的路径
if (path.size() == n_len) {
string path_str = "";
for (int digit : path) {
path_str += to_string(digit);
}
// 检查是否小于n
if (path_str < n_str) {
result = path_str;
return true; // 找到解
}
return false; // 数值等于或大于n,不符合要求
}
// 当前位在n中的索引
int pos = path.size();
int n_digit = n_str[pos] - '0';
// 贪心优化1:如果前面位已经小于n,后续直接填最大数字
if (!preIsEqual) {
string path_str = "";
for (int digit : path) {
path_str += to_string(digit);
}
path_str += string(n_len - path.size(), '0' + max_digit);
result = path_str;
return true;
}
// 贪心优化2:从大到小尝试,优先使用较大的数字
for (int digit : sorted_A) {
// 如果前面位是相等的,当前位需要小于等于n对应位
if (preIsEqual && digit > n_digit) continue;
path.push_back(digit);
// 如果当前位小于n对应位,后续位直接用最大数字填充
if (preIsEqual && digit < n_digit) {
string path_str = "";
for (int d : path) {
path_str += to_string(d);
}
path_str += string(n_len - path.size(), '0' + max_digit);
result = path_str;
path.pop_back();
return true;
}
// 继续递归
bool found = dfsHelper(n_str, n_len, sorted_A, max_digit, path,
preIsEqual && digit == n_digit, result);
if (found) return true;
path.pop_back();
}
return false;
}
/**
* 使用递归回溯算法并结合贪心策略找出小于N的、由集合A中的数字组成的最大数
*
* 算法思路:
* 1. 降序排序数字集合,优先使用较大的数字
* 2. 使用DFS递归回溯尝试构建答案,结合贪心剪枝
* 3. 当发现前面位已经小于n对应位时,后续直接填入最大数字
* 4. 如果找不到解,则返回比n少一位且所有位都用最大数字组成的数
*/
string findMaxNumber(int n, const vector<int>& A) {
// 将数字集合按降序排序
vector<int> sorted_A = A;
sort(sorted_A.begin(), sorted_A.end(), greater<int>());
// 找出集合A中的最大数字和最小数字
int max_digit = sorted_A[0];
int min_digit = sorted_A.back();
// 将n转为字符串,便于按位处理
string n_str = to_string(n);
int n_len = n_str.length();
// 如果n是单位数 5, {2, 4}, "4"
if (n_len == 1) {
int result = -1;
for (int digit : sorted_A) {
if (digit < n && digit > result) {
result = digit;
}
}
return (result == -1) ? "" : to_string(result);
}
// 递归结果和路径
string result = "";
vector<int> path;
// 调用辅助函数
bool isFound = dfsHelper(n_str, n_len, sorted_A, max_digit, path, true, result);
// 如果没找到解,构造一个n-1位的数,每位都用最大数字,1000, {1, 9}, "999"
if (!isFound || result.empty()) {
return string(n_len - 1, '0' + max_digit);
}
return result;
}
/**
* 测试多个用例的函数
* 包括基本测试用例、特殊情况测试和复杂边界测试
*/
void testCases() {
struct TestCase {
int n;
vector<int> A;
string expected;
string description;
};
vector<TestCase> testCases = {
// 基本测试用例
{23121, {2, 4, 9}, "22999", "基本测试用例"},
{5, {2, 4}, "4", "单位数测试"},
{12345, {1, 3, 5, 7, 9}, "11999", "递增序列测试"},
// 特殊形式 10^k 的数字
{10000, {1, 2, 3}, "3333", "10^k形式 - 需要少一位的情况"},
{1000, {1, 9}, "999", "10^k形式 - 仅有两个数字的情况"},
{100, {3, 5, 7}, "77", "10^k形式 - 没有1的情况"},
// 其他用例
{555, {1, 3, 5}, "553", "数字包含在集合A的情况"},
{111, {1, 2}, "22", "最大位数必须减少的情况"},
{1000, {9}, "999", "集合A只有一个数字"},
{54321, {5, 4, 3, 2, 1}, "54315", "修改倒数第二位的情况"},
{9999, {1, 9}, "9991", "从右到左替换的情况"},
{123, {3, 2, 1}, "122", "贪心替换"},
// 复杂用例
{987654, {1, 3, 5, 7, 9}, "979999", "复杂用例 - 需要回溯多位"},
{12000, {1, 2, 8, 9}, "11999", "复杂用例 - 一开始不能找到较小数字"},
{2468, {1, 3, 5, 7, 9}, "1999", "复杂用例 - 所有数字都不在集合A中"},
{99999, {1, 9}, "99991", "复杂用例 - 相同数字多次出现"}
};
int passed = 0;
int total = testCases.size();
for (const auto& tc : testCases) {
// 测试算法
string result = findMaxNumber(tc.n, tc.A);
cout << "N = " << tc.n << ", A = {";
for (size_t i = 0; i < tc.A.size(); i++) {
cout << tc.A[i];
if (i < tc.A.size() - 1) cout << ", ";
}
cout << "}, 结果: " << result;
if (result == tc.expected) {
cout << " ✓ - " << tc.description << endl;
passed++;
} else {
cout << " ✗ (期望: " << tc.expected << ") - " << tc.description << endl;
}
cout << "----------------------------" << endl;
}
cout << "\n测试结果: " << passed << "/" << total << " 通过" << endl;
}
/**
* 主函数
*/
int main() {
// 执行多个测试用例
testCases();
return 0;
}
时间复杂度分析
贪心优化使我们算法的时间复杂度从 O(|A|^L) 降至最好情况下的 O(|A|×L),其中|A|是数字集合的大小,L是数字n的位数。
似乎是一道字节内部题库的题,在力扣上没有找到,感觉难度达到了困难,欢迎大家讨论其他解法~