【leetcode】最少的硬币数目

0、参考资料

稍微总结一下我最近遇到的背包问题题解以及一些非常详细的资料讲解:

0-1背包问题:dp[i][j]表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品最多放一次)

分割等和子集问题:定义 dp[i][j]表示从前 i 个数字中选出若干个,刚好可以使得被选出的数字其和为 j。

分割等和子集 属于0-1背包问题

加减的目标值 属于0-1背包问题

完全背包问题:dp[i][j] 表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品有无限个)。

本题 属于完全背包问题:dp[i][j] 表示:从前 i 种硬币中组成金额 j 所需最少的硬币数量。

更加详细的总结与背包问题详解请看:

背包问题大总结与详解一定要看!!!

一、题目描述

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1

二、 代码思路分析

这是一种经典的完全背包问题,完全背包问题就是给定特定容量的背包,让你从特定重量与价格的物品中选出一些填满背包,同时保证价值最大。之所以叫做完全背包,是因为每个物品可以被重复选。

在本题中,转换为完全背包问题:给定总金额(背包容量)从多个硬币中(选择的物品)选出占满特定金额所需的最少硬币(想要达成的目的)。

本题的状态转换公式为:

dp[i][j] 表示:从前 i 种硬币中组成金额 j 所需最少的硬币数量。

稍微总结一下我最近遇到的背包问题题解以及一些非常详细的资料讲解:

0-1背包问题:dp[i][j]表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品最多放一次)

分割等和子集问题:定义 dp[i][j]表示从前 i 个数字中选出若干个,刚好可以使得被选出的数字其和为 j。

分割等和子集 属于0-1背包问题

加减的目标值 属于0-1背包问题

完全背包问题:dp[i][j] 表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品有无限个)。

本题 属于完全背包问题:dp[i][j] 表示:从前 i 种硬币中组成金额 j 所需最少的硬币数量。

更加详细的总结与背包问题详解请看:

背包问题大总结与详解一定要看!!!

三、代码题解

一共有四种题解来不断优化背包问题,可以看我上面引用的参考资料,讲的很棒!目前我只写了第一种最简单的解法。

3.1 从0 - 1背包演化过来的完全背包解法

此解法是最简单理解的,但是也是复杂度最高的

func coinChange(coins []int, amount int) int {
    //经典背包问题,并非是0-1背包,
    //0-1背包问题:dp[i][j]表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品最多放一次)
    //分割等和子集问题:定义 dp[i][j]表示从前 i 个数字中选出若干个,刚好可以使得被选出的数字其和为 j。
    //完全背包问题:dp[i][j] 表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品有无限个)。
    //本题:dp[i][j] 表示:从前 i 种硬币中组成金额 j 所需最少的硬币数量。
    var dp [][]int
    for i := 0; i < len(coins) + 1; i++ {
        ar := make([]int, amount + 1)
        //切片赋值会更好一些,但是下面是错误的
        //ar[:] = amount + 1
        for j := 0; j < amount + 1; j++ {
            ar[j] = amount + 1
        }
        dp = append(dp, ar)
    }
    fmt.Println(dp)
    dp[0][0] = 0
    for i := 1; i < len(coins) + 1; i++ {
        for j := 0; j < amount + 1; j++ {
            for k := 0; k < (j / coins[i - 1]) + 1; k++ {
                //fmt.Println(k)
                //fmt.Println(dp[i - 1][j - k * coins[i - 1]] + k)
                if dp[i][j] > (dp[i - 1][j - k * coins[i - 1]] + k) {
                    dp[i][j] = dp[i - 1][j - k * coins[i - 1]] + k
                }
            }
        }
    }
    if dp[len(coins)][amount] != amount + 1 {
        return dp[len(coins)][amount]
    } else {
        return -1
    }
}
3.2 【二维DP,两层循环】 基于优化后的状态转移方程,可省去第三层循环
func coinChange(coins []int, amount int) int {
    //经典背包问题,并非是0-1背包,
    //0-1背包问题:dp[i][j]表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品最多放一次)
    //分割等和子集问题:定义 dp[i][j]表示从前 i 个数字中选出若干个,刚好可以使得被选出的数字其和为 j。
    //完全背包问题:dp[i][j] 表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品有无限个)。
    //本题:dp[i][j] 表示:从前 i 种硬币中组成金额 j 所需最少的硬币数量。
    var dp [][]int
    for i := 0; i < len(coins) + 1; i++ {
        ar := make([]int, amount + 1)
        //切片赋值会更好一些,但是下面是错误的
        //ar[:] = amount + 1
        for j := 0; j < amount + 1; j++ {
            ar[j] = amount + 1
        }
        dp = append(dp, ar)
    }
    //fmt.Println(dp)
    dp[0][0] = 0
    for i := 1; i < len(coins) + 1; i++ {
        for j := 0; j < amount + 1; j++ {
            //fmt.Println(k)
            //fmt.Println(dp[i - 1][j - k * coins[i - 1]] + k)
            if j < coins[i - 1] {
                dp[i][j] = dp[i - 1][j]
            } else {
                if dp[i - 1][j] > dp[i][j - coins[i - 1]] + 1 {
                    dp[i][j] = dp[i][j - coins[i - 1]] + 1
                } else {
                    dp[i][j] = dp[i - 1][j]
                }
            }
        }
    }
    if dp[len(coins)][amount] != amount + 1 {
        return dp[len(coins)][amount]
    } else {
        return -1
    }
}

参考:https://leetcode.cn/problems/coin-change/solutions/1412324/by-flix-su7s/
3.3 滚动数组,一维dp
func coinChange(coins []int, amount int) int {
    //经典背包问题,并非是0-1背包,
    //0-1背包问题:dp[i][j]表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品最多放一次)
    //分割等和子集问题:定义 dp[i][j]表示从前 i 个数字中选出若干个,刚好可以使得被选出的数字其和为 j。
    //完全背包问题:dp[i][j] 表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品有无限个)。
    //本题:dp[i][j] 表示:从前 i 种硬币中组成金额 j 所需最少的硬币数量。
    var dp = make([]int, amount + 1)
    for i := 1; i < amount + 1; i++ {
        dp[i] = amount + 1 
    }
    dp[0] = 0
    for i := 1; i < len(coins) + 1; i++ {
        var dp2 = make([]int, amount + 1)
        for k := 0; k < amount + 1; k++ {
            dp2[k] = amount + 1 
        }
        for j := 0; j < amount + 1; j++ {
            //fmt.Println(k)
            //fmt.Println(dp[i - 1][j - k * coins[i - 1]] + k)
            if j < coins[i - 1] {
                dp2[j] = dp[j]
            } else {
                if dp[j] > dp2[j - coins[i - 1]] + 1 {
                    dp2[j] = dp2[j - coins[i - 1]] + 1
                } else {
                    dp2[j] = dp[j]
                }
            }
        }
        dp = dp2
    }
    if dp[amount] != amount + 1 {
        return dp[amount]
    } else {
        return -1
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值