Amazon零钱找零问题|第一轮
最近我有机会面试Amazon,这是我被问到的第一个问题。我将使用递归、记忆化和动态规划来详细说明程序逻辑。
我还将尝试介绍如何处理动态规划问题,同时解释问题背后的逻辑。
老实说,如果不首先使用递归或迭代方法编写程序,使用动态规划来编写程序是很困难的。是的,通过实践,人们可以直接在动态规划上编写程序,因为那里的大多数问题都是相关的。
编写动态程序的步骤如下:
使用递归/迭代方法编写逻辑
使用递归与记忆化
修改程序以使用动态规划
零钱找零问题 给定一个表示不同面额硬币的整数数组和一个表示金额的整数。
我们需要找出选择这些硬币以凑成给定金额的方式数量。
这是问题的基本思想。现在有两种变体的问题:
有无限多个硬币供应(我们将详细讨论此方法)
有有限数量的硬币供应(我在本文末尾提供了使用递归的代码)
有无限多个硬币供应: 示例1:
输入:coins = [1,2,5],amount = 4
输出:3
解释:2+2,1+1+1+1+1,2+1+1
示例2:
输入:coins = [1,2,3],amount = 4
输出:4
解释:1+1+1+1,2+2,2+1+1,3+1
类似的问题:
这个问题可以以很多种方式重新表述。例如,如果你要涵盖100个单位,你只能每次移动1个单位、2个单位和5个单位,那么问题是,有多少种方式可以涵盖这100个单位。
实施说明:
输入:coins = [1,2,3],amount = 4
输出:4
我们需要找出选取硬币以凑成总额4的方式数量。
编写递归程序的步骤:
找出需要的方法参数
找出输出/返回类型
找出基本条件
编写核心逻辑
找到解决方案的核心逻辑:
为了凑成总额4,我们可以包含数组中的各个元素,也可以排除它。假设数组{1, 2, 3}中的最后一个元素为3,它可以是解决方案的一部分,也可以不是解决方案的一部分。
因此,我们有两种方法——要么包括它,要么不包括在解决方案中。
coinchangesolution(array, N, W) =
coinchangesolution(array, N, W - array[N-1])
+
coinchangesolution(array, N — 1, W )
array是提供的硬币面额
N是数组大小
W是金额
上述逻辑的解释 -
coinchangesolution(array, N, W-array[N-1])
当我们包含元素时,我们必须减少金额,因为硬币已包括在内。
因此,第三个参数是W-array[N-1]。N-1是因为数组的索引从0开始。我们将N作为第二个参数传递,因为它可以再次包含在解决方案中(例如,1+1+1+1)。
1被包含了4次,以形成解决方案。
coinchangesolution(array, N - 1, W )
当我们不包含元素时,只需减少数组的值,索引i将减少1,而金额W将保持不变。
输出类型是一个整数,它将是选择硬币以组成金额的方式数量。
基本条件:
当amount == 0时,返回1(因为元素可以包含在内)
当N==0时,返回0(因为未满足上述条件,我们已经遍历了整个数组)
当amount<0时,返回0(因为我们有解决方案,W已经变为负数)
例如,如果我们将2+2+2作为解决方案,那么W=4-2-2-2=-2。所以当W为负数时,所选的解决方案或硬币面额不能是正确的解决方案。
程序示例:
public class CoinChange {
public static void main(String args[]) {
int[] arr = {1, 2, 3,4};
int W = 4;
int number_of_ways = coinchangesolution(arr, arr.length, W);
System.out.println("Number of ways : " + number_of_ways);
}
static int coinchangesolution(int arr[], int n, int W) {
if (W == 0) {
return 1;
}
if (n == 0) {
return 0;
}
if (W < 0) {
return 0;
}
return coinchangesolution(arr, n - 1, W)
+
coinchangesolution(arr, n, W - arr[n - 1]);
}
}
记忆化
记忆化是一种方法,用于存储先前函数调用的结果,以加快未来计算的速度。
如果使用相同的参数进行了重复的函数调用,我们可以存储先前的值,而不是重复不必要的计算。这种方法有助于减少程序的时间复杂度。
在我们使用递归解决问题时,最坏情况下的时间复
杂度更高,而且一些逻辑会一次又一次地计算。
示例:
arr = [1,2,3]
amount = 4
1 + 1 + 1 + 1 = 4
{1 + 1 } + 2= 4
在这里,我们再次计算1 + 1(在括号中),如果1 + 1被存储在某个地方会怎样。
我们将创建一个静态数组,它将存储已经计算过的值,以便可以再次使用。
int store[][] = new int[N+1][W+1]
程序示例:
import java.util.Arrays;
public class CoinChangeMemoisation {
static int store[][];
public static void main(String args[]) {
int[] arr = {1, 2, 3};
int W = 4;
store = new int[arr.length + 1][W + 1];
for (int i = 0, len = store.length; i < len; i++)
Arrays.fill(store[i],-1);
int number_of_ways = coinchangesolution(arr, arr.length, W);
System.out.println("Number of ways : " + number_of_ways);
}
static int coinchangesolution(int arr[], int n, int W) {
if (W == 0) {
return 1;
}
if (n == 0) {
return 0;
}
if (W < 0) {
return 0;
}
if(store[n][W] != -1) {
return store[n][W];
}
return store[n][W] =
coinchangesolution(arr, n - 1, W)
+
coinchangesolution(arr, n, W - arr[n - 1]);
}
}
动态规划 我们将创建一个静态数组,它将存储已经计算过的值,以便可以再次使用。
int store[][] = new int[N+1][W+1]
我们将使用两个for循环来迭代N和W 索引i将是N 索引j将是W
store[i][j]将存储选择硬币以组成金额的方式数量
当i==0时,表示N==0,store[i][j]=0; 表示数组为空,所以解决方案的数量为0,因为没有硬币可以选择以组成金额
当j==0时,表示W==0,store[i][j]=1; 表示当金额=0时,总是存在1个解决方案,因为无法选择任何元素来组成金额,即值为空的数组
程序示例:
package com.mycompany.app.CoinChangeProblem;
public class CoinChangeDP {
public static void main(String args[]) {
int[] arr = {1, 2, 3};
int W = 4;
int number_of_ways = coinchangesolution(arr, arr.length, W);
System.out.println("Number of ways : " + number_of_ways);
}
static int coinchangesolution(int arr[], int N, int W) {
int store[][] = new int[N + 1][W + 1];
//i means N which is size of array
//j means W which is limit given in input
for (int i = 0; i < N + 1; i++) {
for (int j = 0; j < W + 1; j++) {
if (i == 0) {
store[i][j] = 0;
continue;
}
if (j == 0) {
store[i][j] = 1;
continue;
}
if (arr[i - 1] <= j) {
store[i][j] = store[i][j - arr[i - 1]]
+
store[i-1][j];
} else {
store[i][j] = store[i - 1][j];
}
}
}
return store[N][W];
}
}
上述逻辑的解释:非常简单。 我们有一个二维数组。行(i)表示N,列(j)表示W。我们需要两个for循环来遍历它们。基本条件与递归中使用的相同。如果你还不清楚,请再次阅读上面的解释。
现在我们将讨论这一部分:
if (arr[i - 1] <= j) {
store[i][j] = store[i][j - arr[i - 1]]
+
store[i-1][j];
} else {
store[i][j] = store[i - 1][j];
}
i表示N,j表示W。 如果arr[i-1] <= j;表示硬币在i-1处的面额小于金额即W;
如果是的话,它可以成为解决方案的一部分。
如果不是,那么它不能成为解决方案的一部分。
store[i][j] = store[i][j - arr[i - 1]]
+
store[i-1][j];
如果它可以成为解决方案的一部分,那么它可以包含在解决方案中,也可以不包含在解决方案中。
如果包含它,我们将从金额(W)中减去面额,然后可以再次包含它。所以这是 -
store[i][j - arr[i - 1]]
如果不包括它,那么我们只需减少数组的索引,即i,金额(W)将保持不变,即j将保持不变。所以这是 -
store[i-1][j]; 当分母大于或等于金额(W)时,执行else块。在这种情况下,金额不会发生变化,只有索引
i会发生变化,因为我们没有包含这个值,所以我们将索引减1。
else {
store[i][j] = store[i - 1][j];
}
有限硬币供应(递归): 在这种情况下,核心逻辑的主要更改将是 - 不再使用相同的硬币,我们将减少N的值。因为在这种情况下,我们只有有限数量的硬币,不能选择相同的硬币作为解决方案的一部分。
示例:
int[] arr= {1, 2, 3};
int amount= 4;
对于无限供应,{1+1+1+1}是一个解决方案,所以我们将相同的硬币传递给递归方法,而不会减少N的值。
coinchangesolution(arr, n, W - arr[n - 1])
对于有限供应,一旦我们选择{1 + ……}作为解决方案,由于硬币有限,我们无法再次选择1,因此我们将在递归调用的第二个参数中进行N -1操作。 (在代码中以粗体和斜体标记)
coinchangesolution(arr, n-1, W - arr[n - 1])
程序示例:
public class CoinChangeFC {
public static void main(String args[]) {
int[] arr = {1, 2, 3};
int W = 4;
int number_of_ways = coinchangesolution(arr, arr.length, W);
System.out.println("Number of ways : " + number_of_ways);
}
static int coinchangesolution(int arr[], int n, int W ) {
if(W == 0) {
return 1;
}
if(n == 0) {
return 0;
}
if(arr[n - 1] <= W) {
return coinchangesolution(arr, n-1, W - arr[n - 1])
+ coinchangesolution(arr, n - 1, W);
} else {
return coinchangesolution(arr, n-1, W);
}
}
}
DP:
public class CoinChangeDP {
public static int coinChange(int[] coins, int amount) {
int length = coins.length;
int dp[][] = new int[length+1][amount+1];
for(int i=0;i<=length;i++) {
for(int j=0;j<=amount;j++) {
if(j==0){
dp[i][j] = 1;
}
else if(i==0) {
dp[i][j] = 0;
}
else if(coins[i-1]<=j) {
dp[i][j] = dp[i-1][j-coins[i-1]]+ dp[i-1][j];
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[length][amount];
}
public static void main(String args[]) {
System.out.println(coinChange(new int[]{1,2,3,4},6)); // 2
}
}
结论:
动态规划的概念一开始可能会有点棘手。然而,一旦你能够写出递归代码,然后使用动态规划范式编写相同的逻辑就会变得容易得多。继续学习和练习。随着时间的推移,这将变得更容易。
"它会变得更容易" - 《纸牌屋》(第2季结局)