[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)

posted @ 2017-09-04 13:54  木白的菜园  阅读(504)  评论(0编辑  收藏  举报