动态规划+回溯 高效解决凑硬币(零钱兑换)问题

动态规划+回溯 高效解决凑硬币问题

这是我在刷leetcode遇到的一道典型动态规划题,我们先看下问题:

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

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

示例 2:

输入: coins = [2], amount = 3
输出: -1

一般来说,只要看到求最值,一般都是动态规划的问题(不过 动态规划 != 求最值问题 )
具体是不是,这个做多了就知道了

我们设 i 为 需要凑的金额
dp[i] 为 凑成 i 金额所需的最少的硬币个数
设c 为 某个面额的硬币
那么dp[i] = min(dp[i - c] + 1, dp[i])

下面是动态规划实现的c++代码:

int coinChange(vector<int>& coins, int amount) {
    
    
	 if (amount == 0) {
    
    
        return 0;
    }
    vector<int> dp(amount + 1, amount + 1);
    for (auto c : coins) {
    
    
        if(amount >= c)
            dp[c] = 1;
    }
    for (int i = 1; i <= amount; i++) {
    
    
        for (int j = 0; j < coins.size(); j++) {
    
    
            if (i > coins[j] && dp[i] > dp[i - coins[j]] + 1) {
    
      //min(dp[i - c] + 1, dp[i])
                dp[i] = dp[i - coins[j]] + 1;
            }
        }
    }
    return dp[amount] == amount + 1 ? -1 : dp[amount];
}

不难看出上面的动态规划算法,其实是一种暴力破解法,时间复杂度是O( amount*coin.size()),
如果 amount 达到上万,那耗时就特别大了,很不划算,这就需要想下有没有可以优化的地方。

其实这个零钱兑换,肯定只是在 coins里面的硬币中,抽取 0 至多个 同种硬币,抽多种。我们
困惑的地方在于,到底抽多少个,这个是不确定。想到这里,可以用回溯来解决的这个问题。

首先,了解一下回溯算法的基本结构:

void backtrace(int t)
{
    
    	
	if(t > N){
    
    	//大于边界值,不在递归
		...
	}else{
    
    
		for(auto l : list){
    
    
			... //做出选择,或操作
			backtrace(t + 1);	//做出选择后,递归
			... // 撤销选择,或操作
		}
	}
}

回溯算法,其实就是 深度优先遍历算法 加 剪枝操作,该算法可以遍历问题的所有选项。

凑硬币的动态规划转移方程是:dp[i] = min(dp[i - c] + 1, dp[i]),这里可以用回溯遍历,每个硬币金额在dp[amount]中所需要的数量,回溯会存在多个dp[amount], 取最小的那个即可。即:
dp[i] = min(dp[i - c*j] + j, dp[i]), 其中 j的范围是(0 ~ i / c)

下面是动态规划 + 回溯 实现的c++代码:

void helper(vector<int>& coins, int start, int target, int cur, int &res)
{
    
    
	if (start < 0) return;
	if (target % coins[start] == 0) {
    
    
		res = min(res, cur + target / coins[start]);
		return;
	}
	for (int i = target / coins[start]; i >= 0; i--) {
    
    
	//这个判断很精髓,特别的细,首先 cur + i >= res 肯定是成立的,其次 target % coins[start] != 0,所以后面至少还会存在一次,所以用cur + i >= res - 1
		if (cur + i >= res - 1) break;
		//金额减少i * coins[start], 硬币数增加 i, i 可等于0(=0就是该硬币金额不算入其中)
		helper(coins, start - 1, target - i * coins[start], cur + i, res);	
	}
}

int coinChange(vector<int>& coins, int amount)
{
    
    
	int res = INT_MAX;
	sort(coins.begin(), coins.end());
	helper(coins, coins.size() - 1, amount, 0, res);
	return res == INT_MAX ? -1 : res;
}

第一次在csdn写算法题的题解,可能讲的不太好,如果大家感兴趣,但又不太明白的,可以留言给我。

猜你喜欢

转载自blog.csdn.net/h799710/article/details/106003687