【算法01】—动态规划

目录

一、动态规划(Dynamic Programming,DP)

1.1 【编程题】斐波那契数列

1.2【编程题】青蛙跳台阶扩展问题

1.3【编程题】最小花费爬楼梯

1.4【编程题】不同路径(机器人走方格)

1.5【编程题】不同路径机器人走方格(有障碍)

1.6【编程题】走方格的方案数(同上)

1.7【编程题】拆分词句

1.8【编程题】三角形

二、背包理论

2.1  二维dp数组01背包

2.2  一维dp数组 01背包


一、动态规划(Dynamic Programming,DP)

动态规划是分治思想的延伸:将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果。

特点:
1. 把原来的问题分解成了几个相似的子问题。
2. 所有的子问题都只需要解决一次。
3. 储存子问题的解。

本质:对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系)。

考虑角度:状态定义(要求:定义的状态要形成递推关系)、状态间的转移方程定义、 状态的初始化、返回结果

适用场景:最大值/最小值, 可不可行, 是不是,方案个数

对于动态规划问题,拆解为如下五步骤:

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组(打印数组)

1.1 【编程题】斐波那契数列

斐波那契数列—牛客

题目:斐波那契数列,现在要求输入一个正整数 n ,输出斐波那契数列的第 n 项。

斐波那契数列(黄金分割数列):1、1、2、3、5、8、13、21、34、…,以递推的方法定义:

F(0)=0,F(1)=1,  F(n)=F(n-1)+F(n-2)(n ≥2,n ∈ N*)

输入描述:

一个正整数n

输出描述:

输出一个正整数

注意·!!!斐波那契数组,要单独考虑n=0和n=1的情况

1.递归方法:时间复杂度O(2^n),输入较大时,可能栈溢出,递归过程中有大量的重复计算

public class Solution {
    public int Fibonacci(int n) {
        if(n <= 1) return n;
        int f = Fibonacci(n-1)+Fibonacci(n-2);  //如果n>2 则输出f
        return f;
    }
}

2.动态规划:    空间复杂度为O(1)、空间复杂度为O(n)

状态:F(n)
状态递推:F(n)=F(n-1)+F(n-2)
初始值:F(1)=F(2)=1
返回结果:F(N)

public class Solution {
    public int Fibonacci(int n) {   //空间复杂度为O(1)
        if(n <= 0)  return 0;
        if(n == 1 || n == 2) return 1;
        int ret = 0;
        int fn1 = 1, fn2 = 1;
        for(int i = 3; i <= n; i++) {
            ret = fn1 + fn2;
            fn1 = fn2;
            fn2 = ret;
        }
        return ret;
    }
}

//方法二:  //空间复杂度为O(n)
    public int Fibonacci(int n) {   
        int[] dp= new int[n+1];  //创建一个数组保存中间状态的解
        dp[0] = 0;
        dp[1] = 1;
        for(int i = 2; i < n+1; i++) {
        dp[i] = dp[i-1] + dp[i-2];
        }
        return dp[n];
    }

1.2【编程题】青蛙跳台阶扩展问题

牛客—青蛙跳台阶

题目1:青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶(n为正整数)总共有多少种跳法。

方法1:递归

1.逆向思维:若从第n个台阶进行下台阶,下一步有2中可能,一种走到第n-1个台阶,一种是走到第n-2个台阶,可得到如下关系:f[n] = f[n-1] + f[n-2]. (f[n] 表示在第n个台阶上的方法数)

2.初始条件:f[0] = f[1] = 1

3.看到此问题可以想到斐波那契数组,使用动态规划:可优化空间,优化掉递归的栈空间,动态规划直接从子树求得答案。过程是从下往上。

//递归方法
class Solution {
    public int jumping(int num) {
        if(num<=1) return n;
        return jumping(num-1)+jumping(num-2);
    }

//优化1:动态规划
    public int jumping(int num) {   
        int[] arr= new int[num+1];  //创建一个数组存放每一级台阶可以的方法数
        arr[0] = 0;
        arr[1] = 1;
        for(int i = 2; i < num+1; i++) {
            arr[i] = arr[i-1] + arr[i-2];
        }
        return arr[num];  // 返回num此时对应的方法数量
    }

//优化2:可以发现在这个过程中,计算当前台阶数只用到了前两个台阶的值,因此,只需定义三个变量即可
    public int jumping(int num) {   
        int a = 1;
        int b = 1;
        int sum = 0;
        for(int i=2,i <= num,i++) {
            sum = a + b;
            a = b;
            b = sum;
        }
        return sum;
    }
}

题目2:一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶(n为正整数)总共有多少种跳法。

解析:此问题是上面的延伸:可得到关系 :f(n)=f(n-1)+…f(1)---->f(n)=2*f(n-1)

public class Solution {
//方法一:排列
// 每个台阶看成一个位置,除过最后一个位置,其它位置都有两种可能性,
// 所以总的排列数为2^(n-1)*1 = 2^(n-1)
    public int jumpFloorII(int n) {
        int sum = 1;
        if (n == 1) return 1; //如果只有一个台阶,则只有一种方法
        for(int i = 1; i < n; i++) {   //从第二级台阶开始
            sum *= 2;
        }
        return sum;
    }
}

1.3【编程题】最小花费爬楼梯

746. 使用最小花费爬楼梯

给一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。计算并返回达到楼梯顶部的最低花费

输入:cost = [10,15,20]

输出:15

解释:从下标为 1 的台阶开始。 支付 15 ,向上爬两个台阶,到达楼梯顶部, 总花费 15。

输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。

解析:

!!!!!!!!每当你爬上一个阶梯,都要花费对应的体力值cost[i]

1.确定dp数组以及下标的含义:

dp[i]的定义:到达第i个台阶所花费的最少体力为dp[i]。

2.确定递推公式:

可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]。

dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];

3. dp数组如何初始化

dp[0] =cost[0] ;  dp[1]  = cost[1];

4.确定遍历顺序

从前到后遍历cost数组

5.举例推导dp数组

// 方式一:第一步支付费用
class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int len = cost.length;  //求数组长度
        int[] dp = new int[len+1];  //动态规划初始化数组dp大小一般为n+1
        
        dp[0] = 0;
        dp[1] = 0;   // dp[i]为到达第i个台阶所需要支付的费用
//!!!!!!!!每当你爬上一个阶梯,都要花费对应的体力值cost[i]
        for(int i = 2;i < len+1; i++) {
            dp[i] = Math.min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]);  //支付费用后,有两种可选方式,爬一个台阶或者两个台阶,取每一步(局部)最小值,即可求得最后所有步的最小值
        }
        return dp[len];  //返回爬上最后一个台阶需要花费的费用

    }
}

// 方式二:第一步不支付费用
class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int len = cost.length;
        int[] dp = new int[len];
        dp[0] = cost[0];
        dp[1] = cost[1];
        for (int i = 2; i < len; i++) {
            dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
        }
        //最后一步,如果是由倒数第二步爬,则最后一步的体力花费可以不用算
        return Math.min(dp[len - 1], dp[len - 2]);
    }
}

1.4【编程题】不同路径(机器人走方格)

62. 不同路径

一个机器人位于 m *n 网格的左上角 (起始点标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(标记为 “Finish” )。问总共有多少条不同的路径?

解析:

1.根据分析画图可得到:当n或m为1时,ret = 1,只有一种方法 

2.m = 2,且 n = 2时,f(2,2) = 2;

2.递归公式f(m,n) = f(m-1,n) + f(m,n-1)

1.动态规划解法:

class Solution {
    public int uniquePaths(int m, int n) { 
        int[][] dp = new int[m][n];
        for(int i = 0; i < m; i++) {  
            dp[i][0] = 1;   //初始化,表示一条竖线,故只有一种方法·
        }
        for(int i = 0; i < n; i++) {   //初始化,表示一条横线
            dp[0][i] = 1;
        }
        for(int i = 1;i < m; i++) {
            for(int j = 1; j < n; j++) {
                dp[i][j] = dp[i-1][j] + dp[i][j-1]; //动态规划推导公式
            }
        }
        return dp[m-1][n-1];  //f(m,n) = f(m-1,n)+f(m,n-1)
    }
}

2.递归方法:

class Solution {
    public int uniquePaths(int m, int n) {
        if(m <= 0 || n <= 0) return 0 ;
        if(m == 1 || n == 1) return 1 ;
        if(m == 2 && n == 2) return 2 ;
        return uniquePaths(m-1,n) + uniquePaths(m,n-1);
    }
}

1.5【编程题】不同路径机器人走方格(有障碍)

63. 不同路径 II

机器人位于 m*n 网格的左上角 (起始点标记为 “Start” )每次只能向下或者向右移动一步。机器人到网格的右下角( “Finish”)。现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?网格中的障碍物和空位置分别用 1 和 0 来表示。

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

解析:

1.确定dp数组(dp table)以及下标的含义

dp[i][j] :表示从(0 ,0)出发,到(i, j) 有 dp[i][j] 条不同的路径。

2.确定递推公式

无障碍进行递推:dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。有了障碍,(i, j)如果就是障碍的话应该就保持初始状态(初始状态为0)

if (obstacleGrid[i][j] == 0) { // 当(i, j)没有障碍的时候,再推导dp[i][j]
    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}

3.初始化

若无障碍 dp[i][0] = dp[0][i] = 1,有障碍则为·0

 4.确定遍历顺序

从递归公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 中得出,一定是从左到右一层一层遍历,保证推导dp[i][j]时,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值。

5.举例推导dp数组

     

完整代码: 

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];

        if(obstacleGrid[m-1][n-1] == 1 || obstacleGrid[0][0] == 1) {
            return 0;     //起点和终点若是障碍物,则不同通行
        }

       for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
            dp[i][0] = 1;
        }
        for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {
            dp[0][j] = 1;
        }

        for(int i = 1; i < m; i++) {   //无障碍动规操作 ,有障碍为0
            for(int j = 1; j < n; j++) {
                dp[i][j] = (obstacleGrid[i][j] == 0) ? (dp[i-1][j] + dp[i][j-1]): 0;  //无障碍执行动规,有障碍0
            }
        }
        return dp[m-1][n-1];
    }
}

1.6【编程题】走方格的方案数(同上)

走方格

请计算n*m的棋盘格子(n为横向的格子数,m为竖向的格子数)从棋盘左上角出发沿着边缘线从左上角走到右下角,总共有多少种走法,要求不能走回头路,即:只能往右和往下走,不能往左和往上走。

注:沿棋盘格之间的边缘线行走

输入描述:

输入两个正整数n和m,用空格隔开。(1≤n,m≤8)

输出描述:

输出一行结果                如:输入:2  2---->6

 解析:总路径:n,m)=(n-1,m)+(n,m-1) ---->使用递归

2.当n==1 && m>= 1------>对应路径数n+m;

3.当m==1 && n>= 1------>对应路径数n+m

终止条件m,n = 1

4.当m,n都>1时,如下情况:每走一步有两种情况,因此用递归方法来实现

import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        while (in.hasNextInt()) {  //多组样例,因此需要循环读入
            int n = in.nextInt();
            int m = in.nextInt();
            System.out.println(func(n,m));
        }
    }

    public static int func(int n,int m) {  //处理输入的两个数据,递归函数
        if((n == 1 && m >=1) || (m == 1 && n >=1)) {
            return m+n;
        }
        return func(n-1,m)+func(n,m-1); 
    }
}

1.7【编程题】拆分词句

给定一个字符串s和一组单词dict,判断s是否可以用空格分割成一个单词序列,使得单词序列中所有的单词都是dict中的单词(序列可以包含一个或多个单词),如:
给定s=“nowcode”;dict=["now", "code"].
返回true,因为"nowcode"可以被分割成"now code".

import java.util.Set;

public class Solution {
    public boolean wordBreak(String s, Set<String> dict) {
        if(s == null || s.length() == 0) return false;

        boolean[] dp = new boolean[s.length()+1];    //给定一个状态数组,存放每个字符是否被分割的true,false值,判定其是否能够被分割,
        dp[0] = true;  //初始值
        for(int i = 1;i <= s.length();i++) { //遍历字符串,从1开始,因为下标0给了初始状态
             for (int j = i - 1; j >= 0; j--) {
                if (dp[j] && dict.contains(s.substring(j, i))) {  // 字典里有字符串的子串 
                    dp[i] = true;
                }
            }
        }
        return dp[s.length()];
    }
}

1.8【编程题】三角形

给出一个三角形,计算从三角形顶部到底部的最小路径和,每一步都可以移动到下面一行相邻的数字,如 : 给出的三角形如下:

[[20],[30,40],[60,50,70],[40,10,80,30]]

最小的从顶部到底部的路径和是20 + 30 + 50 + 10 = 110。

问题:从顶部到底部的最小路径和

状态F(i,j):(0,0)到(i,j)的最小路径和
状态递推:F(i,j) = min(i-1,j-1), F(i-1,j) + array[i , j]
初始值:F(1)=F(2)=1
返回结果:F(N)

解析:

1. 新增一个数组来存储当前层到下一层各个节点最短的路径值

2.用这个三角形数组每一层本身来存储到达当前层的最短路径,这样就不需要额外的存储空间。。

import java.util.*;
public class Solution {
    public int minimumTotal(ArrayList<ArrayList<Integer>> triangle) {
        if (triangle.size() == 0 || triangle == null) return 0;
        int n = triangle.size();    //记录三角形的层数 (外层数组)      
        int[] temp = new int[n];  //创建一个数组,存放到达每一层的最小步数,数组大小为层数    (内层数组)

        for (int i = 0; i < n; i++) 
        //  triangle.get(n-1)获取(n-1) 行的所有元素  ----->(n-1)行中i位置的元素
            temp[i] = triangle.get(n-1).get(i);  //获取最后一层 i位置 元素
        //继续由下向上运算
        for(int i = n-2; i >= 0; i--){  //i代表行数,j为每一行的元素
            for(int j = 0; j <= i; j++){
                temp[j] = triangle.get(i).get(j)+Math.min(temp[j],temp[j+1]);  //获取当前行i-1的最小值min + 上一层i节点
            }
        }
        return temp[0];
    }
}

二、背包理论

2.1  二维dp数组01背包

1. 确定dp数组以及下标的含义

dp[i][j] :[0,i ] 物品里任意,放进容量为 j 的背包里,价值总和的最大值。

 2.确定递推公式

  • 不放物品 i :最大价值为dp[ i - 1] [ j ],也即是物品 i 的容量 > 背包 j  的体积
  • 放物品 i :最大价值为dp[ i - 1] [ j  - weight[ i ] ]  +  value[ i ]  ---> 也即是  i-1 个物品的价值+第 i 个物品的价值  ( value[ i ] 为物品 i 的价值;weight[ i ]为物品 i 的容量)

递归公式: dp[ i ][ j ] = max(dp[i - 1][ j ], dp[i - 1][j - weight[ i ]] + value[ i ]);

3.dp数组如何初始化

1) 若背包容量 j = 0(dp[ i ][ 0 ]),背包价值总和 dp[ i ][ j ] = 0。

2) i 为 0,存放编号0的物品,各个容量的背包所能存放的最大价值 dp[ i ][ j ]。

当 j < weight [ 0 ] 时,dp[ 0 ][ j ]  = 0,因为背包容量 <  物品容量,(装不下,最大价值为0)

当j >= weight [ 0 ] 时,dp[ 0 ][ j ]  = value[ 0 ],背包容量只要大于物品容量即可存放。

先遍历 物品  还是 背包?都可以!!  先遍历物品更好理解。


二维dp数组实现01背包 完整代码:


    public static void main(String[] args) {
        int[] goodsWeight = {1, 3, 4};  // 物品容量
        int[] value = {15, 20, 30};  // 物品价值
        int bagSize = 4;  //背包最大体积
        testweightbagproblem(goodsWeight, value, bagSize);     //递归
    }

    public static void testweightbagproblem(int[] goodsWeight, int[] value, int bagSize){
        int goodsNum= goodsWeight.length;  //物品个数
        int value0 = 0;  //价值为0
        int[][] dp = new int[goodsNum+ 1][bagSize + 1];  //dp[i][j]包容量为j,前i个物品的最大价值
        for (int i = 0; i <= goodsNum; i++){  //背包容量为0,价值0(不能放物品)
            dp[i][0] = value0;
        }
        
        for (int i = 1; i <= goodsNum; i++){   //先遍历物品,再遍历背包容量
            for (int j = 1; j <= bagSize; j++){
                if (j < goodsWeight[i - 1]){   //背包容量 < 物品i,则i不能放入
                    dp[i][j] = dp[i - 1][j];
                }else{
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - goodsWeight[i - 1]] + value[i - 1]);
                }
            }
        }

        for (int i = 0; i <= goodsNum; i++){   //打印dp数组
            for (int j = 0; j <= bagSize; j++){
                System.out.print(dp[i][j] + " ");
            }
            System.out.print("\n");
        }
    }

2.2  一维dp数组 01背包

二维:dp[ i ][ j ] = max(dp[ i ][ j ], dp[ i ][ j - weight[i] ] + value[i]);

dp[ j ]:容量为j的背包,所背物品价值dp[ j ]

递推公式:

dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

初始化:非0下标都初始化为0

一维dp数组实现01背包 完整代码:

 public static void main(String[] args) {
        int[] goodsWeight = {1, 3, 4};
        int[] value = {15, 20, 30};
        int bagWight = 4;
        testWeightBagProblem(goodsWeight, value, bagWight);  //递归方法
    }

    public static void testWeightBagProblem(int[] goodsWeight, int[] value, int bagWeight){
        int doodsNum = goodsWeight.length;
       
        int[] dp = new int[bagWeight + 1]; // dp[j]背包容量为 j 的最大价值
        
        for (int i = 0; i < doodsNum; i++){   //先遍历物品,再遍历背包容量
            for (int j = bagWeight; j >= weight[i]; j--){
                dp[j] = Math.max(dp[j], dp[j - goodsWeight[i]] + value[i]);
            }
        }
        
        for (int j = 0; j <= bagWeight; j++){  //打印dp数组
            System.out.print(dp[j] + " ");
        }
    }

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值