322. Coin Change--硬币找零钱问题

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35923749/article/details/86619525

硬币找零钱问题 Coin Change

硬币找零产生的一系列问题,都是属于动态规划问题,所以,在此整理和总结相关的问题,需要自己能更好的掌握动态规划问题。

一、每种面值的货币可以使用任意张,求最小货币数

1. 题目
题目链接:322. Coin Change

You are given coins of different denominations and a total amount of money amount. Write a function to compute the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1.
Example 1:
Input: coins = [1, 2, 5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1
Example 2:
Input: coins = [2], amount = 3
Output: -1
Note:
You may assume that you have an infinite number of each kind of coin.

2. 题目分析
给定数组coins,coins中所有的值都是正数且不重复。每个值代表一种面值的硬币,每种面值的货币可以使用任意张,再给定一个整数aim代表要找的钱数,求组成amount的最少货币数。

3. 解题思路
我们用到的样例是面值数组coins=[1, 2, 5], 总额amount = 11;
我们用dp[m][n]来表示所有可能的状态,m表示硬币的种类数,n表示需要凑齐的总额;
则,dp[i][j]表示,用前i(0 <= i <= m)种面值的硬币,去凑齐总额为j(0 <= j <= n),所需要硬币的最少数目。

动态规划,常常适用于有重叠子问题最优子结构性质的问题。若要解一个给定问题,我们需要解其不同部分(即子问题),再通过条件过滤合并子问题的解以得出原问题的最优解。 通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。所以动态规划就是把大问题逐步分解成容易解决的最基础的小问题,然后通过寻找最基础的问题和大问题的关系,找到大问题的解。

1). 那么我们接下来开始解决最基础的小问题。(注:最基础的小问题其实就是做一些初始化,根据动态规划相关的算法题,由于都是采用备忘录的方式解决算法回溯问题,而备忘录记忆化存储的方式是自下而上分析的,所以必须一开始要有初值,即,我们开始必须进行一些相关的初始化,因为后面的解都是根据前面更小的问题产生的结果做出的选择

  • j=0,就是要凑齐的总额为0,不管使用的硬币种类时,所需要的最小货币数,就是 j 最基本的问题了。很明显,可以是0张2,0张5,0张1,也就是说dp[i][0] = 0 (0 <= i <= n),因为创建数组时,在java中会自动给数组中的每个元素赋初值0,所以一般这个步骤不需要程序猿显示用代码实现;
  • i=0,就是用面值为coins[0]的硬币能凑齐任意总额(小于amount11美元),所需要的最小货币数,就是 i 最基本的问题了。很明显,cosin[0]=1, 1美元需要最少使用1张1美元的硬币,2美元需要最少使用2张1美元的硬币,3美元需要最少使用3张1美元的硬币,,,11美元需要最少使用11张1美元的硬币。

2)找到子问题与大问题的联系,即状态转移方程,要找到状态转移方程,首先要先分析每个子问题产生的情况,然后通过条件筛选找到子问题的最优解,从而能最终找到大问题的最优解。子问题是求解每个dp[i][j],即用前i(0 <= i <= m)种面值的硬币,去凑齐总额为j(0 <= j <= n),从而找到凑齐硬币所需要的最少数目硬币数。
coins[0](i=0)的问题解决了,那么i= 1,2,…n的问题,肯定也是从上面这个问题来的,怎么来的呢?
对于第 i 枚硬币有两种选择:用它来找零 和 不用它找零。

  • 用它来找零:
    dp[i][j] = dp[i][j - coins[i]] + 1; 表示 使用 第 i 枚硬币找零时,对金额为 j 进行找钱所需要的最少硬币数。由于用了第 i 枚硬币,故使用的硬币数量要增1;
  • 不用它来找零:
    dp[i][j] = dp[i - 1][j];表示 不使用第 i 枚硬币找零时(即使用硬币种类0 ~ i-1),对金额为 j 进行找钱所需要的最少硬币数;

总共就这两种可能,我要找最少硬币数量的情况,则这一步的状态转移方程(dp[i][j]的最优解):
dp[i][j] = min(dp[i][j - coins[i]] + 1, dp[i - 1][j]);

4. 代码实现(java)

public static int leastCoinsWithNoLimit(int[] coins, int amount) {
        if (coins==null || amount<1){
            return 0;
        }
        int[][] dp = new int[coins.length][amount+1];
        for (int j = 1; j <= amount; j++) {
            dp[0][j] = Integer.MAX_VALUE;
            if (j>=coins[0] && dp[0][j-coins[0]] != Integer.MAX_VALUE){
                dp[0][j] = Math.min(dp[0][j],dp[0][j-coins[0]]+1);
            }
        }

        for (int i = 1; i < coins.length; i++) {
            for (int j = 1; j <= amount; j++) {
                dp[i][j] = Integer.MAX_VALUE;
                if (j>=coins[i]){
                    dp[i][j] = Math.min(dp[i-1][j],dp[i][j-coins[i]]+1);
                }else{
                    dp[i][j] = dp[i-1][j];//如果j小于当前面值,那明显不能用当前面值,只能继承前面的
                }
            }
        }
        return dp[coins.length-1][amount] == Integer.MAX_VALUE ? -1 : dp[coins.length-1][amount];
}
//内存优化,使用一位数组
public static int leastCoinsWithNoLimit(int[] coins, int amount) {
        
        if (coins == null || coins.length == 0 || amount < 0) {
            return -1;
        }

        //dp[i]=j表示组成i元,最少需要j个硬币
        int[] dp = new int[amount + 1];
        for (int j = 0; j <= amount; j++) {
            dp[j] = Integer.MAX_VALUE;
        }
        dp[0] = 0;
        for (int i = 0; i < coins.length; i++) {
            for (int j = 1; j <= amount; ++j) {
                if (j >= coins[i] && dp[j - coins[i]] != Integer.MAX_VALUE) {
                    dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
                }
            }
        }
        return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}

二 、每种面值的硬币可以使用任意张,能凑齐总额为amount的硬币组合数

1. 题目
题目链接:518. Coin Change 2

You are given coins of different denominations and a total amount of money. Write a function to compute the number of combinations that make up that amount. You may assume that you have infinite number of each kind of coin.
Example 1:
Input: amount = 5, coins = [1, 2, 5]
Output: 4
Explanation: there are four ways to make up the amount:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
Example 2:
Input: amount = 3, coins = [2]
Output: 0
Explanation: the amount of 3 cannot be made up just with coins of 2.

2. 题目分析
给定数组coins,coins中所有的值都为正数且重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数amount代表要找的钱数,求换钱有多少种方法。

3. 解题思路
分析思路与前面说到的动态规划思路是一样的。类比题型1,每个面值的钱可以使用任意多次,我们可以构造一个dp数组,如dp数组的行数为m(m=coins.length),列数为amount+1(总金额为0-amount)。
dp[i][j] 的含义是:在可以任意使用coins[0…i]货币的情况下,组成总金额 j 有多少种组合方法。

1)首先进行初始化:

  • j=0,就是要组成总金额为0的硬币组合方法,就是 j 最基本的问题了。很明显,对每种面额的硬币,都只有一种组合方法–用0个硬币组成总金额为0(注:这里虽然不用任何硬币去组合总金额为0,但不能认为组合方式为0,我一开始就跪在这里,坑啊,因为是用0个硬币去组合,也就是属于存在一种组合方式)。所以对所有dp[i][0]=1;
  • i=0,就是用面值为coins[0]的硬币能组成任意总金额(小于amount11美元)的硬币组合方法,就是 i 最基本的问题了。很明显,cosin[0]=1, 1美元需要最少使用1张1美元的硬币,即只有一种组合方法;2美元需要最少使用2张1美元的硬币,即只有一种组合方法;3美元需要最少使用3张1美元的硬币,即只有一种组合方法;,,11美元需要最少使用11张1美元的硬币,即只有一种组合方法;所以对所有dp[0][j]=1。
    2)找到子问题与大问题的联系,即状态转移方程。
    对于第 i 枚硬币有两种选择:用它来找零 和 不用它找零。
  • 用它来找零:
    dp[i][j] =dp[i - 1][j] + dp[i][j - coins[i]]; 表示 使用 第 i 枚硬币找零时,对金额为 j 进行找钱的所有硬币组合种数。
  • 不用它来找零:
    dp[i][j] = dp[i - 1][j];表示 不使用第 i 枚硬币找零时(即使用硬币种类0 ~ i-1),对金额为 j 进行找钱的所有硬币组合种数;

总共就这两种可能,我要找总金额的所有硬币组合种数,则这一步的状态转移方程(dp[i][j]的最优解):
dp[i][j] =
dp[i - 1][j] + dp[i][j - coins[i]] ( j >= coins[i] );
dp[i - 1][j] ( j < coins[i] );

4. 代码实现(java)

public static int countWaysWithNoLimit(int[] coins, int amount) {
        if (coins.length == 0){
            if (amount == 0){
                return 1;
            }else {
                return 0;
            }
        }
        int[][] dp = new int[coins.length][amount + 1];
        //初始化,,注意这里是从0开始的,因为当amount为0时,任何硬币都能找开,即存在一种换钱方法
        for (int j = 0; j <= amount; j++) {
            if (j % coins[0] == 0) {
                dp[0][j] = 1;
            }
        }
        for (int i = 1; i < coins.length; i++) {
            dp[i][0] = 1;
            for (int j = 1; j <= amount; j++) {
                if (j >= coins[i]) {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]];
                } else {
                    dp[i][j] = dp[i - 1][j];//如果j小于当前面值,那明显不能用当前面值,只能继承前面的
                }
            }
        }
        return dp[coins.length - 1][amount];
    }

三、每种面值的硬币有且只有一张,求最小货币数

1. 题目

给定数组coins,coins中所有的值都是正数且不重复。每个值仅代表一张钱的面值,每种面值的货币有且仅有一张,再给定一个整数amount代表要找的钱数,求组成amount的最少货币数。

2. 题目分析
给定数组coins,coins中所有的值都是正数且不重复。每个值仅代表一张钱的面值,每种面值的货币有且仅有一张,再给定一个整数amount代表要找的钱数,求组成amount的最少货币数。

3. 解题思路
与第一题比较,这道题中每种面值的硬币只有一个,而不是任意多张。
我们用dp[m][n]来表示所有可能的状态,m表示硬币的种类数,n表示需要凑齐的总额;
则,dp[i][j]表示,用前i(0 <= i <= m)种面值的硬币,去凑齐总额为j(0 <= j <= n),所需要硬币的最少数目。

1)首先进行初始化:

  • j=0,就是要凑齐的总额为0,不管使用的硬币种类时,所需要的最小货币数,就是 j 最基本的问题了。很明显,可以是0张2,0张5,0张1,也就是说dp[i][0] = 0 (0 <= i <= n),因为创建数组时,在java中会自动给数组中的每个元素赋初值0,所以一般这个步骤不需要程序猿显示用代码实现;
  • i=0,就是用一个面值为coins[0]的硬币能凑齐任意总额(小于amount11美元),所需要的最小货币数,就是 i 最基本的问题了。很明显,cosin[0]=1, 1美元需要最少使用1张1美元的硬币,大于1美元的总金额都无法使用面值为1的硬币去凑齐。所以,dp[0][1]=1,dp[0][j]=Integer.MAX_VALUE。

2)找到子问题与大问题的联系,即状态转移方程。
对于第 i 枚硬币有两种选择:用它来找零 和 不用它找零。

  • 用它来找零:
    dp[i][j] =dp[i - 1][j - coins[i]] + 1; 因为每个面值的硬币只有一个,所以只能用一次,所以我们考虑dp[i-1][j-arr[i]]的值,这个值代表在可以任意使用coins[0…i-1]货币的情况下,组成 j-coins[i] 所需的最小张数。从钱数为 j-coins[i] 到钱数 j ,只用在加上这张coins[i]即可。所以dp[i][j]的值可能等于 dp[i-1][j-arr[i]]+1 。
  • 不用它来找零:
    dp[i][j] = dp[i - 1][j];表示 不使用第 i 枚硬币找零时(即使用硬币种类0 ~ i-1),对金额为 j 进行找钱的所需要的最少硬币数;

4. 代码实现(java)

public static int leastCoinsWithOne(int[] coins, int amount) {
        if (coins == null || coins.length == 0 || amount < 0) {
            return -1;
        }
        int[][] dp = new int[coins.length][amount + 1];
        for (int j = 1; j <= amount; j++) {
            dp[0][j] = Integer.MAX_VALUE;
        }
        if (coins[0] <= amount) {
            dp[0][coins[0]] = 1;
        }
        for (int i = 1; i < coins.length; i++) {
            for (int j = 1; j <= amount; j++) {
                if (j >= coins[i] && dp[i - 1][j - coins[i]] != Integer.MAX_VALUE) {
                    dp[i][j] = Math.min(dp[i - 1][j], dp[i - 1][j - coins[i]] + 1);
                } else {
                    dp[i][j] = dp[i - 1][j];//如果j小于当前面值,那明显不能用当前面值,只能继承前面的
                }
            }
        }
        return dp[coins.length - 1][amount] == Integer.MAX_VALUE ? -1 : dp[coins.length - 1][amount];
    }

四、每种面值的硬币有且只有一张,求凑齐总额为amount的硬币组合数

1. 题目

给定数组coins,coins中所有的值都为正数且重复。每个值代表一张钱的面值, 再给定一个整数amount代表要找的钱数,求换钱有多少种方法。

2. 题目分析
给定数组coins,coins中所有的值都为正数且重复。每个值代表一张钱的面值, 再给定一个整数amount代表要找的钱数,求换钱有多少种方法。

3. 解题思路
类比题型2,每个面值的硬币只有一个,而不是任意多张。
我们可以构造一个dp数组,如dp数组的行数为m(m=coins.length),列数为amount+1(总金额为0-amount)。
dp[i][j] 的含义是:在可以任意使用coins[0…i]货币的情况下,组成总金额 j 有多少种组合方法。

1)首先进行初始化:

  • j=0,就是要组成总金额为0的硬币组合方法,就是 j 最基本的问题了。很明显,对每种面额的硬币,都只有一种组合方法–用0个硬币组成总金额为0(注:这里虽然不用任何硬币去组合总金额为0,但不能认为组合方式为0,我一开始就跪在这里,坑啊,因为是用0个硬币去组合,也就是属于存在一种组合方式)。所以对所有dp[i][0]=1;
  • i=0,就是用面值为coins[0]的硬币能组成任意总金额(小于amount11美元)的硬币组合方法,就是 i 最基本的问题了。很明显,cosin[0]=1, 1美元需要最少使用1张1美元的硬币,即只有一种组合方法;大于1美元的总金额都无法使用面值为1的硬币去凑齐。所以,dp[0][1]=1,dp[0][j]=0。

2)找到子问题与大问题的联系,即状态转移方程。
对于第 i 枚硬币有两种选择:用它来找零 和 不用它找零。

  • 用它来找零:
    dp[i][j] =dp[i - 1][j] + dp[i-1][j - coins[i]]; 因为每个面值的硬币只有一个,所以只能用一次,所以我们考虑dp[i-1][j-arr[i]]的值,这个值代表在可以任意使用coins[0…i-1]货币的情况下,组成 j-coins[i] 的硬币组合数。从钱数为 j-coins[i] 到钱数 j ,只用在加上这张coins[i]即可。所以dp[i][j]的值可能等于 dp[i - 1][j] + dp[i-1][j - coins[i]] 。
  • 不用它来找零:
    dp[i][j] = dp[i - 1][j];表示 不使用第 i 枚硬币找零时(即使用硬币种类0 ~ i-1),对金额为 j 进行找钱的所有硬币组合种数;

总共就这两种可能,我要找总金额的所有硬币组合种数,则这一步的状态转移方程(dp[i][j]的最优解):
dp[i][j] =
dp[i - 1][j] + dp[i-1][j - coins[i]] ( j >= coins[i] );
dp[i - 1][j] ( j < coins[i] );

4. 代码实现(java)

public static int countWaysWithOne(int[] coins, int amount) {
        if (coins == null || coins.length == 0 || amount < 0) {
            return -1;
        }
        int[][] dp = new int[coins.length][amount + 1];
        //初始化
        dp[0][coins[0]] = 1;
        for (int i = 1; i < coins.length; i++) {
            dp[i][0] = 1;
            for (int j = 1; j <= amount; j++) {
                if (j >= coins[i]) {
                    dp[i][j] = dp[i - 1][j] + dp[i-1][j - coins[i]];
                } else {
                    dp[i][j] = dp[i - 1][j];//如果j小于当前面值,那明显不能用当前面值,只能继承前面的
                }
            }
        }
        return dp[coins.length - 1][amount];
    }

五、每种面值的货币可以使用任意张,求最大硬币数

1. 题目

给定数组coins,coins中所有的值都是正数且不重复。每个值代表一种面值的硬币,每种面值的货币可以使用任意张,再给定一个整数aim代表要找的钱数,求组成amount的最大硬币数。

2. 题目分析
给定数组coins,coins中所有的值都是正数且不重复。每个值代表一种面值的硬币,每种面值的货币可以使用任意张,再给定一个整数aim代表要找的钱数,求组成amount的最大硬币数。

3. 解题思路
这题与第一题的区别就是,一个是求最小硬币数,一个是求最大硬币数。思路是一样的,这里就不是多费口舌了,需要改动的就是找最优解时,是把筛选条件,找更大代替找更小的。

4. 代码实现(java)

public static int moreCoins(int[] coins, int amount) {
        if (coins == null || coins.length == 0 || amount < 0) {
            return -1;
        }
        //必须注意amount == 0这种特殊情况,因为最后dp[amount] == 0 是直接返回-1,所以需要提前特殊处理
        if (amount == 0) {
            return 0;
        }
        //dp[i]=j表示组成i元,最少需要j个硬币
        int[] dp = new int[amount + 1];
        for (int i = 1; i <= amount; i++) {
            for (int j = 0; j < coins.length; ++j) {
                if (coins[j] <= i) {
                    dp[i] = Math.max(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        return dp[amount] == 0 ? -1 : dp[amount];
    }

猜你喜欢

转载自blog.csdn.net/qq_35923749/article/details/86619525