[动态规划] 换钱的最少货币数

  算法专题导航页面


【题目描述】
    给定数组arr,arr中所有的值都为正整数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个aim,代表要找的钱数,求组成aim的最少货币数。

【输入描述】
    输入包括两行,第一行两个整数n(0<=n<=1000)代表数组长度和aim(0<=aim<=5000),第二行n个不重复的正整数,代表数组arr(其任何一个元素取值介于1到1000000000之间 )。

【输出描述】
    输出一个整数,表示组成aim的最小货币数,无解时输出-1.


【示例1】
    输入
    3 20
    5 2 3
    输出
    4
    说明
    20=5*4
    示例2
    输入
    3 0
    5 2 3
    输出
    0
    示例3
    输入
    2 2
    3 5
    输出
    -1

【备注】
    时间复杂度O(n*aim)O(n∗aim),空间复杂度O(n)O(n)。


【代码实现 - CPP版】

#include<iostream>
#include<vector>

using namespace std;

/*
 * 当前货币面值为vec[index], 待找零的金额为money
 * 1. 返回值 -1:基于货币面值vec[0], ..., vec[n-1]无法找零剩余金额money
 * 2. 返回值非-1:基于货币面值vec[0], ..., vec[n-1]找零金额money所需要的最少货币数量
 */
int min_coins(const vector<int>& vec, int index, int money) {
    int vec_size = vec.size();
    if (0 == vec_size || 0 > money) {
        return -1;
    }
    /*
     * 已经没有面值需要被考虑 -- base case
     * 1. money值为0,代表此时剩余的待找零金额为0,那么所需货币数量也为0,返回0
     * 2. money值非0,代表剩余的待找零金额非零,那么找不到一个合理的组合满足需求,返回-1
     */
    if (index == vec_size) {
        return money == 0 ? 0 : -1;
    }

    // 返回值初始化为-1,代表暂时还未找到有效解
    int result = -1;

    // 依次尝试不同数量k的面值为vec[index]的货币,其总和不能大于待找零金额money
    for (int k=0; k*vec[index]<=money; k++) {
        // 剩下的待找零金额money-k*vec[index]交给剩下的面值vec[index+1]去搞定
        int next = min_coins(vec, index+1, money-k*vec[index]);
        // 只有当后续找零组合存在,才更新最终的返回值
        if (-1 != next) {
            result = result == -1 ? next+k : min(result, next+k);
        }
    }

    return result;
}

/*
 * 动态规划优化
 * 1. 确定可变参数:index, money -- 返回值确定
 * 2. 利用可变参数组成一张二位表,该表一定可以包含所有的返回值。
 *    二维表中index取值范围:0, ..., n
 *    二维表中money取值范围:0, ..., aim
 *    最终的二维表是一个n+1行aim+1列的表,记为dp[][]
 * 3. 确定最终状态:min_cons(vec, 0, aim),也就是dp[0][aim],位于二维表0行最后一列
 * 4. 确定base case(index == vec.size()),初始化二维表,
 *    也即初始化二维表的最后一行dp[n][...],且dp[n][0]==1
 * 5. 递推base case之外的位置的值 min_coins(vec, i, rest),
 *    其中i != n && rest != 0,也即二维数组元素dp[i][rest]
 *    dp[i][rest] = min{dp[i+1][rest], dp[i+1][rest-vec[i]]+1, ..., dp[i+1][rest-k*vec[i]+k]}
 *    也即其依赖于i+1行的所有值,需要从中寻找最小的那个值。同样的,处于同行的元素dp[i][rest-vec[j]]也依赖于
 *    其左下方及正下方的值。
 *    通过上面分析,二位表中所有非base case的元素都与其左侧及正下方的元素相关,确切的说:
 *    dp[i][rest] = min{dp[i][rest-vec[j]] + 1, dp[i+1][rest]}
*  6. 依据前述,我们已经得知二维表最后一行的元素值,现在我们只需要针对第n-1行到0行,依照从左到右的原则依次填充即可
*     最终返回dp[0][aim]即可
*/

int min_coins_dp(const vector<int> vec, int aim) {
    int size = vec.size();
    if (0 == size || 0 > aim) {
        return -1;
    }

    // define the dp array
    vector<vector<int>> vec_dp(size+1, vector<int>(aim+1, -1));

    // init array according to the base case
    vec_dp[size][0] = 0;

    // scan each row from n-1 to 0 and each column from left to right
    for (int i=size-1; i>=0; i--) {
        for (int rest=0; rest<=aim; rest++) {
            // init dp[i][j] as invalid
            vec_dp[i][rest] = -1;
            // judge the element below
            if (-1 != vec_dp[i+1][rest]) {
                // firstly, update its value to its below
                vec_dp[i][rest] = vec_dp[i+1][rest];
            }
            
            // judge the left element and compare it with the below element once it exists
            if (0 <= rest-vec[i] && -1 != vec_dp[i][rest-vec[i]]) {
                if (-1 == vec_dp[i][rest]) {
                    vec_dp[i][rest] = vec_dp[i][rest-vec[i]] + 1;
                } else {
                    vec_dp[i][rest] = min(vec_dp[i][rest], vec_dp[i][rest-vec[i]]+1);
                }
            }
        }
    }

    return vec_dp[0][aim];
}


int main() {
    // input the first line
    int n, aim;
    cin >> n >> aim;

    // input the second line
    vector<int>  vec_coins;
    int tmp = 0;
    while (n --) {
        cin >> tmp;
        vec_coins.push_back(tmp);
    }

    // find the least coins number in range [0, ..., vec_coins.size()-1]
    //cout << min_coins(vec_coins, 0, aim) <<endl;
    cout << min_coins_dp(vec_coins, aim);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值