题目:
给定数组arr,arr中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim,代表要找的钱数,求换钱有多少种方法。
举例:
arr = [5, 10, 25, 1] aim=0
组成0元的方法有一种,就是所有面值的货币都不用,返回1
arr = [5, 10, 25, 1] aim=15
有6种,3*5,10+5,10+5*1,10*1+5,2*5+5*1,15*1,返回6
方法一:暴力递归
代码实现:
public static int coins1(int[] arr,int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return -1;
}
return process1(arr,0,aim);
}
//用 arr[index]的面值组成aim,返回总的方法数
public static int process1(int[] arr,int index, int aim) {
int res = 0;
//当没有可用面值的时候 判断此时aim是否为0,如果为0的话,返回1,如果不为0,没有可以组成不为0的方法,返回0
if (index == arr.length) {
res = aim == 0 ? 1 : 0;
} else {
for (int k = 0; k * arr[index] <= aim; k++) {
res += process1(arr,index+1,aim-k*arr[index]);
}
}
return res;
}
方法二:使用记忆搜索方法优化暴力递归:
对于arr=[5,10,25,1] aim=1000
当使用0张5元+1张十元,或者使用2张5元+0张十元,他们都会去递归 process1(arr,2,990)
在暴力递归方法中存在重复计算的问题
现在使用一个map来存放之前递归求得的值,在后面递归的时候先去map里面查
代码实现:
public static int coins2(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return -1;
}
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 k = 0; k * arr[index] <= aim; k++) {
//从map里拿值,判断之前这个递归过程是否重复计算过
mapValue = map[index+1][aim-arr[index]*k];
if (mapValue != 0) {
res += mapValue == -1 ? 0 : mapValue;
//如果该递归过程没有计算过,就正常计算
}else {
res += process2(arr, index+1, aim - arr[index] * k, map);
}
}
}
//将结果存入map
map[index][aim] = res == 0 ? -1 : res;
return res;
}
方法三:使用动态规划进行优化:
public static int coins3(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0 ) {
return 0;
}
int[][] dp = new int[arr.length][aim+1];
//将第一列初始化为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;
}
int num = 0;
for (int i = 1; i <arr.length; i++) {
for(int j = 1; j <= aim; j++) {
num = 0;
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];
}
继续优化:
对于上述优化,计算其他 dp[i][j] 位置的时候,其实可以进一步优化:
dp[i][j] = dp[i-1][j] + dp[i][j-arr[i]]
dp[1][5]的含义:使用5元和10元组成5的方法数,只有1种;
dp[1][5] = dp[0][5] + dp[1][5-10] = 1+0 = 1
dp[1][10]的含义:使用5元和10元组成10的方法数,有两种
dp[1][10] = dp[0][10] + dp[1][10-10] = 1+1 = 2
public static int coins4(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0 ) {
return 0;
}
int[][] dp = new int[arr.length][aim+1];
//将第一列初始化为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.length-1][aim];
}