算法基础之递归与动态规划(二)

147 阅读1分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

5. 动态规划

在动态规划里面,递推式一般叫做状态转移方程

5.1. 动态规划例子

5.1.1. 凑零钱问题

leetcode-322. 零钱兑换 给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

5.1.1.1. 递归法

状态转移方程为:

f(v)={1,v<00,,v=0minf(vci)+1cicoins,n>0(5-1)f(v)=\left\{ \begin{aligned} -1,v&<0 \\ 0,,v&=0 \\ min{f(v-c_i)+1|c_i\in coins},n&>0 \end{aligned} \right. \tag{5-1}

上述递推式的含义为,要求组成价值为vv的最少硬币数,只需分别算出组成价值vciv-c_i的最少硬币数再+1,然后看减去哪种cic_i下是硬币数量最少的情况。这样将能减少问题规模,并且在小规模的问题上可以复用上述策略

class Solution {
    public int coinChange(int[] coins, int amount) {
        return dp(coins,amount);
    }
    private int dp(int[] coins, int amount){
        if(amount==0) return 0;
        if(amount<0) return -1;
        int result=Integer.MAX_VALUE;
        for(int coin : coins){
            int subResult=dp(coins, amount-coin);
            if(subResult<0) continue;
            result=Math.min(result,1+subResult);
        }
        return result!=Integer.MAX_VALUE?result:-1;
    }
}

上面的暴力递归法很容易超出时间限制,这是因为存在大量重复计算,如下图所示: 在这里插入图片描述

图5.1 零钱兑换递归示意图

5.1.1.2. 带备忘录的递归(从后往前的动态规划)

  1. 用一个HashMap memo存下中间结果,key是amount,value是result
public class Solution1 {
    Map<Integer, Integer> memo = new HashMap<Integer, Integer>();

    public int coinChange(int[] coins, int amount) {
        return dp(coins, amount);
    }

    private int dp(int[] coins, int amount) {
        if (amount == 0) {
            return 0;
        }
        if (amount < 0) {
            return -1;
        }
        if (memo.containsKey(amount)) {
            return memo.get(amount);
        }
        int result = Integer.MAX_VALUE;
        for (int coin : coins) {
            int subResult = dp(coins, amount - coin);
            if (subResult < 0) {
                continue;
            }
            result = Math.min(result, 1 + subResult);
        }
        if (result == Integer.MAX_VALUE) {
            memo.put(amount, -1);
        } else {
            memo.put(amount, result);
        }
        return memo.get(amount);
    }
}
  1. 用一个数组存储中间结果,index是amount,value是amount
public class Solution2 {
    int[] memo;

    public int coinChange(int[] coins, int amount) {
        memo = new int[amount + 1];// 默认初始化0
        return dp(coins, amount);
    }

    private int dp(int[] coins, int amount) {
        if (amount == 0) {
            return 0;
        }
        if (amount < 0) {
            return -1;
        }
        if (memo[amount] != 0) {
            return memo[amount];
        }

        int result = Integer.MAX_VALUE;
        for (int coin : coins) {
            int subResult = dp(coins, amount - coin);
            if (subResult < 0) {
                continue;
            }
            result = Math.min(result, 1 + subResult);
        }
        if (result == Integer.MAX_VALUE) {
            memo[amount] = -1;
        } else {
            memo[amount] = result;
        }
        return memo[amount];
    }
}

5.1.1.3 从前往后的动态规划

前面的动态规划解法本质上还是递归,时间消耗和空间消耗都比从前往后的动态规划要大。这里我们采用从前往后的动态规划,注意每次递推的时候,要判断前面的项下标是否大于0,以及值是否大于等于0。同时在每次遍历完coins后要将对memo[i]进行判断,如果仍为0则应该置为-1

class Solution3 {
    public int coinChange(int[] coins, int amount) {
        int[] memo = new int[amount + 1];// 默认初始化0
        memo[0] = 0;
        for (int i = 1; i <= amount; i++) {
            for (int coin : coins) {
                if (i - coin >= 0 && memo[i - coin] >= 0) {
                    if (memo[i] > 0) {
                        memo[i] = Math.min(memo[i], memo[i - coin] + 1);
                    } else {
                        memo[i] = memo[i - coin] + 1;
                    }
                }
            }
            if (memo[i] == 0) {
                memo[i] = -1;
            }
        }
        return memo[amount];
    }
}

当然,官方给的解答更巧妙一些,提前将memo初始化为amount,这样就可以减少一些操作。

public class Solution {
public int coinChange(int[] coins, int amount) {
        int[] memo = new int[amount + 1];// 默认初始化0
        Arrays.fill(memo, amount + 1);
        memo[0] = 0;
        for (int i = 1; i <= amount; i++) {
            for (int coin : coins) {
                if (i - coin >= 0) {
                    memo[i] = Math.min(memo[i], memo[i - coin] + 1);
                }
            }
        }
        return memo[amount] > amount ? -1 : memo[amount];
    }
}