文章目录

  • 1 题目
  • 2 解决方案
    • 2.1 思路和图解
      • 2.1.1 遍历法和分治法
      • 2.1.2 带记忆化搜索的分治法
      • 2.1.3 至底向上的动态规划
      • 2.1.4 至顶向下的动态规划
    • 2.3 时间复杂度
      • 2.3.1 以树高h为基准计算
      • 2.3.2 以节点数n为基准计算
    • 2.4 空间复杂度
  • 3 源码
    • 3.1 遍历法
    • 3.2 分治法
    • 3.3 动态规划——带记忆化搜索的分治法
    • 3.4 动态规划——至底向上
    • 3.5 动态规划——至顶向下

1 题目

题目:数字三角形(Triangle)
描述:给定一个数字三角形,找到从顶部到底部的最小路径和。每一步可以移动到下面一行的相邻数字上。

lintcode题号——109,难度——medium

样例1:

输入:
triangle = [[2],[3,4],[6,5,7],[4,1,8,3]
]
输出:11
解释:从顶到底部的最小路径和为11 ( 2 + 3 + 5 + 1 = 11)。

样例2:

输入:
triangle = [[2],[3,2],[6,5,7],[4,4,8,1]
]
输出:12
解释:从顶到底部的最小路径和为12 ( 2 + 2 + 7 + 1 = 12)。

2 解决方案

2.1 思路和图解

2.1.1 遍历法和分治法

  这题按照常规思路,可以使用遍历法或者分治法来做.
  使用遍历法的方式,从顶点往下走,每次向左下或者右下的节点移动,走到最底层时判断路径和是否最小.
  使用分治法的方式,可以将当前节点到最底层的最小路径和进行拆分,转化为对左下节点与右下节点中取较小的路径和的问题。

但是如果按照遍历或者分治来解这一题,提交时对数据量很大的样例会提示代码运行超时。所以需要考虑优化。

#mermaid-svg-EXgW9027PSYluBmZ {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-EXgW9027PSYluBmZ .error-icon{fill:#552222;}#mermaid-svg-EXgW9027PSYluBmZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EXgW9027PSYluBmZ .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-EXgW9027PSYluBmZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EXgW9027PSYluBmZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EXgW9027PSYluBmZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EXgW9027PSYluBmZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EXgW9027PSYluBmZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EXgW9027PSYluBmZ .marker.cross{stroke:#333333;}#mermaid-svg-EXgW9027PSYluBmZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EXgW9027PSYluBmZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EXgW9027PSYluBmZ .cluster-label text{fill:#333;}#mermaid-svg-EXgW9027PSYluBmZ .cluster-label span{color:#333;}#mermaid-svg-EXgW9027PSYluBmZ .label text,#mermaid-svg-EXgW9027PSYluBmZ span{fill:#333;color:#333;}#mermaid-svg-EXgW9027PSYluBmZ .node rect,#mermaid-svg-EXgW9027PSYluBmZ .node circle,#mermaid-svg-EXgW9027PSYluBmZ .node ellipse,#mermaid-svg-EXgW9027PSYluBmZ .node polygon,#mermaid-svg-EXgW9027PSYluBmZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EXgW9027PSYluBmZ .node .label{text-align:center;}#mermaid-svg-EXgW9027PSYluBmZ .node.clickable{cursor:pointer;}#mermaid-svg-EXgW9027PSYluBmZ .arrowheadPath{fill:#333333;}#mermaid-svg-EXgW9027PSYluBmZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EXgW9027PSYluBmZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EXgW9027PSYluBmZ .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-EXgW9027PSYluBmZ .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-EXgW9027PSYluBmZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EXgW9027PSYluBmZ .cluster text{fill:#333;}#mermaid-svg-EXgW9027PSYluBmZ .cluster span{color:#333;}#mermaid-svg-EXgW9027PSYluBmZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EXgW9027PSYluBmZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

二叉树
数字三角形
2
1
3
4
5
6
7
8
9
10
11
12
13
14
15
2
1
3
4
5
6
7
8
9
10

  我们使用遍历和分治的场景一般能够对应一颗二叉树,分析二叉树的图与该题的数字三角形的图,它们很相似,但区别在于二叉树中到达每个节点的路径是唯一的,但数字三角形中的分支节点是被共用的,到达一个节点的路径可以从左肩或者右肩而来,并不唯一。
  换个说法,就是我们使用遍历法来处理数字三角形,在某个节点向下走的时候,它的右下节点可能会被它右边同层的节点遍历第二次,这样就造成了重复访问,且每次访问该节点都需要向下再次遍历该节点的所有子分支来计算该节点的最优解,所以这种重复计算的情况对越底层的节点会出现越多次。这就造成了该题的解法还有优化的空间。

2.1.2 带记忆化搜索的分治法

  为了不进行重复计算,我们会考虑在第一次访问节点后,将计算出的最优值保存起来,下次从不同路径访问该节点时,就不用再遍历该节点的子分支来重新计算一遍最优值了。
  使用带记忆化搜索的分治法的方式,可以避免常规分治法中对节点最优值的重复计算。

#mermaid-svg-VUdPnbtRpkF7VCOK {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-VUdPnbtRpkF7VCOK .error-icon{fill:#552222;}#mermaid-svg-VUdPnbtRpkF7VCOK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VUdPnbtRpkF7VCOK .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-VUdPnbtRpkF7VCOK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VUdPnbtRpkF7VCOK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VUdPnbtRpkF7VCOK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VUdPnbtRpkF7VCOK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VUdPnbtRpkF7VCOK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VUdPnbtRpkF7VCOK .marker.cross{stroke:#333333;}#mermaid-svg-VUdPnbtRpkF7VCOK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VUdPnbtRpkF7VCOK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VUdPnbtRpkF7VCOK .cluster-label text{fill:#333;}#mermaid-svg-VUdPnbtRpkF7VCOK .cluster-label span{color:#333;}#mermaid-svg-VUdPnbtRpkF7VCOK .label text,#mermaid-svg-VUdPnbtRpkF7VCOK span{fill:#333;color:#333;}#mermaid-svg-VUdPnbtRpkF7VCOK .node rect,#mermaid-svg-VUdPnbtRpkF7VCOK .node circle,#mermaid-svg-VUdPnbtRpkF7VCOK .node ellipse,#mermaid-svg-VUdPnbtRpkF7VCOK .node polygon,#mermaid-svg-VUdPnbtRpkF7VCOK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VUdPnbtRpkF7VCOK .node .label{text-align:center;}#mermaid-svg-VUdPnbtRpkF7VCOK .node.clickable{cursor:pointer;}#mermaid-svg-VUdPnbtRpkF7VCOK .arrowheadPath{fill:#333333;}#mermaid-svg-VUdPnbtRpkF7VCOK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VUdPnbtRpkF7VCOK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VUdPnbtRpkF7VCOK .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-VUdPnbtRpkF7VCOK .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-VUdPnbtRpkF7VCOK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VUdPnbtRpkF7VCOK .cluster text{fill:#333;}#mermaid-svg-VUdPnbtRpkF7VCOK .cluster span{color:#333;}#mermaid-svg-VUdPnbtRpkF7VCOK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VUdPnbtRpkF7VCOK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

数字三角形
step1
step2
step3
step4
step5
step6
2: Val=NULL
1: Val=NULL
3: Val=NULL
4: Val=11
5: Val=NULL
6: Val=NULL
7: Val=7
8: Val=8
9: Val=NULL
10: Val=NULL

上图对数字三角形(假设节点值就等于其节点序号)进行分治法,要计算节点1的最优值,先计算左右子分支,即节点2、3,先计算左子分支,这样一直向下,就经过1->2->4->7->4->8->4,回到节点4之后,就记录下了节点4的当前最优解为11,即从4->7到底层比从4->8到底层的路径和要小,是当前的最优解。依次计算出所有节点的最优解,顶点的最优解即是答案。
关于重复计算的问题,以节点5为例,我们第一次访问节点5(从1->2->5),需要继续向下计算完节点8和9才能得到节点5的最优解,此时对该值进行记录,等第二次访问节点5(从1->3->5),则不需要向下计算子分支,直接取之前保存的最优解即可。这样就避免了重复计算。

  其实带记忆化搜索的分治法已经是动态规划中的一种了,动态规划的解决方案分为两种形式,一种是带记忆化搜索的递归,另一种是多重循环。

比较标准的动态规划一般是用多重循环来实现的,在代码中可以看出差别。而多重循环的动态规划也有两种分类,一种是至底向上的方式,另一种是至顶向下的方式。

2.1.3 至底向上的动态规划

  对动态规划的分析有四个要素,状态、方程、初始化、答案。

动态规划的四要素对应递归的三要素:
动态规划中的状态——递归中的定义
动态规划中的方程——递归中的拆解
动态规划中的初始化——递归中的出口
动态规划多了一个要素,即答案

#mermaid-svg-6X0WL51eH1EVig6f {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-6X0WL51eH1EVig6f .error-icon{fill:#552222;}#mermaid-svg-6X0WL51eH1EVig6f .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6X0WL51eH1EVig6f .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-6X0WL51eH1EVig6f .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6X0WL51eH1EVig6f .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6X0WL51eH1EVig6f .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6X0WL51eH1EVig6f .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6X0WL51eH1EVig6f .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6X0WL51eH1EVig6f .marker.cross{stroke:#333333;}#mermaid-svg-6X0WL51eH1EVig6f svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6X0WL51eH1EVig6f .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6X0WL51eH1EVig6f .cluster-label text{fill:#333;}#mermaid-svg-6X0WL51eH1EVig6f .cluster-label span{color:#333;}#mermaid-svg-6X0WL51eH1EVig6f .label text,#mermaid-svg-6X0WL51eH1EVig6f span{fill:#333;color:#333;}#mermaid-svg-6X0WL51eH1EVig6f .node rect,#mermaid-svg-6X0WL51eH1EVig6f .node circle,#mermaid-svg-6X0WL51eH1EVig6f .node ellipse,#mermaid-svg-6X0WL51eH1EVig6f .node polygon,#mermaid-svg-6X0WL51eH1EVig6f .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6X0WL51eH1EVig6f .node .label{text-align:center;}#mermaid-svg-6X0WL51eH1EVig6f .node.clickable{cursor:pointer;}#mermaid-svg-6X0WL51eH1EVig6f .arrowheadPath{fill:#333333;}#mermaid-svg-6X0WL51eH1EVig6f .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6X0WL51eH1EVig6f .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6X0WL51eH1EVig6f .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-6X0WL51eH1EVig6f .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-6X0WL51eH1EVig6f .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6X0WL51eH1EVig6f .cluster text{fill:#333;}#mermaid-svg-6X0WL51eH1EVig6f .cluster span{color:#333;}#mermaid-svg-6X0WL51eH1EVig6f div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6X0WL51eH1EVig6f :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

至底向上的动态规划
2
1
3
4
5
6
7
8
9
10
状态: State[n]——表示节点n到最底层的路径和的最小值
方程: State[n] = Value[n] + min(State[n的左子节点], State[n的右子节点])
初始化: State[底层节点] = Value[底层节点]
答案: State[顶点节点]

  首先解释状态,状态指的是在一个大问题中的某个阶段的当前小规模问题的解,可以看成递归中的定义。在示例中,我们将状态定义为当前节点的最优解(从当前节点到最底层的最小路径和),上图中的节点5的状态即表示从节点5到最底层的最小路径和,记做State[n]
  再看方程,方程指的是如何将大状态用小状态来表示,可以看成递归中的拆解。示例中,State[节点5] = Value[节点5] + min(State[节点8], State[节点9]),节点5的最优解可以拆解为节点8和节点9的最优解中较小的一个加上节点5自身的值。
  还需要确定初始化状态,初始化状态表示动态规划的起点,对应的是递归中的出口,有了初始化状态才能以此为基础进行至底向上的求解,是动态规划求解问题的极限小的状态。示例中,要求解某个节点的最优解,必须得不断向下拆解,直到最底层的元素,所以这里的极限小的状态就是最底层元素的最优解,它们的最优解即为自身节点值,State[底层节点] = Value[底层节点]
  最后需要确定要得到的最终答案,答案指的是动态规划求解问题的极限大的状态。示例中,我们要求解的是从顶点的最优解,所以答案 = State[顶点节点]

状态:存储当前小规模问题的结果。
方程:通过小状态来表示大状态的式子。
初始化:最极限的小状态。
答案:要得到的最大状态。

  把四要素都理清了之后,写出的代码就很清晰了。参看3.4节。

2.1.4 至顶向下的动态规划

  对至顶向下的动态规划而言,其形式是一样的,只是对应的四要素的定义有所不同。
  首先状态,在示例中,将状态同样定义为当前节点的最优解,但是是从顶点到当前节点的最小路径和(注意与至底向上的定义区分开),下图中的节点5的状态即表示从顶点1到节点5的最小路径和,记做State[n]
  再看方程,示例中,State[节点5] = Value[节点5] + min(State[节点2], State[节点3]),节点5的最优解可以拆解为节点2和节点3的最优解中较小的一个加上节点5自身的值。
  确定初始化状态,示例中,要求解某个节点的最优解,必须从左、右父节点向下计算,所以这里的极限小的状态就是三角形两条腰上节点的最优解,它们的最优解需要从顶点累加而来,两腰上的节点都只有唯一的父节点,State[三角形两腰上的节点] = Value[节点自身] + State[唯一父节点]
  最后需要确定要得到的最终答案,示例中,至顶向下计算,每个底层节点都会得到一个最优解,我们需要再次进行比较,得到最终答案,所以答案 = min(State[所有底层节点])

#mermaid-svg-8LFxoA75QJqeEmlm {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-8LFxoA75QJqeEmlm .error-icon{fill:#552222;}#mermaid-svg-8LFxoA75QJqeEmlm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8LFxoA75QJqeEmlm .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-8LFxoA75QJqeEmlm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8LFxoA75QJqeEmlm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8LFxoA75QJqeEmlm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8LFxoA75QJqeEmlm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8LFxoA75QJqeEmlm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8LFxoA75QJqeEmlm .marker.cross{stroke:#333333;}#mermaid-svg-8LFxoA75QJqeEmlm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8LFxoA75QJqeEmlm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8LFxoA75QJqeEmlm .cluster-label text{fill:#333;}#mermaid-svg-8LFxoA75QJqeEmlm .cluster-label span{color:#333;}#mermaid-svg-8LFxoA75QJqeEmlm .label text,#mermaid-svg-8LFxoA75QJqeEmlm span{fill:#333;color:#333;}#mermaid-svg-8LFxoA75QJqeEmlm .node rect,#mermaid-svg-8LFxoA75QJqeEmlm .node circle,#mermaid-svg-8LFxoA75QJqeEmlm .node ellipse,#mermaid-svg-8LFxoA75QJqeEmlm .node polygon,#mermaid-svg-8LFxoA75QJqeEmlm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8LFxoA75QJqeEmlm .node .label{text-align:center;}#mermaid-svg-8LFxoA75QJqeEmlm .node.clickable{cursor:pointer;}#mermaid-svg-8LFxoA75QJqeEmlm .arrowheadPath{fill:#333333;}#mermaid-svg-8LFxoA75QJqeEmlm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8LFxoA75QJqeEmlm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8LFxoA75QJqeEmlm .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-8LFxoA75QJqeEmlm .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-8LFxoA75QJqeEmlm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8LFxoA75QJqeEmlm .cluster text{fill:#333;}#mermaid-svg-8LFxoA75QJqeEmlm .cluster span{color:#333;}#mermaid-svg-8LFxoA75QJqeEmlm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8LFxoA75QJqeEmlm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

至底向上的动态规划
2
1
3
4
5
6
7
8
9
10
状态: State[n]——表示从顶点1到节点n的路径和的最小值
方程: State[n] = Value[n] + min(State[n的左父节点], State[n的右父节点])
初始化: State[三角形两腰上的节点] = Value[节点自身] + State[唯一父节点]
答案: min(State[所有底层节点])

  至顶向下与至底向上只是对状态的定义的不同,都需要把四要素理清,对应的代码参看3.5节。

2.3 时间复杂度

2.3.1 以树高h为基准计算

  如果使用树高h为基准,可以更直观地对比遍历、分治与动态规划:

对于高为h的数字三角形,节点数的数量级是h的平方。
节点数 = 1 + 2 + 3 + …… + (h + 1)= h * (1 + (h + 1)) / 2= (h^2 + 2) / 2对于高为h的二叉树,节点数的数量级为2的h次方。
节点数 = 1 + 2 + 4 + …… + 2^(h - 1)= 1 * (1 - 2^h) / (1 - 2)= 2^h - 1

  使用遍历法和分治法,每次访问一个节点后,有左右两条路径要走,节点数为h^2,时间复杂度为O(2^(h^2))

重复访问节点导致的耗时为指数级增长,

  使用动态规划的方式,每个节点只计算一次,后续可以直接使用计算值,没有对子节点的重复访问和计算,节点数为h^2,时间复杂度为O(h^2)。

2.3.2 以节点数n为基准计算

  使用遍历法和分治法,每次访问一个节点后,有左右两条路径要走,节点数为n,时间复杂度为O(2^n)
  使用动态规划的方式,每个节点只计算一次,后续可以直接使用计算值,没有对子节点的重复访问和计算,节点数为n,时间复杂度为O(n)。

2.4 空间复杂度

  使用遍历法和分治法,只使用常数级的额外空间,空间复杂度为O(1)。
  使用动态规划——带记忆化搜索的分治法,使用了容量为节点数n的容器用于记录当前节点最优值,空间复杂度为O(n)。
  使用多重循环的动态规划,使用了容量为节点数n的容器用于记录当前节点最优值,空间复杂度为O(n)。

3 源码

3.1 遍历法

细节:

  1. 递归的三要素:定义、拆解、出口。
  2. 遍历法走遍所有的节点,并在最底层节点计算出路径和。

该题使用遍历法在Lintcode上Submit的时候并不能获得完全Accept,因为时间复杂度高,在数据量大的时候会Time Limit Exceeded。这里提供代码只做为参考。

C++版本:

/**
* @param triangle: a list of lists of integers
* @return: An integer, minimum path sum
*/
int minimumTotal(vector<vector<int>> &triangle) {// write your code hereint result = INT_MAX;if (triangle.empty() || triangle.front().empty()){return 0;}traverse(triangle, 0, 0, 0, result);return result;
}// 递归的定义:获取第row行、第col列的值,并与当前的最小值打擂台
void traverse(vector<vector<int>> & triangle, int row, int col, int sum, int & result)
{if (row == triangle.size()) // 递归的出口{return;}sum += triangle.at(row).at(col);if (row == triangle.size() - 1 && sum < result) // 在最后一行计算出来的路径和,与当前最小值比较{result = sum;}// 递归的拆解traverse(triangle, row + 1, col, sum, result); // 向左便利traverse(triangle, row + 1, col + 1, sum, result); // 向右便利
}

3.2 分治法

细节:

  1. 递归的三要素:定义、拆解、出口。
  2. 分治法将子问题分给左右分支去处理,与遍历法最大的区别是,分治法有返回值。

该题使用传统的分治法在Lintcode上Submit的时候并不能获得完全Accept,因为时间复杂度高,在数据量大的时候会Time Limit Exceeded。为了顺利获得Accept,需要对分治法进行改进,添加记忆化搜索,下一小节贴上代码。

C++版本:

/*** @param triangle: a list of lists of integers* @return: An integer, minimum path sum*/
int minimumTotal(vector<vector<int>> &triangle) {// write your code hereint result = INT_MAX;if (triangle.empty() || triangle.front().empty()){return 0;}result = divideAndConquer(triangle, 0, 0);return result;
}// 递归的定义:计算以第row行、第col列的节点为起点,向下的所有路径和之中最小的值,并返回
int divideAndConquer(vector<vector<int>> & triangle, int row, int col)
{if (row == triangle.size()) // 递归的出口{return 0;}int val = triangle.at(row).at(col);// 递归的拆解int leftResult = divideAndConquer(triangle, row + 1, col); // 计算左支节点向下的最小值int rightResult = divideAndConquer(triangle, row + 1, col + 1); // 计算右支节点向下的最小值return val + min(leftResult, rightResult);
}

3.3 动态规划——带记忆化搜索的分治法

细节:

  1. 与传统的分治法相比,添加了一个容器用于记忆化搜索。
  2. 每到一个节点,计算出结果之后,保存到容器中进行记录,下一次再访问该节点的时候,直接从容器中取值即可,避免了重复计算。

带记忆化搜索的分治法已经是动态规划的一种了,去除了传统分治法中的重复计算过程。

C++版本:

/**
* @param triangle: a list of lists of integers
* @return: An integer, minimum path sum
*/
int minimumTotal(vector<vector<int>> &triangle) {// write your code hereint result = INT_MAX;if (triangle.empty() || triangle.front().empty()){return 0;}// 使用一个与triangle容量相同的容器来记录各个节点的最优值vector<vector<int>> sum;for (auto it : triangle){sum.push_back(vector<int>(it.size(), INT_MAX));}result = divideAndConquer(triangle, sum, 0, 0);return result;
}// 递归的定义:计算以第row行、第col列的节点为起点,向下的所有路径和之中最小的值,并返回
int divideAndConquer(vector<vector<int>> & triangle, vector<vector<int>> & sum, int row, int col)
{if (row == triangle.size()) // 递归的出口{return 0;}if (sum.at(row).at(col) != INT_MAX){return sum.at(row).at(col); // 如果当前位置已经有记录值,则不用重复计算,直接返回记录值}int val = triangle.at(row).at(col);// 递归的拆解int leftResult = divideAndConquer(triangle, sum, row + 1, col); // 计算左支节点向下的最小值int rightResult = divideAndConquer(triangle, sum, row + 1, col + 1); // 计算右支节点向下的最小值// 记录当前节点的最优值sum.at(row).at(col) = val + min(leftResult, rightResult);return sum.at(row).at(col);
}

3.4 动态规划——至底向上

细节:

  1. 动态规划的四要素:状态、方程、初始化、答案。
  2. 状态:用dp[i][j]表示以第i行、第j列的节点为起点向下的路径和的最小值。
  3. 方程:dp[i][j] = val + min(dp[i+1][j], dp[i+1][j+1]),当前节点最优解 = 当前节点值 + 左右分支中较小的值。
  4. 初始化:dp[maxRow][n] = triangle[maxRow][n],最后一行的路径和的值即为节点自身的值。
  5. 答案:要计算的结果是dp[0][0]

C++版本:

/*** @param triangle: a list of lists of integers* @return: An integer, minimum path sum*/
int minimumTotal(vector<vector<int>> &triangle) {// write your code hereif (triangle.empty() || triangle.front().empty()){return 0;}// 状态:dp[i][j]表示以第i行、第j列的节点为起点向下的路径和的最小值vector<vector<int>> dp;for (auto it : triangle){dp.push_back(vector<int>(it.size(), 0));}// 初始化:最后一行的路径和的值即为节点自身的值dp.back() = triangle.back();for (int i = dp.size() - 2; i >= 0; i--){for (int j = 0; j < dp.at(i).size(); j++){// 方程式:当前节点最优解 = 当前节点值 + 左右分支中较小的值dp[i][j] = triangle[i][j] + min(dp[i + 1][j], dp[i + 1][j + 1]);}}return dp[0][0]; // 答案:要计算的是从顶点向下的最优解
}

3.5 动态规划——至顶向下

细节:

  1. 动态规划的四要素:状态、方程、初始化、答案。
  2. 状态:用dp[i][j]表示从顶点向下到第i行、第j列的节点所有路径中的路径和的最小值。
  3. 方程:dp[i][j] = val + min(dp[i-1][j-1], dp[i-1][j]),当前节点最优解 = 当前节点值 + 左右肩上游节点中较小的值。
  4. 初始化:三角形两条斜边上的值可以通过从顶点进行累加计算出来。
  5. 答案:要得到的结果是最底层值中最小的值,即min(dp[所有底层节点])

初始化操作一般都是为了把不能通过方程得到的非常规点的值先计算出来,该解法中三角形的斜边上的点都缺失左肩或者右肩上的上游节点,所以先把斜边上的点计算出来有利于后续循环的顺利进行。

C++版本:

/**
* @param triangle: a list of lists of integers
* @return: An integer, minimum path sum
*/
int minimumTotal(vector<vector<int>> &triangle) {// write your code hereif (triangle.empty() || triangle.front().empty()){return 0;}// 状态:dp[i][j]表示从顶点向下到第i行、第j列的节点所有路径中的路径和的最小值vector<vector<int>> dp;for (auto it : triangle){dp.push_back(vector<int>(it.size(), 0));}// 初始化:三角形两条斜边上的值dp[0][0] = triangle[0][0];for (int i = 1; i < triangle.size(); i++){dp[i][0] = triangle[i][0] + dp[i - 1][0];dp[i][i] = triangle[i][i] + dp[i - 1][i - 1];}for (int i = 2; i < dp.size(); i++){for (int j = 1; j < dp.at(i).size() -1; j++){// 方程:当前节点最优解 = 当前节点值 + 左右肩上游节点中较小的值dp[i][j] = triangle[i][j] + min(dp[i - 1][j - 1], dp[i - 1][j]);}}// 答案:要得到的结果是最底层值中最小的值int result = INT_MAX;for (auto it : dp.back()){if (it < result){result = it;}}return result;
}

90 数字三角形(Triangle)相关推荐

  1. vijos 1006 晴天小猪历险记之Hill——数字三角形的终极变化

    题目链接:https://vijos.org/p/1006 数字三角形原题看这里:http://www.cnblogs.com/huashanqingzhu/p/7326837.html 背景 在很久 ...

  2. hihoCoder#1037 : 数字三角形(DP)

    [题目链接]:click here~~ 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 问题描写叙述 小Hi和小Ho在经历了螃蟹先生的任务之后被奖励了一次出国旅游的机会,于是他 ...

  3. hiho一下 第五周 Hihocoder #1037 : 数字三角形

    #1037 : 数字三角形 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 问题描述 小Hi和小Ho在经历了螃蟹先生的任务之后被奖励了一次出国旅游的机会,于是他们来到了大洋彼岸 ...

  4. 【蓝桥杯】【python】数字三角形

    问题描述 虽然我前后用了三种做法,但是我发现只有"优化思路_1"可以通过蓝桥杯官网中的测评,但是如果用c/c++的话,每个都通得过,足以可见python的效率之低(但耐不住人家好用 ...

  5. C++数字三角形问题(动态规划)

    一.问题描述 ★问题描述:给字一个由n行数字组成的数字三角形(等腰三角形).试设计一个算法,计算出从三角形的顶至底的一条路径,使该路径经过的数字总和最大. ★算法设计:对于给定的由n行数字组成的数字三 ...

  6. 数字三角形求最大路径

    /**问题描述] 上图给出了一个数字三角形.从三角形的顶部到底部有很多条不同的路径. 对于每条路径,把路径上面的数加起来可以得到一个和,你的任务就是找到最 大的和.路径上的每一步只能从一个数走到下一层 ...

  7. python--lintcode109.数字三角形(动态规划)

    描述 给定一个数字三角形,找到从顶部到底部的最小路径和.每一步可以移动到下面一行的相邻数字上. 如果你只用额外空间复杂度O(n)的条件下完成可以获得加分,其中n是数字三角形的总行数. 您在真实的面试中 ...

  8. 经典算法——数字三角形的三种解题方法:递推、记忆化搜索、动态规划

    上题目链接: http://acm.sdut.edu.cn/onlinejudge2/index.php/Home/Index/problemdetail/pid/1730.html  递推方法: i ...

  9. 数字三角形问题(动态规划)

    目录 问题描述 分析 问题描述 问题描述:给定一个由n行数字组成的数字三角形,如图所示.图数字三角形试设计一个算法,计算出从三角形的顶至底的一条路径,使该路径经过的数字总和最大. 算法设计:对于给定的 ...

最新文章

  1. ASP.NET MVC5+EF6+EasyUI 后台管理系统(46)-工作流设计-设计分支
  2. ip对应的区域查询(php版)(转)
  3. 【知识发现】python开源哈夫曼编码库huffman
  4. 加 解密的c语言程序,c语言程序设计文个件加密解密.doc
  5. 机器人学习--各种学习资源(初稿)
  6. git 配置命令行别名
  7. Kotlin 系列(二) 基本语法(1)
  8. 排序算法 —— 堆排序
  9. linux报mce清除不良代码,如何分析系统MCE异常?
  10. Linux磁盘系统——管理磁盘的命令
  11. java中结构体入参_JNA中自定义结构体如何传参?
  12. Linux下Qt使用QAudio相关类进行音频采集,使用Windows下的Matlab软件播放
  13. 专业的现场调音机架软件 - Deskew Technologies Gig Performer 4 Mac
  14. ps手机计算机图标教程,ps制作手机图标的方法
  15. 服务器被黑怎么用防御系统解决
  16. Python有哪些优势?
  17. 互联网公司测试组长/leader/经理如何面试社招测试工程师
  18. Matlab/Simulink-PFC-Boost功率因数校正电路仿真搭建
  19. 预训练语言模型复现-2 whole word mask
  20. codewars练习(javascript)-2021/2/1

热门文章

  1. docker镜像的版本(bullseye、buster、slim、alphine)
  2. Unity协程的实现原理
  3. Kubernete 概念(pod)
  4. GitHub Page个人博客中评论功能
  5. Linux中使用shell命令创建文件
  6. git clone 本地仓库
  7. pymprog库应用(一)生产线平衡
  8. npm ERR code ELIFECYCLE解决方案
  9. python getattribute方法_Python:避免getattribute中的无限循环__
  10. 1983年的图灵奖获得者-Ken Thompson (与Dennis M. Ritchie共同获得)