您的当前位置:首页正文

leetcode:面试题 08.11. 硬币

2024-11-11 来源:个人技术集锦

题目来源

题目描述

class Solution {
public:
    int waysToChange(int n) {

    }
};

相当于给定一个面值数组,其中的值都是正数且没有重复。再给定一个正数aim。每个值都认为是一种面值,且认为张数是无限的。返回组成aim的最少货币数。

题目解析(测试未通过,待研究)

为什么要对 dp[idx][rest] %= 1000000007;

暴力递归

既然有一些硬币面值,那么我们就先用一个数组,将所有硬币的面值存储起来。注意,这里有一个潜规则:硬币在数组中的顺序,最好按照大小排序,升序和降序都可以

int[] coins = {1, 5, 10, 25}; //升序排列

还有一个情况:假设n = 10,硬币的组合方式有一种是1+1+1+1+1+5,这等价于1+5+1+1+1+1。这两种其实是一种情况。从这里我们可以推测出,在使用硬币的时候,相同的硬币最好放在一起使用,也就是不要出现上面一个5,将1分割成了两部分,如下图:

那怎么做呢?应该这样做:在使用一种硬币的时候,保证一次性拿够(数量)

算法如下:

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

        long ways = 0;
        for (int zhang = 0; zhang * coins[idx] <= rest; ++zhang) {
            ways += process(coins, idx + 1, rest - zhang * coins[idx]);
        }
        return ways;
    }
public:
    int waysToChange(int aim) {
        if(aim < 0){
            return -1;
        }

        std::vector<int> coins{1, 5, 10, 25};
        return (int)process(coins, 0, aim);
    }
};

以上就是递归的版本,核心就在于,相对于每一种硬币来说,我可以选择不要,又或者是要1个,或者是2个……,以这样的方式,一个一个的去尝试,这非常的暴力。

暴力递归改动态规划

(1)准备一个数组。先来分析递归函数的可变参数以及变化范围

 long process(std::vector<int> &coins, int idx,  int rest)
  • idx:0~N
  • rest:0~aim

有两个变化维度,所以准备一个二维数组:

long dp[N + 1][aim + 1];

(2)返回值。分析主函数值怎么调用递归函数的

  return (int)process(coins, 0, aim);

所以应该返回dp[0][aim]

(3)填表

(3.1)使用base case 初始化表

	  if(idx == N){
            return rest == 0 ? 1 : 0;
        }
  • 如上:dp[N][0] = 1,dp[N][1…rest] = 0

(3.3)根据依赖填写表的其他部分

		long ways = 0;
        for (int zhang = 0; zhang * coins[idx] <= rest; ++zhang) {
            ways += process(coins, idx + 1, rest - zhang * coins[idx]);
        }
        return ways;
  • 可以看出dp[idx][rest] 依赖 dp[idx + 1][rest - zhang* coins[idx]]

  • 所以,应该从下往上,从左往右(从右往左也可以)填写
 for (int idx = N - 1; idx >= 0; --idx) {
            for (int rest = 0; rest <= aim; ++rest) {
                
            }
        }

(4)综上,递归函数如下

class Solution {
public:
    int waysToChange(int aim) {
        if(aim < 0){
            return -1;
        }

        std::vector<int> coins{1, 5, 10, 25};
        int N = coins.size();
        std::vector<std::vector<long>> dp(N + 1, std::vector<long>(aim + 1, 0));
        dp[N][0] = 1;
        for (int idx = N - 1; idx >= 0; --idx) {
            for (int rest = 0; rest <= aim; ++rest) {
                dp[idx][rest] = dp[idx + 1][rest];
                if(rest - coins[idx] >= 0){
                    dp[idx][rest] += dp[idx][rest - coins[idx]];
                }
            }
        }

        return dp[0][aim];
    }
};

继续优化(待续)

其实上面写的dp,只是较为经典的写法,本质上,这段代码还是存在一定的小问题,那就是存在枚举问题,也就是说,效率还不够高。还可以进行优化。

(这里应该有一张表格)

从上面动态规划填表中可以看出:

假设rest = 7时

  • d p [ 2 ] [ 7 ] = d p [ 3 ] [ 7 ] + d p [ 3 ] [ 5 ] + d p [ 3 ] [ 3 ] + d p [ 3 ] [ 1 ] dp[2][7] = dp[3][7] + dp[3][5] + dp[3][3] + dp[3][1] dp[2][7]=dp[3][7]+dp[3][5]+dp[3][3]+dp[3][1] + dp[3][-1]
  • d p [ 2 ] [ 9 ] = d p [ 3 ] [ 9 ] + d p [ 3 ] [ 7 ] + d p [ 3 ] [ 5 ] + d p [ 3 ] [ 3 ] + d p [ 3 ] [ 1 ] dp[2][9] = dp[3][9] + dp[3][7] + dp[3][5] + dp[3][3] + dp[3][1] dp[2][9]=dp[3][9]+dp[3][7]+dp[3][5]+dp[3][3]+dp[3][1] + dp[3][-1]

从而:

  • d p [ 2 ] [ 9 ] = d p [ 3 ] [ 9 ] + d p [ 2 ] [ 7 ] ; dp[2][9] = dp[3][9] + dp[2][7]; dp[2][9]=dp[3][9]+dp[2][7];

推而广之:

  • d p [ i d x ] [ r e s t ] = d p [ i d x + 1 ] [ r e s t ] + d p [ i d x ] [ r e s t − c o i n s [ i d x ] ] ; dp[idx][rest] = dp[idx + 1][rest] + dp[idx][rest - coins[idx]]; dp[idx][rest]=dp[idx+1][rest]+dp[idx][restcoins[idx]];
  • 我们只需要确保rest - coins[idx]存在表格中即可
class Solution {
public:
    int waysToChange(int aim) {
        if(aim < 0){
            return -1;
        }

        std::vector<int> coins{1, 5, 10, 25};
        int N = coins.size();
        std::vector<std::vector<int>> dp(N + 1, std::vector<int>(aim + 1, 0));
        dp[N][0] = 1;
        for (int idx = N - 1; idx >= 0; --idx) {
            for (int rest = 0; rest <= aim; ++rest) {
                dp[idx][rest] = dp[idx + 1][rest];
                if(rest - coins[idx] >= 0){
                    dp[idx][rest] += dp[idx][rest - coins[idx]];
                }
            }
        }
        
        return dp[0][aim];
    }
};

备忘录 VS 动态规划

  • 备忘录算法就是某种形态的动态规划方法
  • 备忘录算法不关心到达某一个递归路径的路程,只是每次算之前先看看有没有计算过,如果没有,就去算
  • 动态规划方法则是规定好每一个递归霍城的计算顺序,一次进行计算,后面的计算过程严格依赖前面的计算过程。
  • 动态规划方法:表有多大,就算几次

所以如果单独一个格子计算没有枚举行为(for循环),那么它们的复杂度是一样的;但是如果有for枚举,那么需要弄出严格表结构之后继续优化

思路二

参考

显示全文