【字节面试题】小于n的最大数:C++&测试类、回溯+贪心

【字节面试题】小于n的最大数:C++&测试类、回溯+贪心

题目描述

给定一个数字n,如23121,给定一组数字A,如{2,4,9},求由A中元素组成的,小于n的最大数字,如小于23121的最大数字为22999。

算法思路

使用递归回溯算法并结合贪心策略找出小于N的、由集合A中的数字组成的最大数,算法思路:

  1. 降序排序数字集合,优先使用较大的数字
  2. 使用DFS递归回溯尝试构建答案,结合贪心剪枝
  3. 当发现前面位已经小于n对应位时,后续直接填入最大数字
  4. 如果找不到解,则返回比n少一位且所有位都用最大数字组成的数【1000, {1, 9}, “999”】

题目分析

对于测试用例 {23121, {2, 4, 9}},
初始化阶段

  1. 排序数字集合:将A降序排序得到 {9, 4, 2}
  2. 最大/最小数字:max_digit = 9, min_digit = 2
  3. 转换n为字符串,便于按位处理,避免大数溢出:n_str = “23121”, n_len = 5

递归回溯过程
第一步:处理第一位(pos=0,对应n的第一位"2")

  1. 开始递归,preIsEqual=true
  2. 尝试数字9:9 > 2,不符合条件,跳过
  3. 尝试数字4:4>2,不符合条件,跳过
  4. 尝试数字2:2 == 2,符合条件,path=[2]
  5. 相等,则继续递归,preIsEqual=true (因为2==2)

第二步:处理第二位 (pos=1,对应n的第二位"3")

  1. 尝试数字9:9 > 3,不符合条件,跳过
  2. 尝试数字4:4 >3,不符合条件,跳过
  3. 尝试数字2:2 < 3,符合条件 这时候触发贪心优化
  4. 当前位小于n对应位,后续所有位填充最大数字 path=[2,
    2]
  5. 后续填充:path_str = “2” + “2” + “999” = “22999”
  6. 设置ans ="22999"并返回true,不再继续搜索,算法结束

如果没有贪心优化
如果没有贪心优化,算法会继续递归处理后面的位:

  1. 将第三位填入9,path=[2, 2, 9]
  2. 将第四位填入9,path=[2, 2, 9, 9]
  3. 将第五位填入9,path=[2, 2, 9, 9, 9] 形成数字22999,然后验证22999 < 23121,返回22999

贪心优化的效果

  1. 贪心优化大大减少了递归深度和分支数量:
  2. 早期剪枝:一旦确定前缀比原数小,立即构造结果 避免多余递归:原本需要递归到第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的位数。
在这里插入图片描述
似乎是一道字节内部题库的题,在力扣上没有找到,感觉难度达到了困难,欢迎大家讨论其他解法~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值