动态规划及背包问题

一、动态规划理论

1.1什么是动态规划

动态规划(Dynamic Programming,简称 DP)主要用来解决"有很多重复子问题"的情况,它的核心就是通过已经算出来的结果,去推导新的结果。也就是说,动态规划中的每一个状态,都是从之前的状态一步一步推出来的,这一点是它和贪心算法最大的区别。贪心是每一步直接选当前最优,不关心之前的状态,而动态规划必须依赖之前的结果。

比如经典的 0-1 背包问题:有 N 件物品和一个容量为 W 的背包,每个物品有重量 weight[i] 和价值 value[i],每个物品只能选一次,求最大总价值。用动态规划时,我们定义 dp[j] 表示容量为 j 时的最大价值,那么 dp[j] 是由 dp[j - weight[i]] 推导出来的,在"选当前物品"和"不选当前物品"之间取最大值,也就是 max(dp[j], dp[j - weight[i]] + value[i])。可以看出,每一步都依赖之前的状态,这就是"推导"的过程。

如果用贪心来做,就会变成每次选价值最大或者性价比最高的物品,这种做法并没有考虑之前已经选了什么,因此很容易出错,也说明贪心并不能解决所有动态规划问题。

其实在刷题时,不用死记那些像"最优子结构""重叠子问题"这种抽象概念,记住一句话就够了:动态规划是推出来的,贪心是选出来的。理解这一点,再多做几道题,自然就能体会两者的区别。至于背包问题,后面可以再单独深入讲解。

1.2动态规划解题步骤

做动态规划题的时候,很多人容易掉进一个误区:以为把状态转移公式背下来,照葫芦画瓢就能写代码,甚至题目 AC 了,也不太清楚 dp[i] 表示的是什么。这种"朦胧状态"会让你遇到稍微复杂一点的题就不会做了,然后看题解,再照搬公式,如此循环。

其实,状态转移公式很重要,但动态规划不仅仅是公式而已。真正掌握 DP,需要搞清楚五个步骤:

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

  2. 确定递推公式

  3. 明确 dp 数组如何初始化

  4. 确定遍历顺序

  5. 举例推导 dp 数组

有人可能会问,为什么先确定递推公式再考虑初始化?因为有些情况下,公式本身决定了 dp 数组的初始化方式。很多刷过 DP 题的同学知道公式重要,但只记公式而不理解 dp 数组的含义、初始化和遍历顺序,结果写的程序怎么改都通过不了。

二、实战

746. 使用最小花费爬楼梯

1.到达i位置最少花费dp[i]

我们要找出到达某一层时的最小花费,到达这一层有两个途径,前一层爬一个台阶和前两层爬两个台阶。

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

我们可以选择从0和1的台阶开始往上爬,因此0和1不需要花费

3.dp[0]=0;dp[1]=0;

4.从前往后遍历

复制代码
class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int n=cost.length;
        int[] dp=new int[n+1];
        dp[0]=0;
        dp[1]=0;
        for(int i=2;i<=cost.length;i++){
            dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[n];
    }
}

63. 不同路径 II

1.dp[i][j]:到达(i,j)所有的路径

因为只能往右往下走,因此一个位置它由上方和左方到达,他的路径就是上方和左方的路径种数和。

2.dp[i][j]=dp[i-1][j]+dp[i][j-1]

3.因为一个位置它由上方和左方到达,因此最上面和最左面要初始化,又因为到达最上面和最左面只有一条路径,所以全部初始化为1。

4.从前往后遍历

复制代码
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int r=obstacleGrid.length;
        int c=obstacleGrid[0].length;
        int[][] dp=new int[r][c];
        for(int i=0;i<r;i++){
            if(obstacleGrid[i][0] == 1) break;
            dp[i][0]=1;
        }
        for(int j=0;j<c;j++){
            if(obstacleGrid[0][j] == 1) break;
            dp[0][j]=1;
        }
        for(int i=1;i<r;i++){
            for(int j=1;j<c;j++){
               if(obstacleGrid[i][j] == 1){
                    dp[i][j] = 0; 
                } else {
                    dp[i][j] = dp[i-1][j] + dp[i][j-1];
                }
            }
        }
        return dp[r-1][c-1];
    }
}

因为要考虑障碍,因此遇到障碍时最上方和最右方就走不通,不在最上方或者最右方的话,障碍的地方也到不了,dp数组为0

三、01背包

这道题的暴力解法是回溯,当然我们也可以使用动态规划。

1.dp[i][j]:物品 0-i 任取,放到容量 j 的背包里

2.若不放物品 i ,则他的最大价值是 dp[i-1][j];

若放物品 i,则它的最大价值是dp[i-1][j-weight[i]]+value[i],大家可能看不懂,我们可以这样理解:已经决定要放i了,所以我们需要把相应的重量腾出来,看在剩下的重量下,前面的最大价值,最后再加上 i 的价值。

3.列是物品,所以在容量为0时,所有物品都放不下去,因此竖着全部初始化为0,最上面横着,只有在容量大于物品0时,才初始化为物品0的价值,前面都是0。如图:

4.从前往后遍历

四、完全背包

完全背包和0-1背包有什么区别呢?区别就是完全背包中,每个物品可以使用无数次。

1.dp[j]:容量为 j 时的最大价值

2.dp[0] = 0

3.对于每一个物品 i,再从 j = weight[i] 开始到背包容量 bagWeight 正序遍历。对于每一个容量 j,都有两种选择:一是不放当前物品 i,此时价值就是原来的 dp[j];二是放入当前物品 i,由于完全背包可以重复选,所以放入之后剩余容量 j - weight[i] 仍然可以继续放当前物品,因此价值为 dp[j - weight[i]] + value[i]。两者取最大值,即 dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

4.按物品从前往后遍历

相关推荐
busideyang2 小时前
函数指针类型定义笔记
c语言·笔记·stm32·单片机·算法·嵌入式
Wect2 小时前
LeetCode 215. 数组中的第K个最大元素:大根堆解法详解
前端·算法·typescript
侠客行03172 小时前
Tomcat 从陌生到熟悉
java·tomcat·源码阅读
深邃-2 小时前
数据结构-双向链表
c语言·开发语言·数据结构·c++·算法·链表·html5
2401_878530212 小时前
分布式任务调度系统
开发语言·c++·算法
愤豆2 小时前
06-Java语言核心-JVM原理-JVM内存区域详解
java·开发语言·jvm
_深海凉_2 小时前
LeetCode热题100-两数之和
算法·leetcode·职场和发展
nunca_te_rindas2 小时前
算法刷体小结汇总(C/C++)20260328
c语言·c++·算法
Sunshine for you3 小时前
高性能压缩库实现
开发语言·c++·算法