草庐IT

详解多种动态规划问题,看完必会动态规划

S1aine 2024-03-13 原文

基本概念

动态规划(Dynamic Programming,简称DP)是运筹学的一个分支,是求解决策过程最优化的过程。20世纪50年代初,美国数学家贝尔曼等人在研究多阶段决策过程的优化问题时,提出并创立。

理解认知

动态规划(DP)通过循环做出每一步的最优解从而自底向上的得出对问题的整体最优解;这是它与分支算法的自顶向下求解和与贪心算法寻找局部最优解有本质的区别。

接下来为大家说明三步骤通解动态规划问题

动态规划解题模式

确定定义 —> 找初始值 —> 思考关系 =>写代码解

只要掌握这几步必会动态规划任意题型,本文提供多种动态规划题型按此模板解析,话不多说开始例题实战。

基础题型

一、青蛙跳台阶

问题:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

输入输出示例,下面给出三次 单独的 各自输入输出;

n=2时有2种跳法,n=7时有21种跳法,n=2时有1种跳法

输入:n = 2   |  n = 7  |   n = 0
输出:2       |  21     |   1

确定定义

这里要求跳上n级的台阶有多少种方法,就设dp[n]为跳上n级的台阶的跳法数

找初始值

动态规划是自底向上解决问题的,所以底部最小单元的初始值是必须明确的,这里我们可以看到上面示例中第三组输入n为0时输出为1(台阶为0级时只有一种跳法);而一级台阶只有一种跳法,两个台阶有两种跳法(1+1/2),三个台阶有三种跳法(1+1+1/1+2/2+1)…

显而易见初始值为n=0和n=1和2时情况

即为dp[0]=1,dp[1]=1,dp[2]=2

思考关系

上面提到过动态规划通过循环做出每一步的最优解从而自底向上的得出对问题的整体最优解,循环每一步就是依靠每个单元的关系依次推到,由dp[0]靠关系推导到dp[1],由dp[1]推导dp[2]…接下来推导出最终要求的 dp[n] 即为解决问题

这里的n阶台阶方法数推导之间有什么关系?

抛开初始值n=0和n=1,这里稍微思考一下其中关系
台阶数     跳法                    跳法数
n=2       11 2                    2

n=3       111 12 21               3

n=4       1111 112 121 211 22     5

我们思考一下要跳到一个n级台阶,其最后一步只能在跳一步或两步的方式

这里可以分类

台阶数     最后跳一步的跳法    最后跳两步的跳法    总跳法数
n=2       11               2                 1+1=2         
 
n=3       111 21           12                2+1=3

n=4       1111 121 211     112 22            3+2=5       

总跳法=最后跳一步的跳法数 + 最后跳两步的跳法数

关系方程就出来了

dp[n]=d[n-1]+d[n-2]


因为本题本质是求斐波那契数列的变体解问题

d[0]=1; d[1]=1; d[2]=2; d[3]=3; d[4]=5

1 1 2 3 5

这里数学基础扎实的读者可以看出从dp[1]开始就是斐波那契数列(2=1+1;3=1+2;5=2+3),继而快速推导出公式

d[n] = d[n-1] + d[n-2] ,这是累计经验更快更准确分析分题,多刷题和思考也能到这样

写代码解

完成上三步骤后代码解法就很显而易见了,因为解题本质是从题型中分析并确定需要的算法思想,在将其以代码形式表达出来的过程

这里上代码和注释

class Solution {
    public int numWays(int n) {
        int[] dp = new int[Math.max(n+1, 3)];
        dp[0] = 1;
        dp[1] = 1;
        dp[2] = 2;
        for(int i = 3; i<= n; i++){
            dp[i] = (dp[i-1]+dp[i-2]) % 1000000007;  //对1000000007取余是题干要求
        }
        return dp[n];
    }
}

二、连续子数组的最大和

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(n)。

【示例】
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

确定定义

本题问题是找出数组和的最大值,那就设最后要返回的最大值为 int max(值为最大和的子数组)

动态规划是自底向上不断循环每一步的最优解来使得全局最优,全局最优解max已经有了

再设出每一步的局部最优解 int dp(值为每次循环中求得的最大数组)

比如要求你三天吃饭用时最大的三餐时间之和max;那么 dp[n] 为你第n天里三顿中用时最长的一顿早/中/晚饭,

max = dp[1] + dp[2] +dp[3]

清楚明白,定义完成

找初始值

如果是找数组/字符规律那初始值就从最开始的0,1,2套入,然后发现其中特殊的的值

但这里是求已知数组的最大值,而且已经定义好了max和dp,直接将0赋予作为初始值

int dp = nums[0];
int max = nums[0];

清楚明白,已经找好

思考关系

tip:有小伙伴可能想出想要 子数组和最大,其第一个和最后一个都一定是正数;毕竟开场和结尾可以选择的正数来最大化数组和,但要思考到 [-1] [-1,-2,-3] 这种示例,所以找首尾一定是正数的思维是在这里行不通的。

数组和是由每一个单个的数字环环相扣连接累加而成(理解这句“废话“才能动态规划求最优解)

一个子数组m加上紧接的下一个数字m的值,m+n>n,那铁定把收编到数组里,但如果m+n<n; 那m数组还成了负担,不如重新让数字n替换为新的数组

下举例中m和n都是随机设,主要是表示出这个道理

{3,1,-2}中m为{3+1=4}时,n为-2;m+n>n,所以收编n后新数组m为{3,1,-2}

{-6,2,-1}中m为{-6+2=-4},n为-1;m+n<n,m数组值这么小反而成了累赘,所以令m=n,即m新设为{-1}

将想清楚这种数组的最大构成关系后,就能模拟出关系表达式了

dp = Math.max{ dp + num[i],num[i]}

(第一个max为定义的最大值变量,第二个为 Math.max 是比较并选取两数中较大的一个)

写代码解

package slaine;

public class test {
    public static void main(String[] args) {
        int[] nums = {-2,1,-3,4,-1,2,1,-5,4};
        System.out.println(maxSubArray(nums));
    }
    public static int maxSubArray(int[] nums) {

        int dp = nums[0];
        int max = nums[0];

        for(int i = 1; i < nums.length; i++){
//            System.out.println("传入数组第"+i+"个数字是"+nums[i]);
            dp = Math.max(dp + nums[i], nums[i]);
//            System.out.println("dp选取为"+dp);
            max = Math.max(max, dp);
//            System.out.println("第"+i+"次循环中最大值为"+max);
            System.out.println();
        }
        return max;
    }
}

进阶题型

现在我们开始加速了小伙伴们,冲冲冲,这里选的 “礼物的最大价值” 是刚我们做过的 “连续子数组的最大和” 进阶小变体

三、礼物的最大价值

很显然本题将 “求连续数组最大和”的一维数组变体为 求二位数组中路径最大值,乍一看有点难搞?不要怕,照样三步骤轻松拆解

确定定义

注意传入的二维数组为 int[][] [] [] grid;

新建一个n行m列的与输入同格式二维数组 int [] []dp

int n = grid.length;

int m = grid[0].length;

int[][] dp = new int[n][m];

但新建的二维数组dp里要装什么?动动你的小脑袋想想,

【动态规划通过循环做出每一步的最优解从而自底向上的得出对问题的整体最优解】(这句话在本文至少出现三遍…),

那当然显而易见的新建二维数组dp其中 每个元素 是当前循环中的想求的最优解

看不懂上面这句话的笨蛋 在本文 Ctrl+F 搜索 “吃饭” ,背下例子并把【】的话背三十遍

为了更清楚的解释我们建的dp数组要装什么玩意,这里举例(作者S1aine真的殚心竭虑地想给你说明白)

输入二维数组int [] [] grid: 
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]

那么我们新建的二维数组dp就应该是
[
  [1,4,5],
  [2,9,10],
  [6,11,12]
]

找初始值

回忆一下刚才咱们在 “连续子数组的最大和” 里怎么定义初始值的?直接把初值第0个赋予了max和dp[i]

这里的初值什么? 第一反应是左上角的grid[0] [0] ,将它作为初始值赋予dp[0] [0]没有问题

但是同时要结合题干考虑,题干里明确说了每次只能 “向右 或者 向下” 走,那就明显还需要边界的初始值来约束路径

那边界初始值自然是dp中第0行和第0列了,求第0行和第0列两串初始值

        int n = grid.length;
        int m = grid[0].length;

        int[][] dp = new int[n][m]; //左上角初始值
        dp[0][0] = grid[0][0];
        for(int i = 1; i < n; i++){  //第0列的初始值
            dp[i][0] = dp[i-1][0] + grid[i][0];
        }
        for(int j = 1; j < m; j++){ //第0行的初始值
            dp[0][j] = dp[0][j -1] + grid[0][j];
        }

初始值全部找到,下一个步骤

思考关系

这里直接引用 Krahets 总结好的公式,感谢K佬

写代码解

class Solution {
    public int maxValue(int[][] grid) {

        int n = grid.length;
        int m = grid[0].length;

        int[][] dp = new int[n][m];
        dp[0][0] = grid[0][0];
        for(int i = 1; i < n; i++){
            dp[i][0] = dp[i-1][0] + grid[i][0];
        }
        for(int j = 1; j < m; j++){
            dp[0][j] = dp[0][j -1] + grid[0][j];
        }

        for(int i = 1; i < n; i++){ 
            for(int j = 1; j < m; j++){
                dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) + grid[i][j];
            }
        }

        return dp[n-1][m-1];
    }
    
}

四、机器人的运动范围

还等着看题解?

都学了这么多了不去练练?

与上题同样是二维数组的动态规划但做了些小变体

点击下方传送门开始手撕

机器人的运动范围

加油哦, ~ ~~

有关详解多种动态规划问题,看完必会动态规划的更多相关文章

  1. ruby - 在 64 位 Snow Leopard 上使用 rvm、postgres 9.0、ruby 1.9.2-p136 安装 pg gem 时出现问题 - 2

    我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po

  2. ruby - 通过 rvm 升级 ruby​​gems 的问题 - 2

    尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub

  3. ruby - 通过 RVM (OSX Mountain Lion) 安装 Ruby 2.0.0-p247 时遇到问题 - 2

    我的最终目标是安装当前版本的RubyonRails。我在OSXMountainLion上运行。到目前为止,这是我的过程:已安装的RVM$\curl-Lhttps://get.rvm.io|bash-sstable检查已知(我假设已批准)安装$rvmlistknown我看到当前的稳定版本可用[ruby-]2.0.0[-p247]输入命令安装$rvminstall2.0.0-p247注意:我也试过这些安装命令$rvminstallruby-2.0.0-p247$rvminstallruby=2.0.0-p247我很快就无处可去了。结果:$rvminstall2.0.0-p247Search

  4. ruby - Fast-stemmer 安装问题 - 2

    由于fast-stemmer的问题,我很难安装我想要的任何ruby​​gem。我把我得到的错误放在下面。Buildingnativeextensions.Thiscouldtakeawhile...ERROR:Errorinstallingfast-stemmer:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcreatingMakefilemake"DESTDIR="cleanmake"DESTDIR=

  5. ruby - 安装 Ruby 时遇到问题(无法下载资源 "readline--patch") - 2

    当我尝试安装Ruby时遇到此错误。我试过查看this和this但无济于事➜~brewinstallrubyWarning:YouareusingOSX10.12.Wedonotprovidesupportforthispre-releaseversion.Youmayencounterbuildfailuresorotherbreakages.Pleasecreatepull-requestsinsteadoffilingissues.==>Installingdependenciesforruby:readline,libyaml,makedepend==>Installingrub

  6. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  7. ruby-on-rails - 简单的 Ruby on Rails 问题——如何将评论附加到用户和文章? - 2

    我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。

  8. ruby - nanoc 和多种布局 - 2

    是否可以为特定(或所有)项目使用多个布局?例如,我有几个项目,我想对其应用两种不同的布局。一个是绿色的,一个是蓝色的(但是)。我想将它们编译到我的输出目录中的两个不同文件夹中(例如v1和v2)。我一直在玩弄规则和编译block,但我不知道这是怎么回事。因为,每个项目在编译过程中只编译一次,我不能告诉nanoc第一次用layout1编译,第二次用layout2编译。我试过这样的东西,但它导致输出文件损坏。compile'*'doifitem.binary?#don’tfilterbinaryitemselsefilter:erblayout'layout1'layout'layout2'

  9. 【高数】用拉格朗日中值定理解决极限问题 - 2

    首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有,  也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加

  10. SPI接收数据异常问题总结 - 2

    SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手

随机推荐