leetcode:322. 零钱兑换

题目来源

题目描述

在这里插入图片描述

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {

    }
};

题目解析

思路一

暴力递归

定义一个尝试函数: 考虑coins[idx…],组成rest的最少张数

   int process(vector<int>& coins, int idx, int rest)

令N = coins.length(),那么,有如下可能:
(1)如果 idx == N,也就是没有硬币可以选择了,因此判断rest是否为0,如果为0,返回0张(因为0张硬币可以组成0元钱),否则返回无效

(2)否则,有硬币可以选择。因此尝试使用0张、1张、…张coins[idx]去组成rest

下面,我们用INT32_MAX标记无效

class Solution {
    // coins[index...]面值,每种面值张数自由选择,组成idx需要多少张
    int process(vector<int>& coins, int idx, int rest){
        if(idx == coins.size()){         //已经没有面值能够考虑了
            return rest == 0 ? 0 : INT32_MAX;              //如果此时剩余的钱为0,返回0张
        }


        int ans = INT32_MAX;          //最少张数,初始时为INT32_MAX,因为还没找到有效解
        //依次尝试使用当前面值(arr[i])0张、1张、zhang张,但不能超过rest
        for (int zhang = 0; zhang * coins[idx] <= rest; ++zhang) {
            //使用了zhang张aim[i],剩下的钱为 rest - zhang*arr[i], 交给剩下的面值去搞定 (arr[i+1...N-1])
            int next = process(coins, idx + 1, rest -  zhang *coins[idx]);
            if(next != INT32_MAX){   // 注意,**必须**next有效时,才去比较
                ans =  std::min(zhang + next, ans);
            }
        }

        return ans;
    }
public:
    int coinChange(vector<int>& coins, int amount) {
    	int k = process(coins, 0, amount) ;
        return k  == INT32_MAX ? -1 : k  ;
    }
};

下面我们用-1标记无效:

 int process(vector<int>& coins, int idx, int rest){
        if(idx == coins.size()){
            return rest == 0 ? 0 : -1;
        }


        int ans = -1;
        for (int zhang = 0; zhang * coins[idx] <= rest; ++zhang) {
            int next = process(coins, idx + 1, rest -  zhang *coins[idx]);
            if(next != -1){
                ans = ans == -1 ? next + zhang :  std::min(zhang + next, ans);
            }
        }

        return ans;
    }

注意两点:

  • ans也必须用-1标记;
  • ans必须先变成有效一次,然后才去和next + zhang比较

暴力递归改动态规划

用-1标记时:

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int N = coins.size();
        std::vector<std::vector<int>> dp(N + 1, std::vector<int>(amount + 1));
        dp[N][0] = 0;
        for (int rest = 1; rest <= amount; ++rest) {
            dp[N][rest] = -1;
        }

        for (int idx = N - 1; idx >= 0; --idx) {
            for (int rest = 0; rest <=  amount; ++rest) {
                int ans = -1;
                for (int zhang = 0; zhang * coins[idx] <= rest; ++zhang) {
                    int next = dp[idx + 1 ][rest -  zhang *coins[idx]];
                    if(next != -1){
                        ans = ans == -1 ? next + zhang :  std::min(zhang + next, ans);
                    }
                }
                dp[idx][rest] = ans;
            }
        }

        return dp[0][amount];
    }
};

用INT_MAX标记时:

int coinChange(vector<int>& coins, int amount) {
    int N = coins.size();
    std::vector<std::vector<int>> dp(N + 1, std::vector<int>(amount + 1, INT32_MAX));
    dp[N][0] = 0;
    for (int idx = N - 1; idx >= 0; --idx) {
        for (int rest = 0; rest <=  amount; ++rest) {
            int ans = INT32_MAX;
            for (int zhang = 0; zhang * coins[idx] <= rest; ++zhang) {
                int next = dp[idx + 1 ][rest -  zhang *coins[idx]];
                if(next != INT32_MAX){
                    ans = std::min(zhang + next, dp[idx][rest]);
                }
            }
            dp[idx][rest] = ans;
        }
    }

    return dp[0][amount];
}

斜率优化

class Solution{
public:
    int coinChange(vector<int>& coins, int amount) {
        int N = coins.size();
        std::vector<std::vector<int>> dp(N + 1, std::vector<int>(amount + 1, INT32_MAX));
        dp[N][0] = 0;
        for (int idx = N - 1; idx >= 0; --idx) {
            for (int rest = 0; rest <=  amount; ++rest) {
                dp[idx][rest] = dp[idx + 1][rest];
                if(rest - coins[idx] >= 0 && dp[idx][rest - coins[idx]] != INT32_MAX){
                    dp[idx][rest] = std::min(dp[idx][rest], dp[idx][rest - coins[idx]] + 1);
                }
            }
        }

        return dp[0][amount];
    }
};

思路二:暴力尝试

class Solution {
    int process(vector<int>& coins, int idx, int rest){
        if(idx == coins.size()){
            return rest == 0 ? 0 : INT32_MAX;
        }

        // 对于当前idx,有两种选择
        int p1 = process(coins, idx + 1, rest);  //不要
        int p2 = INT32_MAX;  
        if(rest >= coins[idx]){ // 要:(必须满足条件才能要)
            p2 = process(coins, idx, rest - coins[idx]);
            if(p2 != INT32_MAX){
                p2 = p2 + 1;
            }
        }
        return std::min(p1, p2);
    }
public:
    int coinChange(vector<int>& coins, int amount) {
        return process(coins, 0, amount);
    }
};

动态规划:状态转移方程推导

为什么这道题不能用贪心解决?

  • 为什么不能使用贪心算法思路来解决,因为使用贪心算法的前提是无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关无后滞性,即去掉过去,不影响将来
  • 比如对于硬币[1, 2, 5],和target,贪心指的是尽量用最大的硬币,假设现在我们已经选取了{5, 5},此时剩下的target为1。如果我们拿掉5,那么剩下的target就变成了6,有后滞性

所以我们只能穷举所有的可能性。当然我们应该聪明的选择,也就是使用动态规划。

这道题类似于完全背包问题,

  • 每个物品都可以无限使用,但是要求背包必须装满,而且要求背包中的物品数目最少
  • (最值型的动态规划)

动态规划思路分析。

(1)第一步:也是最重要的一步。确定状态

  • 什么叫做确定状态呢?
    • 简单来说,解动态规划的时候需要开辟一个数组,数组dp[i]或者dp[i][j]代表什么,i和j有分别代表什么
  • 怎么确定状态呢?需要两个东西
    • 最后一步
    • 子问题
  • 关于最后一步:
    • 可以这样想:虽然我们不知道最优策略是什么,但是最终的方案肯定是K枚硬币 a 1 、 a 2 、 . . . . . . a k a_1、a_2、......a_k a1a2......ak面值加起来是 t a r g e t target target
    • 不管方案是什么,一定有一枚最后的硬币 a k a_k ak
    • 除掉这枚硬币,前面的硬币的面值加起来是 t a r g e t − a k target- a_k targetak
    • 也就是说:
      • 我们不关心前面的k-1枚硬币是怎么拼出 t a r g e t − a k target - a_k targetak的,我们也不知道 a k a_k ak K K K,我们唯一可以确定的是前面的硬币拼出了 t a r g e t − a k target - a_k targetak
      • 因为是最优策略,所以拼出 t a r g e t − a k target - a_k targetak的硬币数一定是最少的
  • 关于子问题
    • 从上面的问题,我们要求:最少用多少枚硬币可以拼出 t a r g e t − a k target - a_k targetak
    • 即:
      • 子问题:最少用多少枚硬币可以拼出 t a r g e t − a k target - a_k targetak
      • 原问题:最少用多少枚硬币可以拼出target
    • 可以看出,我们已经把问题规模缩小了。
    • 为了简化定义:f(k)表示最少用多少枚硬币拼出了k,k表示目标,f(k)返回多少枚硬币。可以看出,只有入参是不同的
  • 还剩下一个问题:最后那枚硬币 a k a_k ak是多少?(最优子结构)
    • 最后的那枚硬币 a k a_k ak只可能是 c o i n s [ 0 ] 、 c o i n s [ 1 ] 、 c o i n s [ 2 ] 、 c o i n s [ 3 ] . . . . . . coins[0]、coins[1]、coins[2]、coins[3]...... coins[0]coins[1]coins[2]coins[3]......c当中的某一个。
      • 如果 a k a_k ak是coins[0], f ( t a r g e t ) f(target ) f(target)应该是 f ( t a r g e t − c o i n s [ 0 ] ) + 1 f(target -coins[0] ) + 1 f(targetcoins[0])+1(假设最后这一枚硬币coins[0])
      • 如果 a k a_k ak是coins[1], f ( t a r g e t ) f(target ) f(target)应该是 f ( t a r g e t − c o i n s [ 1 ] ) + 1 f(target -coins[1]) + 1 f(targetcoins[1])+1(假设最后这一枚硬币coins[1])
      • 如果 a k a_k ak是coins[3], f ( t a r g e t ) f(target ) f(target)应该是 f ( t a r g e t − c o i n s [ 3 ] ) + 1 f(target -coins[3]) + 1 f(targetcoins[3])+1(假设最后这一枚硬币coins[3])
    • 也就是说,最后的话,有coins.size()种方案,我们要从这些种方案中选出最好的那个方案,即最少的硬币数,所以:
      • f ( t a r g e t ) = m i n f ( t a r g e t − c o i n s [ 0 ] ) + 1 , f ( t a r g e t − c o i n s [ 1 ] ) + 1 , . . . . . . , f ( t a r g e t − c o i n s [ c o i n s . s i z e ( ) − 1 ] ) + 1 f(target) = min{f(target-coins[0] ) + 1, f(target-coins[1]) + 1, ......, f(target-coins[coins.size() - 1] ) + 1} f(target)=minf(targetcoins[0])+1,f(targetcoins[1])+1,......f(targetcoins[coins.size()1])+1
    • 可以看出,每一步尝试coins.size()种硬币,一共target步。与递归算法相比,没有任何重复运算,所以时间复杂度是target * coins.size()

(2)第二步:转移方程

  • 设状态f[X]=最少用多少枚硬币拼出X,X为目标,f[X]为最少的硬币数
  • 对于任意X,它有coins.size()个最优子结构,它们之间的关系是:
    • f ( t a r g e t ) = m i n f ( t a r g e t − c o i n s [ 0 ] ) + 1 , f ( t a r g e t − c o i n s [ 1 ] ) + 1 , . . . . . . , f ( t a r g e t − c o i n s [ c o i n s . s i z e ( ) − 1 ] ) + 1 f(target) = min{f(target-coins[0] ) + 1, f(target-coins[1]) + 1, ......, f(target-coins[coins.size() - 1] ) + 1} f(target)=minf(targetcoins[0])+1,f(targetcoins[1])+1,......f(targetcoins[coins.size()1])+1

(3)第三步:初始条件和边界情况

  • 对于 f ( t a r g e t ) = m i n f ( t a r g e t − c o i n s [ 0 ] ) + 1 , f ( t a r g e t − c o i n s [ 1 ] ) + 1 , . . . . . . , f ( t a r g e t − c o i n s [ c o i n s . s i z e ( ) − 1 ] ) + 1 f(target) = min{f(target-coins[0] ) + 1, f(target-coins[1]) + 1, ......, f(target-coins[coins.size() - 1] ) + 1} f(target)=minf(targetcoins[0])+1,f(targetcoins[1])+1,......f(targetcoins[coins.size()1])+1,需要解决两个问题:
    • 什么时候停下来?
      • 也就是初始条件也是边界:F[0] = 0,当要拼出0元钱时只需要0枚硬币
    • 如果f(target-coins[i])小于0怎么办?
      • 小于0,也就是不能拼出时,如果不能拼出Y,就定义F[Y] = 正无穷,比如F[-1] = F[-2] = … = 正无穷(正无穷,是因为从公式中可以看出,是要求要求最小)

(4)第四步:计算顺序

  • 先初始条件F[0] = 0
  • 然后计算F[1]、F[2]、F[3]…F[27]

动态规划有两种写法:

递归解法:

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        std::vector<int> dp(amount + 1);
        dp[0] = 0;
        for (int i = 1; i <= amount; ++i) {
            dp[i] = INT_MAX;
            for (int coin : coins) {
                if(
                        i >= coin              // 背包大小 >= 物品大小
                        && dp[i - coin] != INT_MAX  // [背包容量 - 当前物品大小] = 剩下的容量  --> 能拼出 i - coins[j]
                        && dp[i - coin] + 1 < dp[i]  //  拼出[剩下的容量]的硬币数
                ){
                    dp[i] = dp[i - coin] + 1;
                }

            }
        }
        return  dp[amount] == INT_MAX ? -1 : dp[amount];
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值