LeetCode第 322 题:零钱兑换(C++)

322. 零钱兑换 - 力扣(LeetCode)
在这里插入图片描述

典型的动态规划问题,需要求解的是最少的硬币个数,不能使用贪心算法。

这题的测试样例以及设置非常坑,给的硬币是未排序的,amount的值可能是0,也可能比最小的硬币还要小。。。

状态转移表/BFS(很暴力,不推荐使用)

先从基本的开始分析,可以一次遍历coins获取硬币最小值,那么动态规划的阶段数量最多就是amount/硬币最小值。

每个阶段我们决策不同的硬币是否选择,会达到不同的状态(状态用下标表示),以下为例子:

阶段1可达状态为3种,即为各个硬币值(状态计算过程就像BFS):广度遍历,就是每一次累加上所有硬币,因为是广度遍历,所得到一定是可以用最少硬币达到的

阶段2在阶段1的基础上进行选择,可达状态为:2(1+1), 3(1+2),4(2+2),6(1+5),7(2+5),10(5+5)

输入: coins = [1, 2, 5], amount = 11
下标 i: 	0		1		2		3		4		5		6		7		8		9    10	   11
阶段1:	0		1		2		0		0		5		0		0		0		0		0		0	
阶段2:	0 		0		1 		1 		2 		0 		1 		2 		0 		0 		5 		0 
阶段3:	0 		0		0 		1 		1 		1 		2 		1 		2 		5 		0 		5 

虽然阶段数最多为11,但是阶段3结束后,f[3][11]已经可达了,就可以break出去了。

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        if(amount == 0) return 0;
        int min_idx = 0;
        for(int i = 0; i < coins.size(); ++i){//先找到最小的硬币
            if(coins[i] < coins[min_idx])   min_idx = i;
        }
        int level = amount/coins[min_idx]; //最大遍历的循环次数(动态规划的阶段个数)
        if(level < 1)   return -1;//amount小于最小的硬币值

        vector<vector<int>> f(level, vector<int>(amount+1, 0));
        for(int i = 0; i < coins.size(); ++i){//第一阶段放在外层特殊处理
            if(coins[i] <= amount)
                f[0][coins[i]] = coins[i];
        }
        if(f[0][amount] != 0) return 1;//硬币刚好有的金额,一个硬币就可以

        int res = -1;
        for(int i = 1; i < level; ++i){//每个阶段
            for(int j = 0; j < amount; ++j){
                if(f[i-1][j] != 0){
                    for(int k = 0; k < coins.size(); ++k){//每个coin
                        //if里面的写法是为了防止j + coins[k]溢出
                        if(coins[k] <= amount-j)  f[i][j + coins[k]] = coins[k];
                    }
                }
                if(f[i][amount] != 0) return i + 1; //amount的状态已达
            }
        }
        return res;
    }
};

上面是初始代码,效率并不高。
虽然这题我们只需要计算最少的数量,但是这样写的好处是如果有需要我们可以输出选择的序列,比如某个硬币选了多少个,只需要加以下代码就可以:

int tmp = amount;
for(int i = res - 1; i >= 0; --i){//状态(i,j)从状态(i-1, j - f[i][j])而来
		cout << f[i][tmp] << " ";
		tmp -= f[i][tmp];
}

对于上面的例子,输出就是: 5,1,5。

如果要进行优化的话,我们知道二维数组一般是可以用一维数组代替的,只是有些时候需要逆序计算:

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        if(amount == 0) return 0;
        int min_idx = 0;
        for(int i = 0; i < coins.size(); ++i){//先找到最小的硬币
            if(coins[i] < coins[min_idx])   min_idx = i;
        }
        int level = amount/coins[min_idx]; //最大遍历的循环次数(动态规划的最大阶段个数)
        if(level < 1)   return -1;//amount小于最小的硬币值

        vector<int> f(amount+1, 0);//一维数组
        for(int i = 0; i < coins.size(); ++i){//第一阶段放在外层特殊处理
            if(coins[i] <= amount)  f[coins[i]] = coins[i];
        }
        if(f[amount] != 0) return 1;//硬币刚好有的金额,一个硬币就可以

        int res = -1;
        for(int i = 1; i < level; ++i){//每个阶段
            for(int j = amount - 1; j >= 0; --j){//经典逆序处理,避免值覆盖问题
                if(f[j] != 0){
                    for(int k = 0; k < coins.size(); ++k){//每个coin
                        //if里面的写法是为了防止j + coins[k]溢出
                        if(coins[k] <= amount-j)  f[j + coins[k]] = coins[k];
                    }
                }
                if(f[amount] != 0)  return i + 1;//amount的状态已达
            }
        }
        return res;
    }
};

一维数组优化之后内存消耗直接减少了几十倍,不过执行效率还是一般。下面考虑状态转移方程。

状态转移方程dp

其实这就是一个完全背包问题(同种硬币不限选),只不过我们在求的是能够刚好装满容量amount的背包的硬币数量的最小值,硬币数量就相当于传统背包问题里面的价值,要求的是最小值而不是最大值,这儿只不过稍作修改。

所以我们只需要在初始化的时候也做一下修改就可以了。

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount+1, amount+1);//因为求得是最小值,所以初始化的很大
        dp[0] = 0;
        for(const auto &coin : coins){
            for(int j = coin; j <= amount; ++j){//完全背包正序遍历
                dp[j] = min(dp[j], dp[j-coin]+1);
            }
        }
        return dp[amount] == amount+1 ? -1 : dp[amount];
    }
};

到了这儿再看之前bfs打dp表的方法,真的笨死了。。。
完全背包问题两层for循环是可以颠倒的:

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount+1, amount+1);//因为求得是最小值,所以初始化的很大
        dp[0] = 0;
        for(int i = 1; i <= amount; ++i){//完全背包正序遍历
            for(const auto &coin : coins){
                if(i >= coin)    dp[i] = min(dp[i], dp[i-coin]+1);
            }
        }
        return dp[amount] == amount+1 ? -1 : dp[amount];
    }
};

不过不颠倒的效率会好很多。

DFS回溯加剪枝(贪心)

上面的dp还是不够快,虽然不能直接使用贪心算法,但直觉上应该是大面值的越多越好,但是怎么把这个思路加入回溯并且保证是最优解呢?

可以看这位同学的解释:

【零钱兑换】贪心 + dfs = 8ms - 零钱兑换 - 力扣(LeetCode)

DFS + 剪枝 2ms 击败100%,比 DP 还快? - 零钱兑换 - 力扣(LeetCode)

两个剪枝策略:

  • 1、当剩余份额 rem_amount==0 时剪枝,因为大面值硬币需要更多小面值硬币替换,继续减少一枚或几枚大硬币搜索出来的答案(即使有),也肯定不会比现在的更好。
  • 2、当 rem_amount!=0,但当前已使用的硬币数 cur_cnt 满足: cur_cnt+1 >= ans 时剪枝,因为 rem_amount 不为 0,至少还要 1 枚硬币,则继续搜索得到的答案不会更优,就应该break出该层循环。因为少用几枚大(面值)硬币搜索出的方案(即使有)肯定需要更多枚小硬币来弥补少掉的大硬币(策略1),而使用这几枚大硬币都搜索不到更好的解,用小硬币弥补即使有解,硬币个数也只会比现在的更多,故应该 break。
class Solution {
public:
    int res = INT_MAX;
    //idx:此时选择到coins中的下标为idx的硬币
    void backtrack(vector<int> &coins, int idx, int amount, int cnt){
        if(idx < 0) return;
        //amount/coins[idx]为能够选择的最多的coins[idx]个数
        for(int i = amount/coins[idx]; i >= 0; --i){//i即表示选了多少个该硬币
            int rem_amount = amount - i * coins[idx];//剩余amount
            int cur_cnt = cnt + i;//当前硬币个数
            if(rem_amount == 0){
                res = min(res, cur_cnt);
                break;
            }
            if(cur_cnt + 1 > res)   break;
            backtrack(coins, idx - 1, rem_amount, cur_cnt);
        }

    }
    int coinChange(vector<int>& coins, int amount) {
        sort(coins.begin(), coins.end());
        //一定要从后往前
        backtrack(coins, coins.size()-1, amount, 0);
        return res == INT_MAX ? -1 : res;
    }
};

不过标准解法还是dp,转化为背包问题之后很简单。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值