算法学习(动态规划)- 数塔问题

前言

之前碰到了扔鸡蛋问题(给你2个鸡蛋,在100层楼上扔,要求想出一个好的策略,去测出哪一层楼开始鸡蛋就会碎掉),一直摸不着头脑。后来才知道可以使用“动态规划”这种思想(或者叫算法范式)去解决这个问题。

但是看了一些鸡蛋问题和动态规划的文章,依然只是流于形式,并不能理解其中的精髓。想想或许是鸡蛋问题对我现在而言难了一些,所以只好找了一些其它的动态规划问题,从简单入手,先去理解“动态规划”的思想精髓,再反过来去思考“鸡蛋问题”。

其中一个经典问题是01背包问题,这是我之前一直想搞懂的一个问题。看了一篇文章,就大致上理解了这个问题的解决办法和思路。所以我就不班门弄斧了,直接推荐这篇文章:动态规划之01背包问题(最易理解的讲解)

比起一开始就搬概念和公式,这篇文章用了一个“填表格”的方式去让读者理解背包问题的解决过程,这对初学者更友好。推荐大家也试着去画出那个表格,然后自己填一填,不需要编程,只需要一张纸和笔和你的脑子,就能按照文中的思路把表格填完,然后就解决了背包问题。算法编程实际只是对你脑子的这种思维过程的一个模拟。

我想,之所以这种所谓“思想”、“范式”之类的东西难以理解,也许是因为它经过了双重的“抽象”吧。比如,以下是动态规划思想的描述:

动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式…

有时候,真的想吐槽这些东西就是“正确的废话”。你不能说这些描述有什么错,但是真遇到问题的时候,光看这些描述你又完全没办法想出解决问题的方法。

解决某个具体问题,我们会把问题进行抽象,例如把搜索问题抽象成树问题。而这些所谓的“思想”,为了归纳这一类问题的共性,又成了对这类问题的抽象。即,成了“抽象之抽象”,双重抽象。

普适的代价即抽象

不知道别人如何,反正我对于“抽象”这类反人类大脑结构的东西实在无法理解。只能重新“化抽象为形象”,才能去“识”得它。就算是概念、思想这类看不见摸不着的东西,还是得转换成我脑海一个具象化的东西才行。

理解了01背包问题之后,我开始找到第2个动态规划的问题,即数塔问题。企图通过解决这些问题,一窥“动态规划”的冰山一角。

下面进入正题。

问题

如图,有一个数塔,从顶部出发,在每一个节点可以选择向左或者向右走,一直走到底层。要求找出一条路径,使得所走路径上的节点数字之和最大。
在这里插入图片描述
(图片来自百度)

分析

1.穷举
1)思路来源

这是在没有去看正确的动态规划方法之前,我自己想出的思路。

虽然不是“动态规划”,但是这个走歪的方向,也许更能让我们理解究竟怎样才算是“动态规划”。所以这个思路也是有价值的,可以说一说。

我一开始的理解,“动态规划”就是“使用已经计算好的信息,避免重复的计算”。这个想法的来源来自于这篇文章:

如何理解动态规划?

文章用了一张图来解释什么是动态规划:
在这里插入图片描述
我用当时的个人理解来复述一下:要计算A -> Q -> Z这条线路的最优选择,实际上并不需要我们把 A -> H -> Q -> Z、 A -> I -> Q -> Z 这两条路径都计算出来。 我们在计算A -> Q 的时候, 实际上已经把A -> H ->Q 、A -> I -> Q 这部分计算过了,因此已经得出了A -> Q的最优线路。计算A -> Q -> Z的时候,只需要利用这个结果即可。A -> Q是A -> Q -> Z的一个部分(注意:这个说法不完全正确,所以才会导致我得出了一个不完美的思路),因此能够“使用已经计算好的信息,避免重复的计算”。

当时我的理解不能说十分准确,认为动态规划就是:一个子问题如果属于一个大问题的一部分,就可以利用子问题的结果信息去计算大问题,避免重复计算。这个说法不准确,我后面会纠正这种想法。

但此时我们就顺着这种错误想法,看看这种想法会产生怎样的解题思路。

2)思维过程

毫无疑问,我们可以把问题图中的数塔看成一个大数塔,它其实可以看作包含了很多小数塔。比如只有两层的 9、12、15:

在这里插入图片描述
或者三层的9、12、15、10、6、8:
在这里插入图片描述
总之一个大数塔是可以拆分成小数塔的。那么,为了找到一个大数塔哪条路最优,是不是可以对小数塔找到最优?然后每一层每一层我们都最优,最后就能得到大数塔的最优了呢?

咋听起来很诱人,似乎找到了很厉害很巧妙的办法。但可惜,在这里行不通。这实际上就是“贪心算法”(em…又一种算法思想范式…)的本质:只要每一步我们都找最优,找到最后我们就能找到最后的最优。

还是引用推荐文章的一张图:
在这里插入图片描述
去计算A -> Z的最优线路,我们先一步一步来。先算A到“H or I”的最优线路,然后往下算“H or I”到“P or Q”的最优线路,最后算“P or Q”到Z的最优线路。算到最后,我们就得到了A到Z的最优线路。

这种情况下,我们才能用“贪心法”。看上去似乎跟“动态规划”的图不太一样,我把这张图改改,把“H or I”、“P or Q”这两个节点展开:
在这里插入图片描述
可以看到,任意一层的任意节点,都与下一层的所有节点有连接。第一层的A,与第二层的H、I都是连接的。第二层的H、I,与三层的P、Q都是连接的。第三层的P、Q,与第四层的Z都是连接的。

大概只有问题的推理形式具有这样的图特性,我们才能使用“贪心”法去解决吧。

我来再来看看我们的数塔,如果我把数塔横着倒放,变成这样:
在这里插入图片描述
看起来是不是跟“贪心法”的结构图不一样?是不是更像“动态规划”的那张结构图:
在这里插入图片描述
这也就说明了,为什么数塔问题可以用“动态规划”去解决。

3)解决办法(当然,最后想歪了,得出了穷举法。。。)

既然没办法通过一步步计算最优,来得出最后的最优,那我就妥协一些。每一次计算某一级子树塔的时候,我就用低一层级的子数塔的全部路线,然后再跟这一层的全部节点作一个连接,得出新的路线。

这样一步步不断计算出路线、以及这些路线的节点数值之和,最后把所有路线全部计算出来,作一个排序,就能知道哪条路线节点数值之和最大,不是就能得出问题答案了么?

想出这个办法的时候,我并不能确定这到底是不是“动态规划”,毕竟这时候对“动态规划”的理解还太浅了。但是又感觉这似乎不是“穷举”,因为我并没有把从根节点到末尾节点的路线全部列举一遍(至少从程序上我不会),我是“一步一步”计算的路线,并且我也把大数塔拆成来小数塔解决,相当于“把大问题拆分成相似的子问题”。从这些特征来看,这似乎也不算是“太笨的方法”,怎么会是“穷举”呢?( ̄▽ ̄)"

再根据这个思路写写程序,发现我居然可以用递归!?这么一来似乎就更自信了。因为之前曾经看过,“递归”是“动态规划”过程中使用的一种手段(当然,这句话也不准确。。。后面也会纠正它),这些特征表面,我使用的方法就算是“动态规划”了吧?

怀着将信将疑的态度,试着去查找了别人的文章,发现想了那么多层,最后自己还是没绕出“穷举”这个坑。/(ㄒoㄒ)/~~

4)分析纠正

前面说了几点“动态规划”特征的个人理解,很遗憾的是,那几点理解都不是严格地准确。这里我们来分析、纠正一下它们:

<1> 一个子问题如果属于一个大问题的一部分,就可以利用子问题的结果信息去计算大问题,避免重复计算

其实这并算是“动态规划”独有的特征。算法优化的本质就是为了“避免重复计算”。那么问题来了:都是在“避免重复计算”,不同的方法又有什么不同?它们避免重复计算的部分不一样吗?

而且,不是只有动态规划问题才能拆成子问题,例如贪心类型问题,把一段长的路拆成一步步的小路,把整体最优拆成计算局部最优,不也是可以大问题拆成小问题么?

<2>“递归”是“动态规划”过程中使用的一种手段

同理,并不是只有动态规划才会用到递归。“递归”说来,实际上是属于编程层面的东西,“函数自己调用自己”,是一种代码文字的书写、组织、表达方式,一种手段。而动态规划是一种思想,是可以脱离编程、程序而存在的,你可以在自己脑海中去运行这种思想。

2.动态规划
1)理论分析

正经的动态规划方法我参考学习了这篇文章:几个经典的动态规划问题

事实上,动态规划方法最重要的,是找到一个当前状态与前一状态对应的关系。因为在动态规划结构的问题中,当前状态会依赖于上一个状态(或认为是,当前的“可选”与之前的“选择”有关)。

如果拿数塔来说的话,就是我这一层能够选择哪些岔路,跟我之前走过、选择的路是有关系的。比如下图,我走了第一层的“9”,然后走了第二层的“15”,到了第三层我就只能走“6”、“8”这两种选择了,而不可能走“10”这个节点。这就说明我第三层的可选项跟我第一、二层的选择是有关系的。
在这里插入图片描述
如果是贪心结构的问题,比如下图,那当前的选择,就跟我之前的选择没有什么关系。比如我走到第二层,将要考虑如何选择第三层的路,不管我现在是处于H还是I,我的可选项都是一样(都是P、Q),我可以走到第三层的任意节点,因为H、I与第三层所有节点都是通的。每一步都随你瞎走,反正最后给你的选项都是一样的,至于路线是不是最优,那就是另说了。
在这里插入图片描述
如果把这种“可选项”理解成一种状态的话,那么动态规划结构的问题,每一层都是有很多种状态的,且不同的状态取决于上一层是的哪种状态。而贪心结构的问题,每一层的状态都只有一种。

明白了动态规划问题存在着“状态”,并且状态与上个状态是存在着某种关系之后,我们去找出这种关系,将这种关系总结、归纳成“状态转换方程”,然后就是去编程实现了。动态规划问题的基本思路就是如此(当然这还是“正确的废话”,就算告诉你要去找状态转换方程,你也未必就能找出๑乛◡乛๑)

直接说吧,数塔问题的状态转换方程是这个:

dp(i, j) = Max( dp(i+1, j), dp(i+1, j+1 ) )+ f [ i ][ j ]

还需要把它翻译成人话:

为了便于说明,咱们先约定一下节点的表示方法吧:[ i, j ] ,它指的是第 i 层的第 j 个节点。比如下图[ 1, 1 ] 节点就是第1层、第1个节点,即“9”。

在这里插入图片描述
dp(i, j)函数的值,是[ i, j ] 这个节点,到最底层之后,所有可能路线的节点数值之和的最大值。还拿上图来说,比如dp(5, 1)其实就是[5, 1]这个节点(第5层、第1个)到最底层(第5层,也是它自己所在这一层)所有可能路线只有一条,即它自身“19”,所以dp[5, 1] = 19。

又比如dp(4, 2),即[4, 2]这个节点,它到最底层(还是第5层)的所有路线有两条:7 -> 18、10 -> 18。数值和分别是25、28,28更大,所以最大值为28。因此dp(4, 2) = 28。

所有情况依次类推。

然后我们来解释一下这个状态转移方程式在说什么吧,dp(i, j) = Max( dp(i+1, j), dp(i+1, j+1 ) )+ f [ i ][ j ],意思是dp(i , j)这个的值,等于dp(i+1, j)、dp(i+1, j+1 ) 两者之间取大的,然后再加上 f [ i ][ j ]这个。

f [ i ][ j ]的含义是节点[i, j]的数值,比如上图[1, 1]节点,值是9,所以f[1][1 ]= 9。

再翻译得更通俗,就是你想要知道[i, j]这个节点到最底层的最大数值和,首先要知道[i+1, j]和[i+1, j+1]这两节点到最底层的最大数值和。知道之后,我们取大的,然后加上[i, j]节点的值,即f[i][j],就是我们要求的结果。

我们要求第1层、第1个节点到最底层的最大数值和,本质上就是要求dp(1, 1)。

2)实际操作

即使把公式翻译得通俗了,可能依然很绕口。我们就直接拿题目来说,一步步地来说明我们每一步怎么做。

在这里插入图片描述
想要知道根节点“9”到最底层的最大数值和,首先得知道第二层的“12”和“15”到最底层的最大数值和。我假设它们分别是M和N,假如“15”这条线路的数值和更大,即Max(M, N) = N,那么“9”到最底层的最大数值和,就是N + 9, 即下一层级的数值和 加上它自己。

然后我们反推一步,按照第一步的类似原理,我们要知道“12”和“15”到最底层的最大数值和,就得知道第三层的“10”、”6“、”8“到最底层的最大数值和。

其中,连接“12”这个节点的是“10”、“6”。我假设这两条支线的数值和最大分别是O、P,假如”10“这条线路的更大,即Max(O、P) = O,那么“12”这条线到最底层的最大数值和就是 O + 12。

其中,连接“15”这个节点的是“6”、“8”。我假设这两条支线的数值和最大分别是P、Q(注意到,P就是上段话里的那个P,因为节点都是“6”,所以这个值是一样的),假如”6“这条线路的更大,即Max(P,Q) = P,那么“15”这条线到最底层的最大数值和就是 P + 12。

然后我们继续,往下一层推理、不断推理…

最后推理到了最底层,就没法继续往下推了,已经到了一个结束点。此时节点到最底层的数值和最大值,就是它自身。比如“19”,它到最底层线路的数值和最大就是它自己——19了。

现在我们明白了,要想知道第一层的信息,就必须知道第二层。 要想知道第二层的信息,又必须知道第三层…依次类推,只有最底层的信息,我们不需要由别的层来推导,而是可以直接得出。因此,最底层就是一个计算的起始点。

若从实现来说,确实也需要用到递归。最底层既是计算的起始点,同时又是递归的终止点、回溯点。
在这里插入图片描述
在这里插入图片描述

实现

说了那么多,我们就用程序来实现、验证吧。

1)数塔生成器

先来实现一个数塔生成器吧:

function createNumberTower (level) {
    let tower = new Array();
    for (let i = 1; i < level + 1 ; i++) {
        let arr = new Array(i)
        for (let j = 0; j < arr.length; j++) {
            arr[j] = parseInt(Math.random() * 20)
        }
        tower.push(arr)
    }
    return tower;
}

实际上就是生成一个二维数组,只是第二维的长度变化是有规律的,依次是1、2、3、4…这样自增。例如题目图里的数塔用数组表示就是[[9], [12, 15], [10, 6, 8], [2, 18, 9, 5], [19, 7, 10, 4, 16]]

参数是level,即要生成的数塔的层数。

这里我把每个节点设置为0~20的随机数,要想生成别的数,可以自行修改。

arr[j] = parseInt(Math.random() * 20)

2) 穷举法

虽然这个方法的效率低,但是既然已经思考出来了,那还是把它也一块实现了吧。穷举法的结果一般比较准确,正好可以用来验证我们的动态规划法的结果是否正确。

function findPath(tower) {
    let level = tower.length - 1;
    let prevPaths = tower.length === 2 ? [{path: [0], value: tower[0][0]}] : findPath(tower.slice(0, level))
    let newPaths = new Array();
    for (let path of prevPaths) {
        let lastNode = path.path[path.path.length - 1];
        newPaths.push({ 
            path: Array.from([...path.path,lastNode]),
            value: path.value + tower[level][lastNode]
        })
        newPaths.push({ 
            path: Array.from([...path.path,lastNode + 1]),
            value: path.value + tower[level][lastNode + 1]
        })

    }
    return newPaths;
}

结果将是一个数组,数组的每个元素是一条路径。里面包含两个字段信息:path、value。

path也是一个数组,用来表示路径每一层的节点索引。那题目图的数塔举例子,例如某条路径的path值是[0, 1, 2, 3, 4],那就说明第一层我走的是第1个元素(个数 = 索引 + 1嘛),第二层走的也是第2个元素,第三层走的是第3个元素,第四层走的是第4个元素,第五层走的是第5个元素。即9 -> 15 -> 8 -> 5 -> 16这一条路线。

value就是这一条路线所有节点的数值之和了。穷举了所用路线之后,按照value由大到小排序,就能知道哪一条路线数值之和最大了。

3)动态规划法

出乎意料,动态规划法的代码极其简单优雅,不过数行。

let fullTower = createNumberTower(30); // 生成数塔
function dp(i, j) {
    if (i === fullTower.length - 1) { // 起始点
        return fullTower[i][j];
    } else {
        return Math.max(dp(i+1, j), dp(i+1, j+1)) + fullTower[i][j];
    }
}

注意:为了编程方便,i,j 我用的是数组索引,即初始值是0。在这里dp(0 , 0)表示的才是对第1层第1个节点求解,而不是上文分析说明里的dp(1, 1)了。

4)验证

我们来验证一下,先把原题目求解一下。这里就不需要我们的数塔生成器了,直接把题目数塔写成一个二维数组

let fullTower = [[9], [12, 15], [10, 6, 8], [2, 18, 9, 5], [19, 7, 10, 4, 16]]

然后是穷举法:

findPath(fullTower).sort((prev, next) => next.value - prev.value)
输出:
 [{ path: [ 0, 0, 0, 1, 2 ], value: 59 },
  { path: [ 0, 1, 1, 1, 2 ], value: 58 },
  { path: [ 0, 0, 0, 1, 1 ], value: 56 },
  { path: [ 0, 0, 1, 1, 2 ], value: 55 },
  { path: [ 0, 1, 1, 1, 1 ], value: 55 },
  { path: [ 0, 1, 2, 3, 4 ], value: 53 },
  { path: [ 0, 0, 0, 0, 0 ], value: 52 },
  { path: [ 0, 0, 1, 1, 1 ], value: 52 },
  { path: [ 0, 1, 2, 2, 2 ], value: 51 },
  { path: [ 0, 1, 1, 2, 2 ], value: 49 },
  { path: [ 0, 0, 1, 2, 2 ], value: 46 },
  { path: [ 0, 1, 2, 2, 3 ], value: 45 },
  { path: [ 0, 1, 1, 2, 3 ], value: 43 },
  { path: [ 0, 1, 2, 3, 3 ], value: 41 },
  { path: [ 0, 0, 0, 0, 1 ], value: 40 },
  { path: [ 0, 0, 1, 2, 3 ], value: 40 }]

可见,数值和最大的路径是[ 0, 0, 0, 1, 2 ]这种路线,也就是数塔图的9 - > 12 -> 10 -> 18 -> 10这种走法。该路线的和为59。

我们用动态规划来验证一下:

dp(0, 0)
输出:59

对比一下,发现与穷举法结果一致。证明我们的动态规划函数也是对的。为了简单性,这个dp函数里,我就不像穷举法一样,去列出具体的路径节点了。

我们再用数塔函数,随意生成一个数塔来试试。来生成一个层数更多的数塔吧,比如10层的:

let fullTower = createNumberTower(10);
输出:[
  [ 16 ],
  [ 13, 0 ],
  [ 2, 14, 1 ],
  [ 10, 5, 17, 15 ],     
  [ 14, 9, 10, 14, 16 ], 
  [ 1, 7, 2, 18, 5, 19 ],
  [
    11,  0, 8, 5,        
     4, 11, 5
  ],
  [
    13, 13, 11,  3,
     2,  3,  9, 16
  ],
  [
     3, 18,  3, 17, 13,
    11, 18, 10, 18
  ],
  [
    17,  2, 9,  7, 8,
     2, 13, 8, 11, 5
  ],
  [
     4, 5, 15,  0,  6,
     9, 0, 12, 14, 19,
    19
  ],
  [
     7,  3, 17,  3, 3,
    17, 13, 10, 11, 6,
     5,  2
  ],
  [
    14,  0, 5, 2, 9, 17,
    10, 18, 6, 4, 6,  4,
    19
  ],
  [
    18,  9, 5, 12, 15,  8,
    13,  0, 3,  2, 14, 17,
    11, 15
  ],
  [
     1, 6,  4, 18,  8, 3,
     0, 1, 12, 12, 15, 6,
    11, 7,  4
  ],
  [
     7,  5,  4,  0,  1, 19,
    10,  6, 18,  5, 16, 12,
     2, 13, 10, 11
  ],
  [
     1, 19, 11,  0, 15, 4, 15,
    18,  0,  9, 14, 18, 4, 11,
     1,  5,  7
  ],
  [
    10, 8,  0,  8,  5, 11,  2,
     1, 5, 16, 19, 16, 15, 14,
    14, 4, 16, 15
  ],
  [
    3, 19,  2,  4, 10,  2, 16,
    1, 12, 10,  6,  5, 16, 15,
    8,  7, 18, 10, 15
  ],
  [
    2,  1, 10,  4, 12, 19, 11,
    7, 16, 13,  0, 18, 14, 17,
    0,  1, 13, 15,  5,  5
  ],
  [
    16,  3,  1,  0,  1,  2,  5,
     4,  8, 10, 17,  2, 18, 19,
     6, 12,  8, 13, 10,  6, 16
  ],
  [
     4, 15,  9,  6, 10, 17, 2,
    11,  7, 17, 16,  2,  5, 2,
    10, 15,  9, 15,  0, 10, 7,
     8
  ],
  [
    18, 18, 11, 11,  4,  1, 4, 13,
    18, 13,  7, 18, 10, 15, 7,  5,
    12, 18, 10, 15, 12,  0, 9
  ],
  [
     8,  8, 11, 18,  6,  4, 17, 15,
     7,  2,  7, 12, 12, 11,  9,  7,
    17, 15,  5,  3, 12, 10,  5,  7
  ],
  [
    14,  2,  5, 16,  1,  6,  7,  6,
    10, 15,  6,  1,  6,  3, 16,  4,
     8,  0, 16,  5, 11, 10,  9, 16,
     1
  ],
  [
    13,  0, 13,  3, 19, 11, 3, 9,
     5,  5, 11, 11, 15,  7, 9, 3,
    11,  2,  0,  2, 11, 19, 5, 1,
    18, 12
  ],
  [
    10, 10, 16, 12, 14,  3, 19, 11,
     9,  0, 10,  6,  7,  0,  6, 14,
     5,  1, 11, 10,  6, 13,  5,  5,
     3, 14,  5
  ],
  [
     3,  5,  4, 14, 13, 15,  1,  8,
    18, 14,  5, 16, 11,  5, 10,  6,
    15, 10, 10,  6
  ],
  [
    10, 18,  7, 17,  1, 10, 17,  9, 3,
    17, 16, 10,  3,  1, 17,  1, 12, 6,
     9, 19, 13, 18, 14,  0,  5,  5, 0,
    19, 17
  ],
  [
     0, 17, 18, 16,  2,  5,  9, 13, 15,
     0, 18, 13,  8,  0,  0, 15, 16,  4,
    12, 10, 12,  8, 15, 16,  2,  9,  1,
     4,  7, 15
  ]
]

穷举法的结果是:

findPath(fullTower).sort((prev, next) => next.value - prev.value)
输出:[
  {
    path: [
      0, 1, 2, 3, 4,
      4, 5, 6, 7, 8
    ],
    value: 126
  },
  {
    path: [
      0, 1, 1, 1, 2,
      3, 3, 4, 5, 5
    ],
    value: 123
  },
  {
    path: [
      0, 1, 1, 2, 3,
      4, 5, 6, 7, 8
    ],
    value: 122
  } ....]

动态规划法给出的结果是:

dp(0, 0)
输出:126

可见也是一致的。多次尝试,结果都是一致,这样就验证了我们想法的正确性。

  • 24
    点赞
  • 86
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
Python数塔问题是一个经典的动态规划问题,主要是求解从顶部开始,通过相邻节点的选择,找到一条路径使得路径上的节点和最小。 通过引用的代码可以看出,这段代码使用了自底向上的动态规划方法来解决数塔问题。首先,初始化一个mini列表,将其设置为数塔的最后一行节点的值。然后,从倒数第二行开始,通过一个循环遍历每一行的节点。在每个节点上,选择该节点和其相邻节点的较小值,将其更新到mini列表中。最后,返回mini列表的第一个元素,即为最小路径和。 而引用的代码是使用递归的方法来解决数塔问题。在这段代码中,traverse函数接受两个参数i和j,表示当前节点的位置。如果当前节点是数塔的最后一行,即i大于等于总行数-1,那么返回0。否则,通过递归调用traverse函数,分别计算左下节点和右下节点的路径和,并返回较小值。最后,将当前节点的值加上较小路径和,即为最小路径和。 综上所述,以上两种方法都可以用来解决Python数塔问题,其中动态规划方法更为高效,递归方法则更为简洁。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [python解决数塔问题(递归,剪枝,DP)](https://blog.csdn.net/qq_45268474/article/details/108072826)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值