换钱的方法数

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

一、暴力递归

换钱的方法数_第1张图片

public static int coins1(int[] arr, int aim){
        // 排除极端条件
        if (arr == null || arr.length == 0 || aim < 0){
            return 0;
        }
        return process1(arr, 0, aim);
    }

    public static int process1(int[] arr, int index, int aim){
        int res = 0;
        // 判断是否到达递归的终止条件
        if (index == arr.length){
            res = aim == 0 ? 1 : 0;
        } else {
            //
            for (int i = 0; arr[index] * i <= aim; i++){
                //index:当前需要去拼凑的钱 aim - arr[index] * i:剩下的钱
                res += process1(arr, index + 1, aim - arr[index] * i);
            }
        }

        return res;
    }

二、暴力优化1

在暴力搜索方法的函数中看看哪些参数可以代表递归过程

暴力递归都会存在大量的重复计算,是因为每一个递归过程的结果都没被记下来,所以下次还会重复计算,所以可以准备一个map,每计算完一个递归过程,都将结果记录到map中,当下次进行同样的递归过程之前,先在map中查询这个递归过程是否已经计算过,如果已经计算过,就直接拿出来用,如果没有计算过,需要再进入递归过程。记忆化搜索的方法是针对暴力递归最初级的优化,分析递归函数的状态可以由那些变量表示,做出相应的维度和大小的map即可,时间复杂度为O(N*am^2)。

public static int coins2(int[] arr, int aim){
        if (arr == null || arr.length == 0 || aim < 0){
            return 0;
        }

        // 暴力递归会产生大量的重复,是因为每个递归都被记录下来了,所以还要去重
        // 可以通过一个map来表示p(index, aim)--p[index][aim]如果为重复计算的则不必重复去求了。
        int[][] map = new int[arr.length + 1][aim + 1];
        return process2(arr, 0, aim, map);
    }

    public static int process2(int[] arr, int index, int aim, int[][] map){
        int res = 0;
        if (index == arr.length){
            res = aim == 0 ? 1 : 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){
                    res += mapValue == -1 ? 0 : mapValue;
                } else {
                    res += process2(arr, index + 1, aim - arr[index] * i, map);
                }
            }
        }
        map[index][aim] = res == 0 ? -1 : res;
        return res;
    }

三、记忆搜索

记忆化搜索的方法是针对暴力递归最初级的优化技巧,分析递归函数的状态可以由哪些变量表示,做出相应纬度和大小的map即可。记忆化搜索方法的时间复杂度为O(N X aim^2).

动态规范方法。生成行数为N、列数为aim + 1的矩阵dp,dp[i][j]的含义是在使用arr[0...i]货币的情况下,组成钱数为j有多少种方法。dp[i][j]的值求法如下:

1. 对于矩阵dp第一列值dp[..][0],表示组成钱数为0的方法数,很明显是一种,也就是不使用任何货币。所以dp第一列的值统一设置为1.

2. 对于矩阵dp第一行的值dp[0][..],表示只能使用arr[0]这一种货币的情况下,组成钱数的方法,比如arr[0] = 5,能组成的钱数只有0, 5, 10, 15, ...。所以,令dp[0][k*arr[0]] = 1(0 <=k*arr[0]<=aim, k为非负整数).

3.除第1行和第一列的其它位置,记为位置(i,j)。dp[i][j]的值时以下几个值的累加。

  • 完全不用arr[i]货币,只使用arr[0...i-1]货币时,方法数为dp[i-1][j].
  • 用1张arr[i]货币,剩下的钱用arr[0...i-1]货币组成时,方法数为dp[i-1][j-arr[i]]。
  • 用2张arr[i]货币,剩下的钱用arr[0...i-1]货币组成,方法数为dp[i-1][j-2*2*arr[i]].
  • 。。。
  • 用k张arr[i]货币,剩下的钱用arr[0...i-1]货币组成时,方法数为dp[i-1][j-k*arr[i]].

4. 最终dp[N-1][aim]的值就是最终结果。

public int coins3(int[] arr, int aim){
    if (arr == null || arr.length == 0|| aim < 0){
        return 0;
    }
    int [][] dp = new int[arr.length][aim + 1];
    for (int i = 0; i < arr.length; i ++){
        dp[i][0] = 1; //对于所有的钞票而言组成0元的方式都只有1种
    }
    for (int j = 1; arr[0] * j <= aim ; j ++){
        dp[0][arr[0] * j] = 1; // 第一张纸币组成任何这个纸币的倍数的目标的方法都只有一种
    }
    int num = 0;
    
    for (int i = 1; i < arr.lenght; i ++){
        for (int j = 1; j <= aim; j ++){
            num = 0;
            // 转移方程dp[i][j] = dp[i-1][j - k*arr[i]]
            for (int k = 0; j - arr[i] * k >= 0; k ++){
                num += dp[i - 1][j - arr[i] * k];
            }
            dp[i][j] = num;
        }
    }
    return dp[arr.length - 1][aim];
}
        

四、动态规划O(N * aim)

换钱的方法数_第2张图片

public int coins4(int[] arr, int aim){
    if (arr == null || arr.length == 0 || aim < 0){
        return 0;
    }
    int[][] dp = new dp[arr.length][aim + 1];
    for (int i = 0; i < arr.length; i ++){
        dp[i][0] = 1;
    }
    for (int j = 1; arr[0] * j < aim; j ++){
        dp[0][arr[0] * j] = 1;
    }
    for (int i = 1; i < arr.length; i ++){
        for (int j = 1; j < aim ;  j++){
            dp[i][j] = dp[i - 1][j];
            dp[i][j] += j - arr[i] >= 0 ? dp[i][j - arr[i]] : 0;
        }
    }
    return dp[arr.lenght -1 ][aim];
}

时间复杂度为O(N * aim) 的动态规划方法再结合空间压缩的技巧。

public int coins5(int[] arr, int aim){
    if (arr == null ||arr.length == 0 || aim < 0){
        return 0;
    }
    int [] dp = new int[aim + 1] ;
    for (int i = 1; i < arr.length; i ++){
        for (int j = 1; i <= aim; j ++){
            dp[j] += j - arr[i] >= 0 ? dp[j - arr[i]] : 0; 
        }
    }
    return dp[aim];
}

 

你可能感兴趣的:(数据结构与算法,动态规划,java,数据结构)