动态规划(完全背包): 最优硬币组合问题 | 豆包MarsCodeAI刷题

86 阅读4分钟

题目链接

最优硬币组合问题

前置知识

01背包

参考我的另一篇题解01背包

完全背包

完全背包与01背包只有一个区别:01背包每个物品只能取一次,而完全背包的物品无次数限制 虽然看起来变化不大,但代码逻辑有不少出入
OK, 我们从二者代码来分析区别(务必掌握01背包)

nn:表示容量, ww:表示每个物品的重量 vv:表示每个物品的价值 mm:表示物品的数量
01背包简单实现

vector<int>f(n+1);  //等价于其他语言长度为n+1的数组
for(int i = 0; i < m; i++) { //遍历物品 w.size()或v.size()也可以表示物品的数量
     for(int j = n; j >= w[i]; j--) {
         f[i] = max(f[i], f[i - w[i]] + v[i]);
     }
}

可能会出现两个疑问:(应该在01背包的题解里说的 我忘了)

  1. 为什么容量要从大到小遍历 ? 不能从小到大吗?

举个例子来讲为什么不能

n = 5 w = [1,2] v = [1,2]
假设容量从wiw_i 遍历到 n (小于wiw_i不会变)
i=0,wi=1i = 0, w_i=1
n = 1 可以放入w1,f[1]=1+f[0]=1w_1,f[1] =1 + f[0] = 1
n = 2 可以放入w1,f[1]=1+f[1]=2?w_1,f[1] =1 + f[1] = 2? 会发现被物品被多次放入了,原本的f[1]已经存放了w1w_1 被更新了,我们要的应该是旧值f[1] = 0
其实这就是完全背包的方程! 物品可以取无数次 就从小容量到大容量

  1. 能不能先遍历容量,再遍历物品?
    可以,但容量必须从大到小遍历,不然也会出现重复使用的问题,可以手推上面的例子试试

如果你真的理解了01背包,那么完全背包一眼秒,下面是完全背包的简单实现

vector<int>f(n+1);  //等价于其他语言长度为n+1的数组
for(int i = 0; i < m; i++) { //遍历物品 w.size()或v.size()也可以表示物品的数量
     for(int j = w[i]; j <= n; j++) { //比如物品为2 容量为4 此时他会使用2次
         f[i] = max(f[i], f[i - w[i]] + v[i]);
     }
}

OK, 回归本题

解题思路

  • 简化题意:

    • 求恰好凑出amountamount数额的硬币的最小值,我们把coinscoins抽象成ww, 每一个物品的价值v=1v = 1
    • 并且给出对应的硬币
  • 求最少的硬币数很简单,我们只需要把max变成min, v[i] 变成1即可(初始化f时要设置一个大值)

  • 如何求对应的硬币?

    • 思考一下,最优解的情况可能由f[icoins[j]]f[i-coins[j]]得到,这个值又可能是f[...]f[...]得到,其实是有可能形成一条链路的,这句话多半看不懂(我也不知道在写啥) 举个例子

amount=6,coins=[1,2,5]amount = 6, coins=[1,2,5]
ff来记录选择最少的硬币数量 f[i]f[i]表示在数额ii时的最少硬币数,

初始化为INF,f[0]=0(不需要硬币)

拿一个cntcnt来记录数额ii时选择的最优硬币,最开始都没选,那么cnt[...]=0cnt[...] = 0
遍历coins0=1amount=1coins_0 = 1,amount= 1 时, f[1]=min(f[1],f[11]+1)f[1] = min(f[1], f[1-1]+1) 此时有更少的硬币数f[1]=0+1f[1]=0+1,那么当前的硬币是要选择的, 我们把cnt[i]cnt[i]赋值为当前的coini,cnt[1]=1coin_i,cnt[1] =1

遍历coins0=1amount=2coins_0 = 1,amount= 2 时, f[2]=min(f[2],f[21]+1)f[2] = min(f[2], f[2-1]+1) 此时有更少的硬币数f[2]=1+1f[2]=1+1,那么当前的硬币是要选择的, 我们把cnt[i]cnt[i]赋值为当前的coini,cnt[2]=1coin_i,cnt[2] = 1
本轮结束后
f=[0,1,2,3,4,5,6],cnt=[0,1,1,1,1,1]f=[0,1,2,3,4,5,6], cnt=[0,1,1,1,1,1]

第二轮coins1=2coins_1 = 2
数额要从2开始 (1小于2, 不能更新)
amout=2,f[2]=min(f[2],f[22]+1)amout = 2, f[2] = min(f[2], f[2-2]+1) 此时明显f[22]+1=1f[2-2]+1=1更少,更新f[2]f[2]f[2]f[2]更新了,说明在amount=2amount=2时选择的硬币变了,更新cnt[2]=coin[1]=2cnt[2] = coin[1] = 2

本轮结束
f=[0,1,1,2,2,3,3],cnt=[0,1,2,2,2,2,2]f= [0,1,1,2,2,3,3], cnt = [0,1,2,2,2,2,2]

最后一轮 f=[0,1,1,2,2,1,2],cnt=[0,1,2,2,2,5,5]f=[0,1,1,2,2,1,2], cnt = [0,1,2,2,2,5,5]
我们从cnt[amount]开始取值,不断的amountcnt[amount]amount-cnt[amount]就是上一步的最优选择

流程有点乱,我没表达清楚,后面有时间重写这个步骤
简单来说,只要f[icoins[j]]<f[i]f[i-coins[j]] < f[i],那么肯定是选择了当前这个硬币,那么在ii时你选择的硬币就是coins[j]coins[j], 如果把目前这个硬币丢了 就是你上一次选择的最优硬币

通过代码来手推一下可能会更清晰

代码

vector<int> solution(vector<int> coins, int amount) {
    int n = coins.size();
    vector<int>f(amount+1,INT_MAX-1);
    f[0] = 0;
    vector<int>cnt(amount+1);
    for(int i=0; i < n; i++) {
        for(int j = coins[i]; j <= amount; j++) {
            int v = f[j-coins[i]];
            if(v + 1 < f[j]) {
                f[j] = v + 1;
                cnt[j] = coins[i];
            }
        }
        
    }
    vector<int>ans;
    
    while(cnt[amount] != 0) {
        ans.push_back(cnt[amount]);
        amount = amount - cnt[amount];
    }
    if(f[n] == INT_MAX)return {};
    return ans;
    
}