Amazon零钱找零问题|第一轮

Amazon零钱找零问题|第一轮

最近我有机会面试Amazon,这是我被问到的第一个问题。我将使用递归、记忆化和动态规划来详细说明程序逻辑。

我还将尝试介绍如何处理动态规划问题,同时解释问题背后的逻辑。

老实说,如果不首先使用递归或迭代方法编写程序,使用动态规划来编写程序是很困难的。是的,通过实践,人们可以直接在动态规划上编写程序,因为那里的大多数问题都是相关的。

编写动态程序的步骤如下:

  1. 使用递归/迭代方法编写逻辑

  2. 使用递归与记忆化

  3. 修改程序以使用动态规划

零钱找零问题 给定一个表示不同面额硬币的整数数组和一个表示金额的整数。

我们需要找出选择这些硬币以凑成给定金额的方式数量。

这是问题的基本思想。现在有两种变体的问题:

  1. 有无限多个硬币供应(我们将详细讨论此方法)

  2. 有有限数量的硬币供应(我在本文末尾提供了使用递归的代码)

有无限多个硬币供应: 示例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的方式数量。

编写递归程序的步骤:

  1. 找出需要的方法参数

  2. 找出输出/返回类型

  3. 找出基本条件

  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季结局)

你可能感兴趣的:(面试,算法)