【算法】【动态规划篇】第3节:数字三角形问题

本期任务:介绍算法中关于动态规划思想的几个经典问题

【算法】【动态规划篇】第1节:0-1背包问题

【算法】【动态规划篇】第2节:数字矩阵问题

【算法】【动态规划篇】第3节:数字三角形问题

【算法】【动态规划篇】第4节:硬币找零问题

【算法】【动态规划篇】第5节:剪绳子问题


一、问题描述

"""
    有一个由非负整数组成的三角形,第一行只有一个数,除了最下行之外每个数的左下方和右下方各有一个数.

        1
       3 2
     4 10 1
    4 3 2 20

    从第一行的数开始,每次可以往左下或右下走一格,直到走到最下行,把沿途经过的数全部加起来,如何走才能使得这个和尽量大?

    输入:三角形的行数n,数字三角形的各个数(从上到下,从左到右)
    n=4
    [1, 3, 2, 4, 10, 1, 1, 3, 2, 20]
    
    输出:最大的和
    24

"""

二、算法思路

本题的解法与数字矩阵问题大同小异,可以阅读【算法】【动态规划篇】第2节:数字矩阵问题,加深对此类问题的理解。

1. 策略选择

一个模型:

  • 数字三角形问题是典型的“多阶段决策最优解”问题,每一层决策一次,共决策n次(n为数字三角形行数);最优解是最长路径值。

三个特征:

  • 重复子问题:

    • 不同的决策序列,到达某个相同的阶段时,可能会产生重复的状态。
    • 本题中,不同的路径可能到达相同的位置,如示例中的数字10对应位置的状态由 ( 1 , 3 , 10 ) (1,3,10) ( 1 , 2 , 10 ) (1,2,10)
  • 无后效性:

    • 前面阶段的状态确定之后,不会被后面阶段的决策所改变。一般而言,满足多阶段决策最优解模型的问题都满足无后效性,特例情况,如八皇后问题解数独问题等。
  • 最优子结构:

    • 后面阶段的状态可以通过前面阶段的状态推导出。
    • 本题中,每一个状态都可以通过上一轮的状态推倒而来,如示例中的数字10对应位置的状态由数字10与上一行的数字3和2对应状态中较大者的值共同决定。

综上所述,本问题满足一个模型、三个特征,所以可以使用动态规划来求解。
当然,凡是能用动态规划解决的问题,都可以用回溯思想来暴力求解,具体实现代码文末已给出,更多关于回溯思想的应用,可以参照:【算法】【回溯篇】第7节:0-1背包问题


2. 动态规划算法思路

动态规划使用的流程:自顶向下分析问题,自底向上解决问题!

  • 由于本题给定的是用一维数组表示的二维矩阵,所以需要对数组元素进行重新编号,并用字典(代码中的new2old)来保存新旧编号的关系,以方便后续访问。(大白话:明明是二维矩阵,非得给我一个一维数组,嘿嘿,不熟,换回来。)
  • 使用与原矩阵规模一致的二维维数组来保存从第0行到任意位置的最短路径长度。
  • 更新过程(状态转移思路):
    • (0, 0)到(i, j)的最长路径为:当前位置的值+(0, 0)到上一轮两个父节点的最长路径的较大者
      d p _ a r r [ i ] [ j ] = a r r [ n e w 2 o l d . g e t ( ( i , j ) ) ] + m a x ( d p _ a r r [ i 1 ] [ j ] , d p _ a r r [ i 1 ] [ j 1 ] ) dp\_arr[i][j] = arr[new2old.get((i, j))] + max(dp\_arr[i - 1][j], dp\_arr[i - 1][j - 1])

三、Python代码实现

1. 动态规划解法

class Solution():
    def reindex(self, arr):
        """
        由于输入的是一个一维数组,而且数字三角形不满足完全二叉树的特性,需要重新编号
        """
        d = list()
        index_i = 0
        index_j = 0
        for i in range(len(arr)):
            d.append((index_i, index_j))
            index_j += 1
            if index_i < index_j:
                index_i += 1
                index_j = 0
        d = {item[1]: item[0] for item in enumerate(d)}  # 由新标号反推就编号
        return d

    def dp(self, n, arr):
        """
        使用动态规划法求解数字三角形问题
        :param n: 三角形高度
        :param arr: 保存三角形数据的一维数组
        :return: 最大路径值
        """
        new2old = self.reindex(arr)  # 对数组进行重新标号,方便后续访问左右孩子(三角形不同于完全二叉树)
        dp_arr = [[0 for j in range(i + 1)] for i in range(n)]  # 维护一个三角形的数组用来存储max_dist(i,j)

        dp_arr[0][0] = arr[0]  # 初始化首个元素
        for i in range(1, n):
            dp_arr[i][0] += arr[new2old.get((i, 0))] + dp_arr[i - 1][0]  # 初始化第一列元素
            dp_arr[i][i] += arr[new2old.get((i, i))] + dp_arr[i - 1][i - 1]  # 初始化对角线元素

        # 填写各阶段重复子问题的最优解
        for i in range(2, n):
            for j in range(1, i):
                # (0, 0)到(i, j)的最长路径为:当前位置的值+(0, 0)到上一轮两个父节点的最长路径的较大者
                dp_arr[i][j] = arr[new2old.get((i, j))] + max(dp_arr[i - 1][j], dp_arr[i - 1][j - 1])

        return max(dp_arr[-1])  # 返回最后一行的最大值


def main():
    arr = [1, 3, 2, 4, 10, 1, 1, 3, 2, 20]
    n = 4

    client = Solution()
    print(client.dp(n, arr))


if __name__ == '__main__':
    main()

运行结果:

24

2. 回溯解法

class Solution():
    def reindex(self, arr):
        """
        由于输入的是一个一维数组,而且数字三角形不满足完全二叉树的特性,需要重新编号
        """
        d = list()
        index_i = 0
        index_j = 0
        for i in range(len(arr)):
            d.append((index_i, index_j))
            index_j += 1
            if index_i < index_j:
                index_i += 1
                index_j = 0
        d = {item[1]: item[0] for item in enumerate(d)}  # 由新标号反推就编号
        return d

    def trackback(self, n, arr):
        self.size = n
        self.arr = arr
        self.max_v = 0
        self.new2old = self.reindex(arr)  # 对数组进行重新标号,方便后续访问左右孩子(三角形不同于完全二叉树)
        self.helper(0, 0, 1)
        return self.max_v

    def helper(self, index_i, index_j, value):
        if index_i == self.size - 1:  # 当遍历到最后一行时进行结算
            if self.max_v < value:
                self.max_v = value
            return
        self.helper(index_i + 1, index_j, value + self.arr[self.new2old[(index_i + 1, index_j)]])
        self.helper(index_i + 1, index_j + 1, value + self.arr[self.new2old[(index_i + 1, index_j + 1)]])


def main():
    arr = [1, 3, 2, 4, 10, 1, 1, 3, 2, 20]
    n = 4

    client = Solution()
    print(client.trackback(n, arr))


if __name__ == '__main__':
    main()

运行结果:

24
原创文章 36 获赞 32 访问量 2737

猜你喜欢

转载自blog.csdn.net/weixin_43868754/article/details/105715238