动态规划及背包问题

一、动态规划理论

1.1什么是动态规划

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

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

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

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

1.2动态规划解题步骤

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

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

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

  2. 确定递推公式

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

  4. 确定遍历顺序

  5. 举例推导 dp 数组

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

二、实战

746. 使用最小花费爬楼梯

1.到达i位置最少花费dpi

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

2.dpi=Math.min(dpi-1+costi-1,dpi-2+costi-2)

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

3.dp0=0;dp1=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.dpij:到达(i,j)所有的路径

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

2.dpij=dpi-1j+dpij-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.dpij:物品 0-i 任取,放到容量 j 的背包里

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

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

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

4.从前往后遍历

四、完全背包

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

1.dpj:容量为 j 时的最大价值

2.dp0 = 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.按物品从前往后遍历

相关推荐
葡萄成熟时 !12 小时前
JAVA 常用API学习笔记
java·笔记·学习
鹿角片ljp12 小时前
LeetCode 46. 全排列|吃透回溯
算法·leetcode·职场和发展
鼎艺创新科技12 小时前
不依赖 UE/Unity:我们如何从零搭建一套国产三维 GIS 渲染引擎
人工智能·算法·unity·游戏引擎·三维电子沙盘
Rain的Java大神之路13 小时前
介绍一下分布式事务
java·分布式·后端·spring·spring cloud·架构·springcloud
石头猫灯13 小时前
WordPress wp2shell 漏洞链完整拆解流程
网络·数据结构·安全·web安全
吴声子夜歌13 小时前
Java面试题——基础(一)
java·开发语言
南城以南溫暖如初14714 小时前
外卖CPS实战指南:从技术选型到运营落地全流程解析
java·spring boot·mysql·架构·vue
傻啦嘿哟14 小时前
英雄联盟皮肤爬虫:爬取全皮肤价格与特效,做比价工具
java·c++·爬虫
.道阻且长.15 小时前
11.LeetCode算法习题讲解--滑动窗口--将x减到0的最小操作数
算法·leetcode·职场和发展
码匠许师傅15 小时前
【C++ 面试真题】24. 聊聊 C++ 的时间日期处理
java·c++·面试