算法通关村第十九关——动态规划是怎么回事(青铜)

news2025/1/18 2:10:39

算法通关村第十九关——动态规划是怎么回事(青铜)

    • 前言
    • 1 什么是动态规划
    • 2 动态规划的解题步骤
    • 3 简单入门
      • 3.1 组合总和
      • 3.2 最小路径和
      • 3.3 三角形最小路径和
    • 4 理解动态规划

前言

动态规划是一种解决复杂问题的算法思想,它将一个大问题分解为多个相互关联的子问题,并通过递推关系将子问题的解整合起来,最终得到原问题的解。动态规划的核心思想是将问题划分为重叠子问题,并存储子问题的解,避免重复计算。

动态规划通常用于求解最优化问题,如求解最长公共子序列、最短路径、背包问题等。它的基本步骤包括定义状态、设置初始状态、确定状态转移方程和计算最优解。

动态规划的优点是减少了重复计算,提高了算法效率,但它也需要额外的空间来存储子问题的解,因此在使用动态规划时需要权衡时间和空间的开销。

1 什么是动态规划

动态规划(Dynamic Programming),简称dp,是一种解决多阶段决策问题的优化方法。它通过将问题划分为多个子问题,并保存子问题的解,以避免重复计算,从而得到原问题的最优解。

动态规划的核心思想是利用子问题的最优解来推导出原问题的最优解。具体来说,动态规划通常包含以下步骤:

  1. 定义状态:将原问题划分为若干个子问题,并确定每个子问题的状态,即问题的不同维度。
  2. 设置初始状态:初始化边界条件和初始状态值。
  3. 确定状态转移方程:根据子问题之间的关系,建立状态之间的递推关系,即通过已解决的子问题来求解当前问题。
  4. 计算最优解:按照状态转移方程,从初始状态逐步计算出最终的目标状态,即原问题的最优解。

下面以求解斐波那契数列为例进行详细说明。

斐波那契数列的定义为:F(n) = F(n-1) + F(n-2),其中F(0) = 0,F(1) = 1。

使用动态规划求解斐波那契数列的步骤如下:

  1. 定义状态:将斐波那契数列的第n个数记为F(n),即问题的状态为n。
  2. 设置初始状态:定义F(0) = 0和F(1) = 1,作为初始状态。
  3. 确定状态转移方程:根据斐波那契数列的递推关系式F(n) = F(n-1) + F(n-2),可以得到状态转移方程F(n) = F(n-1) + F(n-2)。
  4. 计算最优解:按照状态转移方程从初始状态开始逐步计算出F(n)的值,直到计算出F(n)。

代码如下:

public class Fibonacci {
    public static int fibonacci(int n) {
        if (n <= 1) {
            return n;
        }

        // 定义一个数组来保存斐波那契数列的每个元素的值
        int[] dp = new int[n + 1];

        // 设置初始状态
        dp[0] = 0;
        dp[1] = 1;

        // 确定状态转移方程,计算最优解
        for (int i = 2; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }

        // 返回结果
        return dp[n];
    }

    public static void main(String[] args) {
        int n = 10;
        System.out.println("Fibonacci(" + n + ") = " + fibonacci(n));
    }
}

在上面的代码中,我添加了注释来说明使用动态规划解决斐波那契数列的步骤。

  1. 首先定义了一个数组dp用于保存斐波那契数列的每个元素的值。
  2. 然后,设置初始状态,即dp[0] = 0dp[1] = 1
  3. 接下来,通过一个循环从第3个元素开始计算每个元素的值,并使用状态转移方程dp[i] = dp[i - 1] + dp[i - 2]来计算最优解。
  4. 最后,返回数组中索引为n的元素值,即得到斐波那契数列的第n个数。

执行上述代码,可以得到输出结果为Fibonacci(10) = 55,表示斐波那契数列的第10个数为55.

与动态规划相对应的是贪心算法(Greedy Algorithm)。

贪心算法每次选择当前状态下的最优解,而不考虑全局最优解。贪心算法通常适用于满足贪心选择性质和最优子结构性质的问题,但不一定能得到全局最优解。

举个例子:

假设有一笔钱要找零,在某个国家的货币单位只有1元、5元和10元。目标是找零的总数量最少。

  • 使用贪心算法来解决这个问题时,每次都选择面额最大的币种进行找零。例如,要找零27元,先选择10元,剩下17元,再选择10元,剩下7元,最后选择5元和两个1元,得到找零总数量为4。

  • 然而,贪心算法在某些情况下并不一定能得到最优解。对于要找零15元的情况,贪心算法会选择10元和5个1元,共计6个硬币。而实际上,最优解是使用三个5元的硬币,共计3个硬币。

因此,动态规划可以得到全局最优解,而贪心算法只能得到局部最优解。

2 动态规划的解题步骤

以下内容摘抄于代码随想录:代码随想录——动态规划

当解动态规划问题时,许多同学常常会陷入一个误区,认为将状态转移公式背下来,稍加修改就可以开始编写代码了。甚至有些同学在通过测试之后,仍不清楚dp[i]所代表的是什么。

这种模糊的状态会使我们对问题的本质理解不清,因此在遇到更复杂的问题时可能就束手无策了。结果往往是去看题解,然后继续模仿而陷入这种恶性循环中

虽然递推公式(状态转移公式)非常重要,但动态规划不仅仅只包含递推公式。

为了真正掌握动态规划,我们需要将解题过程拆解为以下五个步骤,并确保每个步骤都清晰明了!

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

可能有些同学会想,为什么要先确定递推公式,然后再考虑初始化呢?

因为在某些情况下,递推公式决定了dp数组应该如何初始化!

接下来的讲解都是以这五个步骤为基础进行的。

刷过动态规划题目的同学可能已经意识到了递推公式的重要性,觉得一旦确定了递推公式,问题就解决了。

然而,确定递推公式只是解题过程中的一小部分!

有些同学虽然知道递推公式,但却不清楚dp数组该如何初始化,或者无法找到正确的遍历顺序。结果就是他们能记住公式,但无论如何修改代码都无法通过测试。

后续的讲解将逐渐展示这五个步骤的重要性。

3 简单入门

下面会通过一些例子一步步了解DP,循序渐进~

3.1 组合总和

leetcode 62. 不同路径

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

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

  1. 确定递推公式

想要求dp[ i ] [ j ] ,只能有两个方向来推导出来,即dp[ i -1 ] [ j ] 和 dp[ i ] [ j-1 ] 。

此时在回顾一下 dp[ i-1 ] [ j ] 表示啥,是从(0, 0)的位置到(i - 1, j)有几条路径,dp[ i ] [ j-1 ] 同理。

为什么呢?

当我们想要求解dp[ i ] [ j ] ,时,只有两个方向可以推导出它的值,即dp[ i-1 ] [ j ] ,和dp[ i ] [ j-1 ] 。这是因为在问题中机器人只能向下或向右移动。

假设我们要求dp[ i ] [ j ] ,那么根据题目的限制条件,有以下两种情况:

  1. 从上方的位置dp[ i -1] [ j ] 向下移动一步,到达位置dp[ i ] [ j ] 。
  2. 从左边的位置dp[ i ] [ j-1 ] 向右移动一步,到达位置dp[ i ] [ j ] 。

因此,我们可以通过这两个方向的状态值来推导出dp[ i ] [ j ] 的值,即dp[ i ] [ j ] ,= dp[ i -1 ] [ j ] + dp[ i ] [ j -1 ] ,。

通过不断迭代计算每个位置的路径数量,最终就能得到起点到终点的总路径数量。

  1. dp数组的初始化

如何初始化呢,首先dp[ i ] [ 0 ] 一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[ 0] [ j ] 也同理。

所以初始化代码为:

for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;
  1. 确定遍历顺序

这里要看一下递推公式dp[ i ] [ j ] = dp[ i-1 ] [ j ] + dp[ i ] [ j-1 ] ,dp[ i ] [ j ] 都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。

这样就可以保证推导dp[ i ] [ j ] 的时候,dp[ i -1 ] [ j ] 和 dp[ i ] [ j-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];
    }
}
  1. 距离推导dp数组

如图所示:

image-20230906231158729

最后代码如下:

class Solution {
    public static 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];
    }
}

3.2 最小路径和

leetcode 64. 最小路径和

思路:

这道题要一步步去理解,因为刚刚入门,所以需要逐渐理解整个思路,尤其是dp数组的定义,特别重要!!

下面使用5步法来解决:

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

在这道题中,我们要求从起点到达位置(i,j)的最小路径和。

因此,dp[ i ] [ j ]表示从起点到达位置(i,j)的最小路径和。

int[][] dp = new int[i][j];
  1. 确定递推公式

根据题目要求,我们可以向右向下移动,

所以到达位置(i,j)的最小路径和等于上方和左方路径和的较小值加上当前位置的数字

即dp[ i ] [ j ] = min(dp[ i-1 ] [ j ], dp[ i ] [ j-1 ] ) + grid [ i ] [ j ] 。

dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
  1. 初始化dp数组

由于题目要求找出最小路径和,我们可以将dp数组全部初始化为一个较大的值(比如MAX_VALUE),

除了dp [ 0 ] [ 0 ] 应该等于grid [ 0 ] [ 0 ] ,因为到达起点的最小路径和就是起点的数字本身。

dp[0][0] = grid[0][0];
  1. 确定遍历顺序

题目要求从左上角开始,先遍历行再遍历列。

这是因为在计算dp[i] [j]时,我们需要用到dp[i-1] [j]和dp[i] [j-1]的值,而这两个值都是在当前行或当前列的前面位置计算得出的。所以我们要按照从上到下、从左到右的顺序进行遍历。

for (int i = 1; i < m; i++) {
    for (int j = 1; j < n; j++) {
        dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
    }
}
  1. 举例推导dp数组
image-20230906233334984

最后代码如下:

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        
        // 1. 确定dp数组及其下标的含义
        int[][] dp = new int[m][n];
        
        // 2. 递推公式:dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
        // 3. 初始化dp数组
        dp[0][0] = grid[0][0];
        for (int i = 1; i < m; i++) {
            dp[i][0] = dp[i-1][0] + grid[i][0];
        }
        for (int j = 1; j < n; j++) {
            dp[0][j] = dp[0][j-1] + grid[0][j];
        }
        
        // 4. 确定遍历顺序
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                // 5. 举例推导dp数组
                dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
            }
        }
        
        return dp[m-1][n-1]; // 最后结果即为dp数组右下角的值
    }
}

3.3 三角形最小路径和

leetcode 120. 三角形最小路径和

这道题跟上一题很像,也是计算路径和,所以整体思路是一样的

老样子,使用五步法:

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

dp[i] [j] 表示到达第 i 行第 j 列的最小路径和。

int m = triangle.size();
int[][] dp = new int[m][m];
  1. 确定递推公式:
  • dp[i] [j] = dp[i-1] [j] + triangle[i] [j] (当 j=0时,只能从上一行的第一个元素向下走)

  • dp[i] [j] = dp[i-1] [j-1] + triangle[i] [j] (当 j=i 时,只能从上一行的最后一个元素向下走)

  • dp[i] [j] = min(dp[i-1] [j], dp[i-1] [j-1]) + triangle[i] [j] (其他情况)

  1. 初始化dp数组:

没啥好说的,初始化的就是第一个数

dp[0][0] = triangle.get(0).get(0);
  1. 确定遍历顺序:

从上到下依次遍历每一行,从左到右依次遍历每一列。

for (int i = 1; i < m; i++) {
    dp[i][0] = dp[i - 1][0] + triangle.get(i).get(0);
    for (int j = 1; j < i; j++) {
        dp[i][j] = Math.min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle.get(i).get(j);
    }
    dp[i][i] = dp[i-1][i-1] + triangle.get(i).get(i);
}
  1. 举例推导dp数组

这一步是验证,也是为了防止错误

image-20230907122208307

全代码如下:

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        int m = triangle.size();
        int[][] dp = new int[m][m];

        dp[0][0] = triangle.get(0).get(0);

        for (int i = 1; i < m; i++) {
            dp[i][0] = dp[i - 1][0] + triangle.get(i).get(0);
            for (int j = 1; j < i; j++) {
                dp[i][j] = Math.min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle.get(i).get(j);
            }
            dp[i][i] = dp[i-1][i-1] + triangle.get(i).get(i);
        }

        int min = dp[m - 1][0];
        for (int i = 1; i < m; i++) {
            min = Math.min(min, dp[m - 1][i]);
        }

        return min;
    }
}

4 理解动态规划

做了前面三道题,也能感觉到动态规划与回溯的一些不一样地方,虽然都有模版可以使用

动态规划(Dynamic Programming)是一种解决问题的算法思想,它将一个待求解的问题分解成若干个子问题,并先求解这些子问题,再从中得到原问题的解。动态规划可以高效地解决一些需要穷举所有可能情况的问题。

重点:

区分动态规划和回溯的重要区别在于动态规划只关心当前结果是什么,而不关心怎么来的,因此无法获得完整的路径。而回溯可以记录所有的路径,但解决效率较低。

动态规划的基本思想是通过穷举来找到满足要求的最优解,并使用记忆化搜索来消除重复计算。记忆化搜索将已经计算过的结果存储在数组中,避免重复计算。

动态规划问题具备最优子结构,即问题的最优解可以由其子问题的最优解递推得到。为了能正确地穷举子问题并得到最优解,需要编写正确的状态转移方程。大部分状态转移可以通过数组实现,因此动态规划代码一般以for循环为主体。

以下是一个典型的动态规划代码模板:

int dp[] = new int[n];  // 创建一个数组用于存储子问题的解
dp[0] = base case;  // 设置初始状态
for (int i = 1; i < n; i++) {
    for (int j = 0; j < i; j++) {
        dp[i] = 计算dp[i]和dp[j]之间的关系;  // 根据状态转移方程计算当前子问题的解
    }
}
return 最终结果;

要注意的是,动态规划问题的难点在于找到最优子结构和编写正确的状态转移方程。具体问题的最优子结构和状态转移方程需要根据实际情况进行分析。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/997869.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Apache HTTPD 多后缀名解析漏洞复现

什么是多后缀名解析漏洞加粗样式: 多后缀名解析漏洞&#xff08;Multiple Extension Handling Vulnerability&#xff09;指的是一种安全漏洞&#xff0c;发生在某些操作系统或网络服务中的文件扩展名处理机制中。 这种漏洞的本质是当文件具有多个后缀名&#xff08;例如file.…

l8-d12 IP协议与ethernet协议

一、IP协议作用和意义 分组在互联网中的传送 分组传输路径 二、IP 数据报首部格式 1.IP 数据报的格式 版本——占 4 位&#xff0c;指 IP 协议的版本。目前的 IP 协议版本号为 4 (即 IPv4)。 首部长度——占 4 位&#xff0c;可表示的最大数值是 15 个单位(一个单位为 4 字…

【Spring】手动实现Spring底层机制-问题的引出

&#x1f384;欢迎来到边境矢梦的csdn博文&#x1f384; &#x1f384;本文主要梳理手动实现Spring底层机制-问题的引出 &#x1f384; &#x1f308;我是边境矢梦&#xff0c;一个正在为秋招和算法竞赛做准备的学生&#x1f308; &#x1f386;喜欢的朋友可以关注一下&#x1…

【实践篇】Redis最强Java客户端(四)之Ression分布式集合使用指南

文章目录 0. 前言1.Ression分布式集合1.1 分布式列表1.1.1 使用场景和实现原理&#xff1a;1.1.2 基本概念和使用方法&#xff1a; 1.2 分布式集合1.2.1 使用场景和实现原理&#xff1a;1.2.2 基本概念和使用方法&#xff1a; 1.3 分布式映射1.3.1 使用场景和实现原理&#xff…

CSS三种样式表、样式表优先级、CSS选择器

一、CSS介绍&#xff1a; 1.1、CSS介绍&#xff1a; CSS&#xff0c;全称是&#xff1a;Cascading Style Sheets&#xff0c;层叠样式表&#xff0c;用于修饰HTML页面的。 CSS编写规则如下所示&#xff1a; CSS编写的规则分为两部分&#xff0c;分别是&#xff1a;选择器、声…

SpringMVC学习简要

学习资料&#xff1a; SpringMVC-03-入门案例工作流程解析_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1ZF411G7eP/?p3&spm_id_frompageDriver&vd_source4ac53f52a57eb96a3c8406b971b038ae 常用MYSQL命令&#xff1a;http://t.csdn.cn/zshCP 学习要求 什么是…

微信小程序Day4笔记

1. 组件的创建与引用 创建组件 在项目的根目录中&#xff0c;鼠标右键&#xff0c;创建components -> test文件夹在新建的test文件夹上&#xff0c;鼠标右键&#xff0c;点击新建Component键入组件的名称之后回车&#xff0c;会自动生成组件对应的4个文件&#xff0c;后缀…

《服务器无状态设计:为什么如何实现无状态API?》

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

3.1 栈和队列的定义和特点

3.1.1 栈的定义和特点 主要内容&#xff1a; 3.1 栈和队列的定义和特点 3.1.1 栈的定义和特点 定义&#xff1a; 栈是一种特殊的线性表&#xff0c;只允许在一端进行插入或删除操作。这一端被称为栈顶&#xff0c;而另一端则称为栈底。不包含任何元素的栈被称为空栈。 特点…

分布式调度 Elastic-job

分布式调度 Elastic-job 1.概述 1.1什么是任务调度 我们可以思考一下下面业务场景的解决方案: 某电商平台需要每天上午10点&#xff0c;下午3点&#xff0c;晚上8点发放一批优惠券某银行系统需要在信用卡到期还款日的前三天进行短信提醒某财务系统需要在每天凌晨0:10分结算…

Java虚拟机(JVM)解析:内存区域、类加载、垃圾回收和选型考虑

目录 一、JVM内存区域划分 二、JVM类加载 三、JVM垃圾回收&#xff08;GC&#xff09; 一、JVM内存区域划分 栈堆方法区(元数据区)程序计数器 1.栈 举个例子&#xff1a; 那具体是怎么分的呢&#xff1f;如下图 本地方法栈&#xff1a;给JVM内部的方法准备的栈空间 虚拟…

在MySQL客户端使用Tab健进行命令补全

在MySQL客户端中&#xff0c;你可以使用Tab键进行命令补全&#xff0c;这将提高我们的效率&#xff0c;这与Linux命令行中的行为类似。例如&#xff0c;如果你输入SEL然后按Tab键&#xff0c;MySQL客户端会自动补全为SELECT。 然而&#xff0c;需要注意的是&#xff0c;这个功能…

数据结构 - 栈

目录 二、栈的实现 1.数组模拟实现栈 设计思想: 方法实现: Peek(): 偷窥栈顶元素 pop(): 栈顶出栈 push(): 2.链表模拟实现 3 . 栈的继承体系 总结 前言 大家好,这篇博客带大家了解一下栈是什么? 并且用两种方式为大家实现一下栈 一、栈是什么&#xff1f; 栈是一种数…

npm介绍

npm介绍 npm&#xff08;Node Package Manager的缩写&#xff09;是一个软件包管理器&#xff0c;主要进行JavaScript的包管理。通过npm&#xff0c;我们可以很方便地进行JavaScript包的下载、升级&#xff0c;我们也可以把我们开发的JavaScript包共享给其他使用者。 在npm没…

大数据-玩转数据-Flink状态后端(下)

一、状态后端 每传入一条数据&#xff0c;有状态的算子任务都会读取和更新状态。由于有效的状态访问对于处理数据的低延迟至关重要&#xff0c;因此每个并行任务(子任务)都会在本地维护其状态&#xff0c;以确保快速的状态访问。 状态的存储、访问以及维护&#xff0c;由一个…

goweb入门

创建gomod项目 go mod init web01新建main.go package mainimport ("fmt""net/http" )func handler(writer http.ResponseWriter, request *http.Request) {fmt.Fprintf(writer, "Hello World,%s!", request.URL.Path[1:]) } func main() {fmt…

Mysql基于成本选择索引

本篇文章介绍mysql基于成本选择索引的行为&#xff0c;解释为什么有时候明明可以走索引&#xff0c;但mysql却没有走索引的原因 mysql索引失效的场景大致有几种 不符合最左前缀原则在索引列上使用函数或隐式类型转换使用like查询&#xff0c;如 %xxx回表代价太大索引列区分度过…

PHP8中获取并删除数组中最后一个元素-PHP8知识详解

在php8中&#xff0c;array_pop()函数将返回数组的最后一个元素&#xff0c;并且将该元素从数组中删除。语法格式如下&#xff1a; array_pop(目标数组) 获取并删除数组中最后一个元素&#xff0c;参考代码&#xff1a; <?php $stu array(s001>明明,s002>亮亮,s…

Ansible数组同步至Shell脚本数组中

1、ansible中定义数组&#xff0c;我以 ccaPojectList 数组为例子,如下图数组内容 2、需要写一个j2模板的Shell脚本&#xff0c;在j2模板的Shell脚本中引用ansible的 ccaPojectList 数组&#xff0c;大致如下图&#xff1a; {% for item in ccaPojectList %} "{{ item }…

Linux JAVA环境的搭建tomcat的部署(含多实例)

tomcat tomcat是Apache软件基金会项目中的一个核心项目由 Apache、Sun 和其他一些公司及个人共同开发而成。tomcat 是 Java 语言开发的&#xff0c;Tomcat 服务器是一个免费的开放源代码的 Web 应用服务器. tomcat的组成 tomcat用于支持Java Servlet和JSP。它是一个重要的We…