换钱的方法数

题目:
给定数组arr,arr中所有的值都为整数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数代表要找的钱数,求换钱有多少种方法。
举例:
arr = [5, 10, 25, 1], aim = 0
组成0元的方法有1种,就是所有面值的货币都不用。所以返回1。
arr = [5, 10, 25, 1] , aim = 15
组成15元的方法有6种,分别为3张5元、1张10元+1张5元、1张10元+5张1元、10张1元+1张5元、2张5元+5元一张和15元一张。所以返回6。
arr = [3, 5] , aim = 2
任何方法都无法组成2元。所以返回0。

【详解】
这道题可以体现暴力递归、记忆搜索和动态规划之间的关系,并且还可以在动态规划的基础上再进行一次优化。
暴力递归的方法。如果arr = [5, 10, 25, 1] , aim = 1000, 分析过程如下:
1. 用0张5元的货币,让[10, 25, 1]组成剩下的1000, 最终方法数记为res1。
2. 用1张5元的货币,让[10, 25, 1]组成剩下的995, 最终方法数记为res2。
3. 用2张5元的货币,[10, 25, 1]组成剩下的990,最终方法数记为res3。
………
201. 用200张5元的货币,[10, 25, 1]组成剩下的0,最终方法数记为res201。
那么res1 + res2 + … + res201的值就是总的方法数。根据如上的分析过程定义递归函数process(arr, index, aim),它的含义是如何用arr[index…N-1]这些面值的钱组成aim,返回总的方法数。

#include<iostream>
#include<vector>
using namespace std;

int process(vector<int> arr, int index, int aim)
{
    int res = 0;
    if(index == arr.size())
    {
        if(aim == 0)
            res = 1;
        else
            res = 0;
    }
    else
    {
        for(int i = 0; arr[index] * i <= aim; i ++)
            res = res + process(arr, index+1, aim - arr[index]*i);
    }
    return res;
}

int coins(vector<int> arr, int aim)
{
    if(arr.size() == 0 || aim < 0)
        return 0;
    else
        return process(arr, 0, aim);
}


int main()
{
    vector<int> arr;
    arr.clear();
    arr.push_back(5);
    arr.push_back(10);
    arr.push_back(25);
    arr.push_back(1);

    int aim[3] = {1, 15, 1000};

    cout<<"coins(arr, 1) = "<<coins(arr, aim[0])<<endl;
    cout<<"coins(arr, 15) = "<<coins(arr, aim[1])<<endl;
    cout<<"coins(arr, 1000) = "<<coins(arr, aim[2])<<endl;
    return 0;
}

暴力递归之所以暴力,是因为存在着大量的重复计算。还是用上面说过的例子。当已经使用0张5元+1张10元的情况下,后续应该求[25, 1]组成剩下的990的方法总数。当已经使用2张5元+0张10元的情况下,后续还是求[25, 1]组成剩下的990的方法总数。两种情况下都需要求process(arr, 2, 990)。类似这样的重复计算在暴力递归的过程中大量产生,所以暴力递归方法的时间复杂度非常高,并且与arr中钱的面值有关,最差的情况下为O(aim^N)。

下面我们进行一些优化处理,process(arr, index, aim)中arr始终不变的,变化的只有index和aim,所以可以用p(index, aim)表示一个递归的过程。重复计算之所以大量缠身,是因为没一个递归过程都没记下来,所以下次还要重复去求,所以可以事先准备好一个map,每计算完一个递归过程,都将结果记录到map中。当下次进行同样的递归过程之前,现在map中查询这个递归过程是否已经计算过,如果已经计算过,就把值拿出来直接用,如果没有计算过,需要再进入递归过程。具体看下面的代码中的coins2方法,它和coins1方法的区别就是准备好全局变量map,记录已经计算过的递归过程的结果,防止下次重复计算。因为本题的递归过程可由两个变量表示,所以map是一张二维表。map[i][j]表示递归过程p(i,j)的返回值。另外还有一些特别值,map[i][j]表示递归过程p(i,j)从来没有计算过。map[i][j] == -1表示递归过程p(i,j)计算过,但返回值是0。如果map[i][j]的值既不等于0,也不等于-1,记为a,则表示递归过程p(i, j)的返回值为a。

#include<iostream>
#include<vector>
using namespace std;

int **map;

int process2(vector<int> arr, int index, int aim, int **map)
{
    int res = 0;
    if(index == arr.size())
    {
        if(aim == 0)
            res = 1;
        else
            res = 0;
    }
    else
    {
        int mapValue = 0;
        for(int i = 0; arr[index]*i <= aim; i ++)
        {
            mapValue = map[index+1][aim-arr[index]*i];
            if(mapValue != 0)
            {
                if(mapValue == -1)
                    res += 0;
                else
                    res += mapValue;
            }
            else
                res += process2(arr, index+1, aim-arr[index]*i, map);
        }
    }
    if(res == 0)
        map[index][aim] = 0;
    else
        map[index][aim] = res;
    return res;
}

int coins2(vector<int> arr, int aim)
{
    if(arr.size() == 0 || aim < 0)
        return 0;

    //动态申请map内存空间 
    map = new int*[arr.size()+1];
    for(int i = 0; i < arr.size()+1; i ++)
        map[i] = new int[aim+1];

    //map数组进行初始化    
    for(int i = 0; i < arr.size()+1; i ++)
        for(int j = 0; j < aim+1; j ++)
            map[i][j] = 0;

    return process2(arr, 0, aim, map);
}


int main()
{
    vector<int> arr;
    arr.clear();
    arr.push_back(5);
    arr.push_back(10);
    arr.push_back(25);
    arr.push_back(1);

    int aim[3] = {1, 15, 1000};

    cout<<"coins1(arr, 1) = "<<coins2(arr, aim[0])<<endl;
    cout<<"coins1(arr, 15) = "<<coins2(arr, aim[1])<<endl;
    cout<<"coins1(arr, 1000) = "<<coins2(arr, aim[2])<<endl;
    return 0;
}

这个优化步骤就是记忆化搜索。它是暴力递归的最初级的优化技巧,分析递归函数的状态可以由哪些变量表示,做出相应维度和大小的map即可。记忆化搜索方法的时间复杂度为O(N*aim^2)。此题还可以运用动态规划的思想和空间压缩的技巧再去进行进一步的优化!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值