• [DP]换钱的方法数


    题目三

    给定数组arr, arr中所有的值都为整数且不重复.每个值代表一种面值的货币,每种面值的货币可以使用任意张,在给定一个整数aim代表要找的钱数,求换钱有多少种方法.

    解法一 --暴力递归

    用0张arr[0], 让arr[1...N-1]组成剩下的钱,这样的方法数记为res1
    用1张arr[0], 让arr[1...N-1]组成剩下的钱(aim - arr[0]),方法数记为res2
    用2张arr[0], 让arr[1...N-1]组成剩下的钱(aim - 2 * arr[0]),方法数记为res3
    ...
    从上面可以看出,这是一个递归的过程,总的方法书为res1 + res2 +...

    代码一

    int process1(vector<int> arr, int index, int aim) {
        int res = 0;
        if (index == int(arr.size()))
            res = aim == 0 ? 1 : 0;
        else {
            for (int k = 0; k * arr[index] <= aim; k ++)  //k代表使用arr[index]的张数
                res += process1(arr, index + 1, aim - k * arr[index]);
        }
        return res;
    }
    
    // 暴力递归的方法
    int coins1(vector<int> arr, int aim) {
        if (arr.size() == 0 || aim < 0)
            return 0;
        return process1(arr, 0, aim);
    }
    

    方法二--记忆化搜索

    递归多次调用函数自己,会消耗大量的栈空间.递归的一个初级的优化方法就是用一张表记忆已经计算出的结果,避免重复计算.这张表的维度需要分析递归过程中那些是变量.从上面的代码中可以看到arr是不变的,只有index和aim在递归中是变化的,可以做出相应维度的map表来表示递归的过程.代码如下:

    代码

    
    int process2(vector<int> arr, int index, vector<vector<int> > &mapTable, int aim) {
        int res = 0;
        if (index == int(arr.size()))
            res = aim == 0? 1:0;
        else {
            //mapTable 中的特殊值,0说明mapTable[i][j]没有被计算过,-1表示计算过,但是返回值为0
            int mapValue = 0;
            for (int k = 0; k * arr[index] <= aim; k ++) {
                mapValue = mapTable[index + 1][aim - k * arr[index]];
                if (mapValue != 0)
                    res += mapValue == -1 ? 0 : mapValue;
                else
                    res += process2(arr, index + 1, mapTable, aim - arr[index] * k);
            }
            mapTable[index][aim] = res == 0 ? -1 : res;
        }
        return res;
    }
    
    //记忆化搜索方法
    int coins2(vector<int> arr, int aim) {
        if (arr.size() == 0 || aim < 0)
            return 0;
    
        //记忆搜索表,用于保存递归的中间结果,避免重复计算
        vector<vector<int> > mapTable(arr.size() + 1, vector<int>(aim + 1));
        return process2(arr, 0, mapTable, aim);
    }
    

    方法三--动态规划

    可以直接参考代码,时间复杂度为O(N * aim^2)

    代码

    // 动态规划的方法
    int coins3(vector<int> arr, int aim) {
        int length = arr.size();
        if (length == 0 || aim < 0)
            return 0;
    
        vector<vector<int> > dp(length, vector<int>(aim + 1));
        for (int k = 0; k * arr[0] <= aim ; k ++)  // 初始化第一行
                dp[0][k * arr[0]] = 1;
    
        for (int i = 1; i < length; i ++) // 初始化第一列,0张a[i]可以构成0元钱也是一种方法
            dp[i][0] = 1;
    
        int sum;
        for (int i = 1; i < length; i ++) {
            for (int j = 1; j <= aim; j ++) {
                sum = 0;
                for (int k = 0; k * arr[i] <= j; k++)
                    sum += dp[i - 1][j - k * arr[i]];
                dp[i][j] = sum;
            }
        }
    
        return dp[length - 1][aim];
    }
    

    动态规划--优化

    上面的DP代码最内层的for循环是根据递归中的步骤来表达的.步骤一的方法数为dp[i-1][j],而第二种到第k种情况的方法数累加值其实就是dp[i][j-arr[i]]的值.所以递推方程为:
    dp[i][j] = dp[i-1][j] + dp[i][j-arr[i]]
    这样时间复杂度就简化成了O(N*aim),

    还有进一步的优化就是空间,上面的动态规划空间复杂度都是O(N*aim),通过把二维的dp数组用一维数组来代替,滚动计算能够是空间复杂度降低到O(aim)

  • 相关阅读:
    【Java并发编程】:使用synchronized获取互斥锁
    【Java并发编程】:Runnable和Thread实现多线程的区别
    【Java并发编程】:volatile变量修饰符
    【Java并发编程】:守护线程与线程阻塞的四种情况
    【Java并发编程】:线程挂起、恢复与终止
    【Java并发编程】:线程中断
    redis配置详细解析
    redis常用命令
    centos7上安装redis
    redis简介
  • 原文地址:https://www.cnblogs.com/mooba/p/7473297.html
Copyright © 2020-2023  润新知