题目:
arr是货币数组,其中的值都是正数。再给定一个正数aim。 每个值都认为是一张货币, 即便是值相同的货币也认为每一张都是不同的, 返回组成aim的方法数 例如:arr = {1,1,1},aim = 2 第0个和第1个能组成2,第1个和第2个能组成2,第0个和第2个能组成2 一共就3种方法,所以返回3
假设,这个一维数组为 【1,7,3,5】,目标值aim为4. 我们就可以以肉眼可见的速度知道,返回值只有1种,即 1+3.
那么该如何推导呢?
其实,所谓的从左到右尝试模型,口诀就是 针对固定集合,值不相同,就是讨论要和不要的累加和。
怎么理解呢?
就是要1 和不要1 。要7 和不要7,依此类推.....
以下是我推导的全部过程,Y代表要,N代表不要。
所谓的累加和,就是把所有推导的结果进行相加。
递归版本:
//暴力递归
public static int coinsWay(int[] arr, int aim)
{
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
return process(arr, 0, aim);
}
public static int process (int[] arr, int index, int rest)
{
//rest小于0, 代表已经超过目标值了
if (rest < 0)
{
return 0;
}
if (index == arr.length) { // 没钱了!
return rest == 0 ? 1 : 0;
} else {
return process(arr, index + 1, rest) + process(arr, index + 1, rest - arr[index]);
}
}
既然有了递归版本,接下来就是如何修改成动态规划的代码了。
1. 动态规划,首先就是定义一个二维数组。通常情况下,我们以当前数组的行为行,以aim为列。
2. 然后构建二维数组表,然后根据递归版本构建出整张表的初始值。
rest 0 | rest 1 | rest 2 | rest 3 | rest 4 | |
index 0 | |||||
index 1 | |||||
index 2 | |||||
index 3 | |||||
index 4 |
这里很多人会觉得奇怪,明明数组只有4个值,aim也只是4,为什么要构建5行5列的二维数组呢?
因为,我们在递归的时候有index == arr.length的判断,所有要多一行。从左到右尝试模型,一般是直接返回aim处的值,所以要多构建一列。这就是技巧,记住就好。
3. 根据递归推导数据:
if (rest < 0) { return 0; } 代表列小于0,针对动态规划版本的二维数组,第一次进入是不存在的
if (index == arr.length) { return rest == 0 ? 1 : 0 } 这就是为什么要多建一行的原因。由此可以推导出,如果是最后一行第一列为1,其余的都为0.可得:
rest 0 | rest 1 | rest 2 | rest 3 | rest 4 | |
index 0 | |||||
index 1 | |||||
index 2 | |||||
index 3 | |||||
index 4 | 1 | 0 | 0 | 0 | 0 |
4. 根据 process(arr, index + 1, rest) + process(arr, index + 1, rest - arr[index]); 可知。每一列都是按照此规则进行依赖的。下面进行逐行推导:
dp[3][0] = dp[4][0] + 0; 这个0是根据 rest - arr[index]得到的。如果rest - arr[index] 大于0,就返回dp[index+1][ rest - arr[index]的值。如果小于0, 折直接返回0。
下面的大于0,小于0,都是 rest - arr[index]的结果. rest为列的小标,arr[index]为一维数组的值,即对应行的下标
rest 0 | rest 1 | rest 2 | rest 3 | rest 4 | |
index 0 (1) | |||||
index 1 (7) |
|||||
index 2 (3) |
|||||
index 3 (5) |
0 - 5 < 0 得到0 dp[4][0] + 0: 1+ 0 = 1. 此列为1 |
1 - 5 < 0 得到0 0 + 0 = 0; 此列为 0 |
依次类推: 0 + 0 = 0 |
0 + 0 =0 | 0 + 0 = 0 |
index 4 | 1 | 0 | 0 | 0 | 0 |
index2推导:
rest 0 | rest 1 | rest 2 | rest 3 | rest 4 | |
index 0 (1) | |||||
index 1 (7) |
|||||
index 2 (3) |
1 | 0 | 0 |
此时rest = arr[2],都为3 所以此列为 dp[3][3] + dp[3][0]. 即 0 + 1= 1 |
0 |
index 3 (5) |
0 - 5 < 0 得到0 dp[4][0] + 0: 1+ 0 = 1. 此列为1 |
1 - 5 < 0 得到0 0 + 0 = 0; 此列为 0 |
依次类推: 0 + 0 = 0 |
0 + 0 =0 | 0 + 0 = 0 |
index 4 | 1 | 0 | 0 | 0 | 0 |
index1推导:
rest 0 | rest 1 | rest 2 | rest 3 | rest 4 | |
index 0 (1) | |||||
index 1 (7) |
1 | 0 | 0 | 1 | 0 |
index 2 (3) |
1 | 0 | 0 |
此时rest = arr[2],都为3 所以此列为 dp[3][3] + dp[3][0]. 即 0 + 1= 1 |
0 |
index 3 (5) |
0 - 5 < 0 得到0 dp[4][0] + 0: 1+ 0 = 1. 此列为1 |
1 - 5 < 0 得到0 0 + 0 = 0; 此列为 0 |
依次类推: 0 + 0 = 0 |
0 + 0 =0 | 0 + 0 = 0 |
index 4 | 1 | 0 | 0 | 0 | 0 |
index 0推导:
rest 0 | rest 1 | rest 2 | rest 3 | rest 4 | |
index 0 (1) | 1 | rest = arr[0], 都为1; 所以可得到 dp[1][1] + dp[1][0]; 即 0+1 = 1 |
dp[1][2] + dp[1][1]; 即 0+0 = 0 |
dp[1][3] + dp[1][1]; 即 1+0 =1 |
dp[1][4] + dp[1][3]; 即 0+1= 1 |
index 1 (7) |
1 | 0 | 0 | 1 | 0 |
index 2 (3) |
1 | 0 | 0 |
此时rest = arr[2],都为3 所以此列为 dp[3][3] + dp[3][0]. 即 0 + 1= 1 |
0 |
index 3 (5) |
0 - 5 < 0 得到0 dp[4][0] + 0: 1+ 0 = 1. 此列为1 |
1 - 5 < 0 得到0 0 + 0 = 0; 此列为 0 |
依次类推: 0 + 0 = 0 |
0 + 0 =0 | 0 + 0 = 0 |
index 4 | 1 | 0 | 0 | 0 | 0 |
因此,最终的返回值 dp[0][4] 为1:
代码如下:
//动态规划
public static int coinsWay2(int[] arr, int aim)
{
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
if (aim == 0) {
return 1;
}
int N = arr.length;
int[][] dp = new int[N + 1][aim + 1];
dp[N][0] = 1;
for (int index = N - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
dp[index][rest] = dp[index + 1][rest] + (rest - arr[index] >= 0 ? dp[index + 1][rest - arr[index]] : 0);
}
}
return dp[0][aim];
}
完整代码,添加对数器进行海量数据测试:
package code03.动态规划_07.lesson4;
/**
* arr是货币数组,其中的值都是正数。再给定一个正数aim。
* 每个值都认为是一张货币,
* 即便是值相同的货币也认为每一张都是不同的,
* 返回组成aim的方法数
* 例如:arr = {1,1,1},aim = 2
* 第0个和第1个能组成2,第1个和第2个能组成2,第0个和第2个能组成2
* 一共就3种方法,所以返回3
*/
public class ContainWaysEveryPaperDiff_04 {
//暴力递归
public static int coinsWay(int[] arr, int aim)
{
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
return process(arr, 0, aim);
}
public static int process (int[] arr, int index, int rest)
{
//rest小于0, 代表已经超过目标值了
if (rest < 0)
{
return 0;
}
if (index == arr.length) { // 没钱了!
return rest == 0 ? 1 : 0;
} else {
return process(arr, index + 1, rest) + process(arr, index + 1, rest - arr[index]);
}
}
//动态规划
public static int coinsWay2(int[] arr, int aim)
{
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
if (aim == 0) {
return 1;
}
int N = arr.length;
int[][] dp = new int[N + 1][aim + 1];
dp[N][0] = 1;
for (int index = N - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
dp[index][rest] = dp[index + 1][rest] + (rest - arr[index] >= 0 ? dp[index + 1][rest - arr[index]] : 0);
}
}
return dp[0][aim];
}
// 为了测试
public static int[] randomArray(int maxLen, int maxValue) {
int N = (int) (Math.random() * maxLen);
int[] arr = new int[N];
for (int i = 0; i < N; i++) {
arr[i] = (int) (Math.random() * maxValue) + 1;
}
return arr;
}
// 为了测试
public static void printArray(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
// 为了测试
public static void main(String[] args) {
int maxLen = 20;
int maxValue = 30;
int testTime = 1000000;
System.out.println("测试开始");
int aim = (int) (Math.random() * maxValue);
int[] a = randomArray(maxLen, maxValue);
int a1 = coinsWay(a, aim);
int a2 = coinsWay2(a, aim);
System.out.println("a1:" + a1 + ", a2:" + a2);
System.out.println("测试结束");
}
}