算法沉淀 —— 动态规划篇(路径问题)

简介: 算法沉淀 —— 动态规划篇(路径问题)

算法沉淀 —— 动态规划篇(路径问题)

前言

几乎所有的动态规划问题大致可分为以下5个步骤,后续所有问题分析都将基于此


  • 1.、状态表示:通常状态表示分为以下两种,其中更是第一种为主。
  • 以i为结尾,dp[i] 表示什么,通常为代求问题(具体依题目而定)
  • 以i为开始,dp[i]表示什么,通常为代求问题(具体依题目而定)
  • 2、状态转移方程
    *以上述的dp[i]意义为根据, 通过最近一步来分析和划分问题,由此来得到一个有关dp[i]的状态转移方程。
  • 3、dp表创建,初始化
  • 动态规划问题中,如果直接使用状态转移方程通常会伴随着越界访问等风险,所以一般需要初始化。而初始化最重要的两个注意事项便是:保证后续结果正确,不受初始值影响;下标的映射关系
  • 初始化一般分为以下两种:
  • 直接初始化开头的几个值。
  • 一维空间大小+1,下标从1开始;二维增加一行/一列
  • 4、填dp表、填表顺序:根据状态转移方程来确定填表顺序。
  • 5、确定返回值

一、不同路径1

【题目链接】:LCR 098. 不同路径

【题目】:

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

【分析】:

 这是个二维数组问题。我们定义dp[i][j]表示机器人走到下标为[i][j]位置时的总路径数。显然机器人要走到[i][j]位置,只能从[i][j-1]向右走、[i-1][j]向下走。所以状态转移方程为dp[i][j] = dp[i-1][j] + dp[i][j-1]。 但当i = 0或j =0时,显然状态转移方程不适应,需要特殊处理。这里我们采用的办法时,横纵都新增一行。然后我们还需将dp[0][1]或dp[1][0]初始化为1。

 接下我仅需从左往右、从上到下依次填表。最后返回结果即可!!

【代码实现】:

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));//创建dp表
        //初始化
        dp[1][0] = 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][n];
    }
};

二、珠宝的最高价值

【题目链接】:LCR 166. 珠宝的最高价值

【题目】:

现有一个记作二维矩阵 frame 的珠宝架,其中 frame[i][j] 为该位置珠宝的价值。拿取珠宝的规则为:

只能从架子的左上角开始拿珠宝每次可以移动到右侧或下侧的相邻位置到达珠宝架子的右下角时,停止拿取。

注意:珠宝的价值都是大于 0 的。除非这个架子上没有任何珠宝,比如 frame = [[0]]。

【分析】:

 我们可以定义dp[i][j]表示从开始到[i][j]位置所能拿到的珠宝最大价值。所以要得到dp[i][j]的值,我们只需将dp[i][j-1]和dp[i-1][j]的较大值假设当前下标[i][j]的珠宝价值即可。即动态转移方程为dp[i][j] = max(dp[i-1][j] + dp[i][j-1]) + frame[i][j]。但显然当i=0或j=0时,需要特殊处理。这里还是采用横竖都各加一行。需要注意的是此时下标的映射关系(具体参考代码)。

 ;接下我仅需从左往右、从上到下依次填表。最后返回结果!!

【代码实现】:

class Solution {
public:
    int jewelleryValue(vector<vector<int>>& frame) {
        int m = frame.size(), n = frame[0].size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        for(int i = 1; i <= m; i++)
            for(int j = 1; j <= n; j++)
                dp[i][j] = max(dp[i][j-1], dp[i-1][j]) + frame[i - 1][j - 1];
        return dp[m][n];        
    }
};

三、下降路径最小和

【题目链接】:931. 下降路径最小和

【题目】:

给你一个 n x n 的 方形 整数数组 matrix ,请你找出并返回通过 matrix 的下降路径 的 最小和 。

下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 (row, col) 的下一个元素应当是 (row + 1, col - 1)、(row + 1, col) 或者 (row + 1, col + 1) 。

【分析】:

 我们定义dp[i][j]表示下降到[i][j]位置时,下降路径的最小和。并且题目中已经明确表示下降到[i][j]位置有如下三种方式:

 显然我们容易得到状态转移方程为dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i-1][j+1])) + matrix[i-1][j-1]。但又有一个问题,当i=0、j=0、j=n时,状态转移方程会越界访问。所以我们给出的办法时加1行、加2列。同时为了不影响后续填表结果,我们将第一行初始为0,第1列和第n+1列初始化为INT_MAX(dp[0][1]、dp[0][n+1]除外)。

 接下来从左往右、从上到下依次填表。dp表填好后,最后一行的每个数都有可能是结果。我们需要依次比较,将最后一行的最小值返回!

【代码实现】:

class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& matrix) {
        int m = matrix.size(), n = matrix[0].size();
        vector<vector<int>> dp(m+1, vector<int>(n + 2, INT_MAX));
        //初始化
        for(int j = 0; j < n + 2; j++)
            dp[0][j] = 0;
        for(int i = 1; i <= m; i++)
            for(int j = 1; j <= n; j++)
            {
                dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i-1][j+1])) + matrix[i-1][j-1];
            }
        int ret = INT_MAX;
        for(int j = 1; j <= n; j++)
            ret = min(ret, dp[m][j]);
        return ret;
    }
};

四、地下城游戏

【题目链接】:174. 地下城游戏

【题目】:

恶魔们抓住了公主并将她关在了地下城 dungeon 的 右下角 。地下城是由 m x n 个房间组成的二维网格。我们英勇的骑士最初被安置在 左上角 的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。

骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。

有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。

为了尽快解救公主,骑士决定每次只 向右 或 向下 移动一步。

返回确保骑士能够拯救到公主所需的最低初始健康点数。

注意:任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。

【实例】:

输入:dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]]

输出:7

解释:如果骑士遵循最佳路径:右 -> 右 -> 下 -> 下 ,则骑士的初始健康点数至少为 7

【分析】:

 dp问题中我们一般定义dp[i][j]表示从开始到[i][j]位置的待解结果,即骑士从[0][0]走到[i][j]所需的最低初始健康点数。但我们发现[i][j]位置后面的数据对结果存在影响。例如:dungeon = [[1, 1],[1, -100]],假设我们走到了[0][1]位置,此时dp[0][1]=1。但此时走到dungeon[1][1]时,骑士死亡。后面结果会对当前数据有影响!!因此该思路错误。

 我们可以定义dp[i][j]表示从[i][j]位置走到结尾(假设结尾下标为[m][n])骑士所需的最低健康点数。此时示意图如下:(各位懂意思就行,手残画不了图)

 所以我们可以得到状态转移方程为dp[i][j] = min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j]。但还有两个问题:

  1. 如果dungeon[i][j]非常大,此时dp[i][j]可能为负数。此时骑士死亡,不符合要求。所以我们要进一步处理,dp[i][j] = max(1, dp[i][j])(即如果dp[i][j]为负,此时表示dungeon[i][j]j较大,我们仅需保证骑士到[i][j]位置时没有死亡即可)
  2. 如果[i][j]表示结尾呢?此时状态转移方程不适应。我们给出的办法是,最后一行、和最后一列各增一行。同时为了保存新增行列对后续填dp表不产生影响,我们其中的元素初始化为INT_MAX。同时为了保证dp[m][n]在是由状态转移方程时填表正确。我们要保证的时骑士处于[m][n]位置时还剩1个健康点数即可。所以我们将dp[m+1][n]或dp[m][n+1]初始化为1!

【代码实现】:

class Solution {
public:
    int calculateMinimumHP(vector<vector<int>>& dungeon) {
        int m = dungeon.size(), n = dungeon[0].size();
        //创建dp表
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
        //初始化
        dp[m][n - 1] = dp[m - 1][n] = 1;
        //填表
        for(int i = m - 1; i >= 0; i--)
            for(int j = n - 1; j >= 0; j--)
            {
                dp[i][j] = min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j];
                dp[i][j] = max(1, dp[i][j]);
            }
        return dp[0][0];
    }
};


相关文章
|
5天前
|
算法 开发者 Python
惊呆了!Python算法设计与分析,分治法、贪心、动态规划...这些你都会了吗?不会?那还不快来学!
【7月更文挑战第10天】探索编程巅峰,算法至关重要。Python以其易读性成为学习算法的首选。分治法,如归并排序,将大问题拆解;贪心算法,如找零问题,每步求局部最优;动态规划,如斐波那契数列,利用子问题解。通过示例代码,理解并掌握这些算法,提升编程技能,面对挑战更加从容。动手实践,体验算法的神奇力量吧!
26 8
|
7天前
|
算法 Python
算法不再难!Python分治法、贪心、动态规划实战解析,轻松应对各种算法挑战!
【7月更文挑战第8天】掌握Python算法三剑客:分治、贪心、动态规划。分治如归并排序,将大问题拆解递归解决;贪心策略在每步选最优解,如高效找零;动态规划利用子问题解,避免重复计算,解决最长公共子序列问题。实例展示,助你轻松驾驭算法!**
17 3
|
5天前
|
算法 Python
Python算法高手进阶指南:分治法、贪心算法、动态规划,掌握它们,算法难题迎刃而解!
【7月更文挑战第10天】探索Python算法的精华:分治法(如归并排序)、贪心策略(如找零钱问题)和动态规划(解复杂问题)。通过示例代码揭示它们如何优化问题解决,提升编程技能。掌握这些策略,攀登技术巅峰。
|
6天前
|
算法 程序员 Python
算法小白到大神的蜕变之路:Python分治法、贪心、动态规划,一步步带你走向算法巅峰!
【7月更文挑战第9天】探索算法之旅,以Python解锁编程高手之路。分治法如二分查找,将复杂问题拆解;贪心算法解决活动选择,每次选取局部最优;动态规划求斐波那契数列,避免重复计算,实现全局最优。每一步学习,都是编程能力的升华,助你应对复杂挑战,迈向算法大师!
12 1
|
6天前
|
存储 算法 Python
Python算法界的秘密武器:分治法巧解难题,贪心算法快速决策,动态规划优化未来!
【7月更文挑战第9天】Python中的分治、贪心和动态规划是三大关键算法。分治法将大问题分解为小问题求解,如归并排序;贪心算法每步选局部最优解,不保证全局最优,如找零钱;动态规划存储子问题解求全局最优,如斐波那契数列。选择合适算法能提升编程效率。
17 1
|
6天前
|
存储 算法 Python
震撼!Python算法设计与分析,分治法、贪心、动态规划...这些经典算法如何改变你的编程世界!
【7月更文挑战第9天】在Python的算法天地,分治、贪心、动态规划三巨头揭示了解题的智慧。分治如归并排序,将大问题拆解为小部分解决;贪心算法以局部最优求全局,如Prim的最小生成树;动态规划通过存储子问题解避免重复计算,如斐波那契数列。掌握这些,将重塑你的编程思维,点亮技术之路。
14 1
|
6天前
|
存储 算法 大数据
Python算法高手的必修课:深入理解分治法、贪心算法、动态规划,让你的代码更智能!
【7月更文挑战第9天】在Python算法学习中,分治法(如归并排序)将大问题分解为小部分递归解决;贪心算法(如货币找零)在每步选择局部最优解尝试达到全局最优;动态规划(如斐波那契数列)通过存储子问题解避免重复计算,解决重叠子问题。掌握这三种方法能提升代码效率,解决复杂问题。
|
7天前
|
算法 索引 Python
逆袭算法界!Python分治法、贪心算法、动态规划深度剖析,带你走出算法迷宫!
【7月更文挑战第8天】分治法,如快速排序,将大问题分解并合并解;贪心算法,选择局部最优解,如活动选择;动态规划,利用最优子结构避免重复计算,如斐波那契数列。Python示例展示这些算法如何解决实际问题,助你精通算法,勇闯迷宫。
16 1
|
7天前
|
算法 索引 Python
Python算法设计与分析大揭秘:分治法、贪心算法、动态规划...掌握它们,让你的编程之路更加顺畅!
【7月更文挑战第8天】探索Python中的三大算法:分治(如快速排序)、贪心(活动选择)和动态规划(0-1背包问题)。分治法将问题分解求解再合并;贪心策略逐步求局部最优;动态规划通过记忆子问题解避免重复计算。掌握这些算法,提升编程效率与解决问题能力。
15 1
|
7天前
|
算法 开发者 Python
惊!Python算法界的三大神器:分治法、贪心算法、动态规划,让你秒变算法大师!
【7月更文挑战第8天】在Python编程中,分治、贪心和动态规划是核心算法。分治如归并排序,将大问题拆解并递归求解;贪心算法针对找零问题,每次都选最大面额硬币,追求局部最优;动态规划则通过记忆化避免重复计算,如斐波那契数列。这些算法巧妙地提升效率,解决复杂问题。