一文带你入门动态规划

动态规划

写在前面

没思路的时候就把树画出来,这会事半功倍

概述

我们首先明确一点,动态规划问题的一般形式就是求最大值或者最小值。
其核心就是穷举。因为求最值肯定要将其全部的可能都列出来,这才找的出最值。
动态规划适合的穷举具有重叠子问题的特征,如果暴力穷举,效率回极其低下,所以需要备忘录或则DB table来优化穷举过程,避免不必要的计算。
动态规划问题一定具备最优子结构性质,这样才可以通过子问题得到原问题的解。
动态规划问题的核心是就是穷举出最值,但是问题可以千变万化,穷举出所有可行解并不是 容易的事情,只有列出正确的动态转移方程,才可以正确的穷举。写出动态转移方程也是最难的。
**

写出动态转移方程的核心要义

步骤

1.这个问题最简单的情况(basecase)是什么
2.这个问题有什么状态
3.每个状态可以做什么,可以做出什么选择使得状态发送变化
4.如何定义dp数组/函数的含义来表现“状态”和选择

基本框架

#初始化basecase
db[][]..=base case
#进行状态转移
for 状态1 in 状态2的所有取值
    for 状态1 in 状态2的所有取值

斐波那契数入门动态规划

Leetcode链接509 斐波那契数https://leetcode-cn.com/problems/fibonacci-number/

题目描述

斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给你 n ,请计算 F(n) 。
 
示例 1:
输入:2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1


示例 2:
输入:3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2


示例 3:
输入:4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3

1.解法1,暴力递归穷举

代码

public int fib(int n) {
    if (n==0){
        return 0;
    }
    if (n==1||n==2){
        return 1;
    }
    return fib(n-1)+fib(n-2);
}

注意:但凡遇到递归的问题都应该画出递归树,这对分析算法的复杂度,寻找算法低效性的原因都有巨大的帮助

递归树图解

从递归树中我们可以看到这存在大量的重复的运算,这是没意义的运算而且十分耗时。

要计算fib(5)就必须计算fib(4)和fib(3)
要计算fib(4)就必须计算fib(3)和fib(2)

如果fib(4)中已经计算过fib(3)那么fib(5)中就不必重复计算fib(3)了,这时候就需要引入DP table 或则备忘录,通过查表的方式来判断该值有没有计算过,有没有重复计算

在这里插入图片描述

时间复杂度分析

二叉树的节点个数为指数级别,所求子问题的个数为O(2^n)
解决一个子问题的时间为O(1),因为值涉及到一个加法运算
故时间复杂度为 O(2^n)

消耗的内存与时间情况

在这里插入图片描述

2.解法二,备忘录解法

在解法1中我们也介绍了暴力解法中存在的问题,及其问题存在的原因,那么在解法二中我们就通过加上备忘录的方式,来避免重复计算,这样可以大大提高解题的效率

代码

class Solution {
    int[] DpTable;
       public  int fib(int n){
        DpTable=new int[n+1];
        return fib2(n);
    }

    public  int fib2(int n) {
        /*结束递归的条件*/
      if (n==0){
          return 0;
      }
      if (n==1||n==2){
          return 1;
      }
      if (DpTable[n]!=0){
          return DpTable[n];
      }
      DpTable[n]=fib2(n-1)+fib2(n-2);
      return DpTable[n];
    }
}

消耗的内存与时间情况

在这里插入图片描述

3.解法3 dp数组的迭代解法

我们可以把备忘录独立出来成为一张表,就叫做DB table 在这张表上自底向上推算

代码

class Solution {
   public  int fib(int n) {
        if (n==0){
            return 0;
        }
        if (n==1||n==2){
            return 1;
        }
        int[] arr = new int[n+1];
        arr[0]=0;
        arr[1]=1;
        arr[2]=1;
        for (int i = 3; i <=n; i++) {
            arr[i]=arr[i-1]+arr[i-2];
        }
        return arr[n];
    }
}

消耗的内存与时间情况

在这里插入图片描述

小发现

可以发现时间和空间往往二者不能兼得,要想减少时间就必须花费一定的空间开销来建立备忘录来减少时间开销

凑零钱问题进阶动态规划

题目描述

Leetcode链接 322 零钱兑换https://leetcode-cn.com/problems/coin-change/
**

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。

示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1


示例 2:
输入:coins = [2], amount = 3
输出:-1



示例 3:
输入:coins = [1], amount = 0
输出:0

示例 4:
输入:coins = [1], amount = 1
输出:1

示例 5:
输入:coins = [1], amount = 2
输出:2

1.暴力递归解法

代码

class Solution {
    int res = Integer.MAX_VALUE;
    public int coinChange(int[] coins, int amount) {
        if(coins.length == 0){
            return -1;
        }
        findWay(coins,amount,0);
        // 如果没有任何一种硬币组合能组成总金额,返回 -1。
        if(res == Integer.MAX_VALUE){
            return -1;
        }
        return res;
    }

    public void findWay(int[] coins,int amount,int count){
        if(amount < 0){
            return;
        }
        if(amount == 0){
            res = Math.min(res,count);
        }

        for(int i = 0;i < coins.length;i++){
            findWay(coins,amount-coins[i],count+1);
        }
    }
}

消耗的内存与时间情况

超时,超时了,说明时间复杂度过高,需要通过加入备忘录的反式来减少时间复杂度,一空间换时间

![image.png](https://img-blog.csdnimg.cn/img_convert/b1da5a6646a60c146bdd16e237332c69.png#align=left&display=inline&height=126&margin=[object Object]&name=image.png&originHeight=252&originWidth=847&size=11567&status=done&style=none&width=423.5)

2.添加了备忘录的解法

代码

package com.pjh;
import com.sun.xml.internal.ws.api.model.MEP;
public class Leetcode322Solution11 {
    int[] memory;
    public int coinChange(int[] coins, int amount) {
        /*coins硬币的数组为空返回-1*/
        if (coins.length==0){
            return -1;
        }
        memory=new int[amount+1];
        return findMin(coins,amount);
    }
    /*coins为存储硬币的数组,amount为当前还剩的钱的数量,account为所用硬币的数量*/
    public int findMin(int[] coins,int amount){
        /*结束递归的条件*/
        if (amount==0){
           return 0;
        }
        if (amount<0){
          return -1;
        }
        /*判断备忘录中有没有该值,有该值则直接返回*/
        if (memory[amount]!=0){
            return memory[amount];
        }
        int min1=Integer.MAX_VALUE;
        for (int coin : coins) {
            /*减去该硬币的值进行下一次递归*/
            int temp= findMin(coins,amount-coin);
            if (temp>=0&&temp+1<min1){
                // 加1,是为了加上得到res结果的那个步骤中,兑换的一个硬币
                min1=temp+1;
            }
        }
        /*备忘录记录*/
        memory[amount]=min1;
        /*返回值*/
        return memory[amount]==Integer.MAX_VALUE?-1:memory[amount];
    }
}

消耗的内存与时间情况

在这里插入图片描述

3.按照四个步骤列出动态转移方程

步骤

1.这个问题最简单的情况(basecase)是什么
2.这个问题有什么状态
3.每个状态可以做什么,可以做出什么选择使得状态发送变化
4.如何定义dp数组/函数的含义来表现“状态”和选择

分析

1.最基本条件即 钱的金额为0的时候所需硬币数的0
2.状态就是钱的总金额,随着决策树一层一层决策,金额不断减少
3.发生状态变化的条件,每选择一枚硬币就减少一定的金额
4.dp数组的定义,定义数组存储金额

状态转移方程如下

db(n)=
  0,n==0
  -1,n<0
   min{dp(n-coins)+1} , n>0

4.dp数组的迭代解法

代码

class Solution {
   public int coinChange(int[] coins, int amount) {
        if(coins.length == 0){
            return -1;
        }
        int[] memory = new int[amount + 1];
        /*初始化数组,数组值设置为比传入值大1即可*/
        Arrays.fill(memory,amount+1);
        /*初始化basecase*/
        memory[0]=0;
        /*遍历ammount*/
        for (int i = 0; i <= amount; i++) {
            /*遍历coins,状态遍历的种类*/
            for (int coin : coins) {
                /*发生状态变化的条件*/
                if (i-coin<0) continue;
                /*比较当前值与memory的值谁大*/
                memory[i]=Math.min(memory[i], memory[i-coin]+1);
            }
        }
        return memory[amount]==amount+1?-1: memory[amount];
    }
}


消耗的内存与时间情况

在这里插入图片描述

posted @ 2021-04-01 11:46  一只胡说八道的猴子  阅读(257)  评论(0编辑  收藏  举报