学习方法与心得3 | 豆包MarsCode AI 刷题

186 阅读4分钟

最优硬币组合问题题解

1. 问题描述

小C有多种不同面值的硬币,每种硬币的数量是无限的。他希望知道,如何使用最少数量的硬币,凑出给定的总金额N。小C对硬币的组合方式很感兴趣,但他更希望在满足总金额的同时,使用的硬币数量尽可能少。 例如:小C有三种硬币,面值分别为 1, 2, 5。他需要凑出总金额为 18。一种最优的方案是使用三个 5 面值的硬币,一个 2 面值的硬币和一个 1 面值的硬币,总共五个硬币。

2. 解题思路

2.1 问题分析

该问题属于经典的“硬币组合问题”变体,目的是在给定的硬币面值和目标金额的情况下,找到一种硬币组合,使得总金额达到目标值且硬币数量最少。小C拥有面值为1, 2, 5的硬币,需要凑出总金额为18。根据题意,显然这是一个“最优化”问题,其中目标是最小化所用的硬币数量。

在解决此类问题时,常用动态规划和深度优先搜索(DFS)算法。这里采用了深度优先搜索算法,通过遍历所有可能的组合,找到满足条件的最小硬币组合。在DFS中,我们递归地选择硬币面值,当总金额等于0时保存当前组合并更新最小硬币数。

2.2 算法思想与模板

该算法使用深度优先搜索(DFS)配合剪枝优化来解决问题,整体流程如下:

  1. 排序和降序排列:首先对硬币面值从大到小进行排序,以便优先使用面值大的硬币,从而更快凑出目标金额。优先选用大面值的硬币可以减少组合中的硬币数量,加速收敛到最优解。

  2. DFS递归:递归过程中维护两个参数,i表示当前硬币面值的索引,amount表示当前还需凑的金额。

    • amount0,则表示当前组合已经达成目标,此时更新最优解。
    • amount < 0i超出硬币面值数组,则回溯到上一层。
  3. 剪枝优化:在DFS中,当发现当前组合的硬币数量已经大于当前的最优解时,直接停止递归,避免无效的进一步计算。

下面是DFS的代码模板:

void dfs(int i, int amount, vector<int> & combination, vector<int> &coins) {
    if (amount == 0) {
        // 更新最小硬币数及最优组合
    }
    if (amount < 0 || i >= coins.size()) {
        return;
    }
    combination.push_back(coins[i]);
    dfs(i, amount - coins[i], combination, coins);
    combination.pop_back();
    dfs(i + 1, amount, combination, coins);
}

在递归过程中,通过combination.push_back(coins[i])combination.pop_back()分别选择或取消当前硬币面值,以探索不同的组合方式。

3. 代码

#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;

vector<int> ans;
vector<int> best_combination;
int min_coins = INT_MAX;

bool cmp(int a, int b) {
    return a > b;
}

void dfs(int i, int amount, vector<int> &combination, vector<int> &coins) {
    if (amount == 0) {
        if (combination.size() < min_coins) {
            min_coins = combination.size();
            best_combination = combination;
        }
        return;
    }
    if (amount < 0 || i >= coins.size()) {
        return;
    }

    combination.push_back(coins[i]);
    dfs(i, amount - coins[i], combination, coins);

    combination.pop_back();
    dfs(i + 1, amount, combination, coins);
}

vector<int> solution(vector<int> array, int total) {
    sort(array.begin(), array.end(), cmp);
    vector<int> combination;
    dfs(0, total, combination, array);
    return best_combination;
}

int main() {
    vector<int> result = solution({1, 2, 5}, 18);
    for (int num : result) {
        cout << num << " ";
    }
    cout << endl;
    cout << (result == vector<int>({5, 5, 5, 2, 1})) << endl;
    return 0;
}

4. 代码详解

  • 变量初始化

    • best_combination:用于存储当前最优解的组合。
    • min_coins:记录当前组合的最少硬币数,初始值为INT_MAX
  • 自定义排序函数cmp

    • 定义降序排序函数cmp,保证大面值硬币优先使用。
  • dfs函数

    • 递归地查找硬币组合,通过combination.push_back(coins[i])combination.pop_back()操作控制当前选择的硬币。
    • amount == 0时,组合已达成目标,更新最优解。
    • 剪枝条件:如果amount < 0或索引i超出边界,立即返回避免无效搜索。
  • 主函数solution

    • 调用sort对硬币数组排序,然后初始化组合并调用dfs搜索。
  • 测试输出

    • 使用示例{1, 2, 5}和目标金额18,输出最优组合。

5. 性能分析

该算法采用DFS实现,时间复杂度为O(2n)O(2^n)n为硬币种类数量)。尽管DFS效率较低,但对于小规模硬币和目标金额问题较为适用。

6. 图解分析

以下是DFS过程的图解说明,以简化的硬币集{1, 2}和目标金额4为例:

       dfs(0, 4)                     // 初始值
         /      \
    dfs(0, 3)   dfs(1, 4)
     /      \     /       \
dfs(0, 2) dfs(1, 3) dfs(1, 2) dfs(2, 4)

每次选择一种硬币并递归,直至找到最优解,或跳出递归。