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

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

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

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

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

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

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

【算法】【动态规划篇】第6节:最低票价问题

【算法】【动态规划篇】第7节:最长子串问题

【算法】【动态规划篇】第8节:最大正方形问题

【算法】【动态规划篇】第9节:乘积最大子数组问题

【算法】【动态规划篇】第10节:最长连续序列问题


一、问题描述

    假设我们有一个n乘以n的矩阵w[n][n]。矩阵存储的都是正整数。
    棋子起始位置在左上角,终止位置在右下角。我们将棋子从左上角移动到右下角。每次只能向右或者向下移动一位。
    从左上角到右下角,会有很多不同的路径可以走。我们把每条路径经过的数字加起来看作路径的长度。
    那从左上角移动到右下角的最短路径长度是多少呢?

    输入:矩阵行数n,矩阵M
    4
    1 3 5 9
    2 1 3 4
    5 2 6 7
    6 8 4 3

    输出:从(0, 0)(3, 3)的最短路径长度
    19

二、算法思路

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

1. 策略选择

一个模型:

  • 数字矩阵问题是典型的“多阶段决策最优解”问题,如下图所示,整个决策过程分为6个阶段,每个阶段决策是向下还是向右;最优解是从(0, 0)到(3, 3)的最短路径长度
    在这里插入图片描述

三个特征:

  • 重复子问题:
    • 不同的决策序列,到达某个相同的阶段时,可能会产生重复的状态。
    • 如图,本题中 ( 0 , 0 ) (0,0) (0,0) ( 2 , 2 ) (2,2) (2,2)有多条路径
      在这里插入图片描述
  • 无后效性:
    • 前面阶段的状态确定之后,不会被后面阶段的决策所改变。一般而言,满足多阶段决策最优解模型的问题都满足无后效性,特例情况,如八皇后问题解数独问题等。
    • 本题中,如果我们走到(i, j)这个位置,我们只能通过(i-1, j),(i, j-1)这两个位置移动过来,也就是说,我们想要计算(i, j)位置对应的状态,只需要关心(i-1, j),(i, j-1)两个位置对应的状态,并不关心棋子是通过什么样的路线到达这两个位置的。而且,我们仅仅允许往下和往右移动,不允许后退,所以,前面阶段的状态确定之后,不会被后面阶段的决策所改变,所以,这个问题符合“无后效性”这一特征。
  • 最优子结构:
    • 后面阶段的状态可以通过前面阶段的状态推导出。
    • 本题中,我们把从起始位置(0, 0)到(i, j)的最小路径,记作min_dist(i, j)。因为我们只能往右或往下移动,所以,我们只有可能从(i, j-1)或者(i-1, j)两个位置到达(i, j)。也就是说,到达(i, j)的最短路径要么经过(i, j-1),要么经过(i-1, j),而且到达(i, j)的最短路径肯定包含到达这两个位置的最短路径之一。换句话说就是,min_dist(i, j)可以通过min_dist(i, j-1)和min_dist(i-1, j)两个状态推导出来。这就说明,这个问题符合“最优子结构”。

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


2. 动态规划算法思路

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

  • 使用与原矩阵规模一致的二维维数组来保存从 ( 0 , 0 ) (0,0) (0,0) ( 3 , 3 ) (3,3) (3,3)的最短路径长度。
  • 更新过程(状态转移思路):
    m i n _ d i s t ( i , j ) = w [ i ] [ j ] + m i n ( m i n _ d i s t ( i , j − 1 ) , m i n _ d i s t ( i − 1 , j ) ) min\_dist(i, j) = w[i][j] + min(min\_dist(i, j-1), min\_dist(i-1, j)) min_dist(i,j)=w[i][j]+min(min_dist(i,j1),min_dist(i1,j))
  • 状态更新过程:
    在这里插入图片描述
    在这里插入图片描述

三、Python代码实现

1. 动态规划解法

class Solution():

    def dp(self, n, arr):
        """
        使用动态规划法求解数字矩阵问题
        :param n: 矩阵规模n,
        :param arr: 矩阵M
        :return: 从(0, 0)到(3, 3)的最短路径长度
        """
        dp_arr = [[0 for j in range(n)] for i in range(n)]  # 维护一个二维数组用来存储max_dist(i,j)

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

        # 填写各阶段重复子问题的最优解
        for i in range(1, n):
            for j in range(1, n):
                # (0, 0)到(i, j)的最短路径为:当前位置的值+(0, 0)到上一轮两个父节点的最短路径的较小者
                dp_arr[i][j] = arr[i][j] + min(dp_arr[i - 1][j], dp_arr[i][j - 1])
        return dp_arr[n - 1][n - 1]  # 返回右下角元素值


def main():
    n = 4
    arr = [[1, 3, 5, 9], [2, 1, 3, 4], [5, 2, 6, 7], [6, 8, 4, 3]]

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


if __name__ == '__main__':
    main()

运行结果:

19

2. 回溯解法

class Solution():

    def trackback(self, n, arr):
        """
        使用回溯法求解数字矩阵问题
        :param n: 矩阵规模n,
        :param arr: 矩阵M
        :return: 从(0, 0)到(3, 3)的最短路径长度
        """
        self.size = n
        self.arr = arr
        self.min_v = int(1e+5)

        self.res = dict()  # 用来优化重复子问题

        self.helper(0, 0, 1)
        return self.min_v

    def helper(self, index_i, index_j, value):
        if index_i == self.size - 1 and index_j == self.size - 1:  # 当遍历到右下角时进行结算
            if self.min_v > value:
                self.min_v = value
            return
        if index_i + 1 < self.size:  # 避免行方向越界访问
            i, j, v = index_i + 1, index_j, value + self.arr[index_i + 1][index_j]
            if self.res.get((i, j)) == None or v < self.res[(i, j)]:  # 优化重复子问题,只有比现有value的值小的情况才有再计算意义
                self.res[(i, j)] = v
                self.helper(i, j, v)
        if index_j + 1 < self.size:  # 避免列方向越界访问
            i, j, v = index_i, index_j + 1, value + self.arr[index_i][index_j + 1]
            if self.res.get((i, j)) == None or v < self.res[(i, j)]:  # 优化重复子问题,只有比现有value的值小的情况才有再计算意义
                self.res[(i, j)] = v
                self.helper(i, j, v)


def main():
    n = 4
    arr = [[1, 3, 5, 9], [2, 1, 3, 4], [5, 2, 6, 7], [6, 8, 4, 3]]

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


if __name__ == '__main__':
    main()

参考
http://gk.link/a/102op

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值