LeetCode 零钱兑换(322题)四种方法求解

LeetCode 零钱兑换

@author:Jingdai
@date:2021.01.10

题目描述(322题)

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

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

示例 :

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

思路及代码

看到这个题目时,我第一想到的就是用 dfs 或者 bfs 去解这个题目,但是会发现如果不优化的话会超时,不能通过,但我还是认为这个题目是一个非常好的练习 dfs 和 bfs 的题目。下面我会先写 dfs 和 bfs 的思路,再写 dp 的思路,虽然遍历回溯过不了,但相信还是可以在写这个题目的过程中学习到很多东西。

dfs方法

在这里插入图片描述

一图胜千言,看上面这个图。对于示例 amount 等于 11,硬币面额有 1、2、5三种,我们每次对 amount 减去对应的硬币面额,当找到使 amount 等于 0 时,就找到了一个解,解就是当前节点的深度。我们需要遍历整个搜索树,然后找到最小的解,不要误认为从最大的开始找,找到的第一个解就是最小的解,比如求 amount 等于 10, 硬币面额有1、5、8三种,找到的第一个解是 [8, 1, 1],结果是 3,但是最小的解却是 [5, 5],结果是 2,所以我们必须遍历整个树找到最小的解。

同时,还可以对这个树进行剪枝,我们是用一个全局变量记录已经找到的最小解,如果遍历到某处时树的深度已经大于之前的最小解了,说明这个分支肯定不会有最小的解,我们可以对该分支进行剪枝。

参考代码如下:

private int coinNumber = 0;
private int minCoinNumber = Integer.MAX_VALUE;

public int coinChange(int[] coins, int amount) {
    
    

    // precheck
    if (amount == 0) {
    
    
        return 0;
    }

    if (amount < 0) {
    
    
        return -1;
    }

    // reverse sort
    Arrays.sort(coins);
    int left = 0;
    int right = coins.length - 1;
    while (left < right) {
    
    
        int temp = coins[left];
        coins[left] = coins[right];
        coins[right] = temp;
        left++;
        right--;
    }

    // dfs
    dfs(coins, amount, 0);
    if (minCoinNumber != Integer.MAX_VALUE) {
    
    
        return minCoinNumber;
    }
    return -1;
}

private void dfs(int[] coins, int amount, int index) {
    
    

    /*
        cant reach here
        if (index == coins.length) { 
            return;
        }
        */

    if (coinNumber >= minCoinNumber) {
    
    
        return;
    }

    for (int i = index; i < coins.length; i++) {
    
    

        if (amount == coins[i]) {
    
    
            // same as allocation
            if (coinNumber + 1 < minCoinNumber) {
    
    
                minCoinNumber = coinNumber + 1;
            }
            break;
        } else if (amount > coins[i]) {
    
    
            // try to allocate
            coinNumber ++;
            amount -= coins[i];
            dfs(coins, amount, i);
            // bcaktarak
            coinNumber --;
            amount += coins[i];
        } 
    }
}

bfs方法

前面的 dfs 虽然用了剪枝来进行优化,但最后还是超时了,bfs 也是对整个搜索树进行遍历,但是由于题目相当于是求解的深度,而 bfs 是一层一层遍历的,所以可以在找到第一个解时就直接返回。同时,由于是一层一层遍历,如果后面遍历到的 amount 值前面已经遍历过了,那么就可以从这里剪枝,因为以该节点为分支的解不会比之前那个小,根据这点进行优化,就可以使 bfs 不超时了,如果不优化 bfs 也是会超时的。具体优化做法也很简单,就是用一个 visited 数组记录是否访问过该 amount,如果访问过就不让该节点入队,进行剪枝。

参考代码如下:

private class State {
    
    
    int amount;
    int nextStartIndex;

    State (int amount, int nextStartIndex) {
    
    
        this.amount = amount;
        this.nextStartIndex = nextStartIndex;
    }
}

public int coinChange(int[] coins, int amount) {
    
    

    // precheck
    if (amount == 0) {
    
    
        return 0;
    }
    if (amount < 0) {
    
    
        return -1;
    } 

    // sort and reverse
    Arrays.sort(coins);
    int left = 0;
    int right = coins.length - 1;
    while (left < right) {
    
    
        int temp = coins[left];
        coins[left] = coins[right];
        coins[right] = temp;
        left ++;
        right --;
    }

    return bfs(coins, amount);
}

private int bfs(int[] coins, int amount) {
    
    
    // assert amount > 0

    LinkedList<State> queue = new LinkedList<>();
    queue.offer(new State(amount, 0));

    boolean[] visited = new boolean[amount+1];

    int layer = 1;
    while (queue.size() != 0) {
    
    
        int size = queue.size();
        for (int i = 0; i < size; i++) {
    
    
            State curState = queue.poll();
            for (int j = curState.nextStartIndex; j < coins.length; j++) {
    
    
                if (curState.amount == coins[j]) {
    
    
                    return layer;
                } else if (curState.amount > coins[j]) {
    
        
                    if (!visited[curState.amount - coins[j]]) {
    
    
                        queue.offer(new State(curState.amount - coins[j], j));
                        visited[curState.amount - coins[j]] = true;
                    }
                }
            }
        }
        layer++;
    }
    return -1;
}

dp方法

这个题目同时可以用 dp 方法来进行求解,先定义 dp[i] 表示凑成金额为 i 的最少硬币数。假设我们知道凑成amount 的最后一个硬币的面值是 c,那么最后的结果 dp[amount] = dp[amount - c] + 1,但是我们并不知道最后一个硬币的面值是多少,所以我们把每一个可能都尝试一下,求得最小值就可以了,即 dp[amount] = min{dp[amount - ci]} + 1 , ci 为所有可能的硬币面值。

根据以上的定义,我们就可以写出 dp 的代码了,首先初始化 dp 数组为一个不可能的值 amount + 1,然后初始化 dp[0] = 0,表示凑成金额为0的最少硬币是0个。从1 - amount 依次求解,后面的结果依赖前面的结果,最后的结果就是 dp[amount]

参考代码如下:

public int coinChange(int[] coins, int amount) {
    
    

    int[] dp = new int[amount+1];
    Arrays.fill(dp, amount+1);

    Arrays.sort(coins);

    dp[0] = 0;

    for (int i = 1; i <= amount; i++) {
    
    
        for (int coin : coins) {
    
    
            if (i >= coin) {
    
    
                if (dp[i-coin] != amount+1) {
    
    
                    dp[i] = Math.min(dp[i], dp[i-coin] + 1);
                }
            } else {
    
    
                break;
            }
        }
    }

    if (dp[amount] == amount+1) {
    
    
        return -1;
    }
    return dp[amount];
}

同时,我们也可以直接从顶向下进行求解,思路基本一样,这里我们用 -2 代表还没有遍历过该值,用 -1 表示遍历过但是现有的硬币面值不能凑成给定的值。

参考代码如下:

public int coinChange(int[] coins, int amount) {
    
    
        
    if (amount == 0) {
    
    
        return 0;
    }

    if (amount < 0) {
    
    
        return -1;
    }

    Arrays.sort(coins);
    // -2 : dont reach
    // -1 : cant reach (have tried)
    int[] memo = new int[amount+1];
    Arrays.fill(memo, -2);
    memo[0] = 0;

    dfs(coins, memo, amount);
    return memo[amount];
}

public void dfs(int[] coins, int[] memo, int amount) {
    
    
    if (memo[amount] != -2) {
    
    
        return;
    }

    int res = Integer.MAX_VALUE;
    for (int coin : coins) {
    
    
        if (amount >= coin) {
    
    
            dfs(coins, memo, amount - coin);
            if (memo[amount - coin] != -1) {
    
    
                res = Math.min(res, memo[amount - coin] + 1);
            }
        } else {
    
    
            break;
        }
    }

    if (res == Integer.MAX_VALUE) {
    
    
        memo[amount] = -1;
    } else {
    
    
        memo[amount] = res;
    }
}

猜你喜欢

转载自blog.csdn.net/qq_41512783/article/details/112450835