Python动态规划算法求解不同路径问题

691 阅读6分钟
原文链接: zhuanlan.zhihu.com

专栏开通有一段时间了,却没有一篇和专栏主题相关的文章,😓。今天简单总结一下关于动态规划(dynamic programming)的应用。最近在学习算法过程中发现了一个叫动态规划的东东,看了很多概念,还是不能应用,于是在leetcode上面找了一道关于动态规划的简单题目来练习。

动态规划基本概念:

什么是动态规划?动态规划的意义是什么?www.zhihu.com图标

题目描述:

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

问总共有多少条不同的路径?

题目来源:

不同路径 - 领扣 (LeetCode)leetcode-cn.com图标

题目分析:

第一眼看到题目觉得是一个迷宫问题的简化,所谓简化是方向限制在向下和向右,并且是一个没有障碍物的迷宫,因此想到了常用的搜索算法。采取深度搜索优先开始遍历迷宫,统计出总的路径数目即可。代码也比较简单:

class Solution2:
    counter = 0
    def uniquePaths(self, m, n):
        """
        :type m: int
        :type n: int
        :rtype: int
        """
        if m != 1 and n != 1:
            self.uniquePaths(m - 1, n)
            self.uniquePaths(m, n - 1)
        if m == 1 or n == 1:
            self.counter = self.counter + 1
            return

运行之后测试通过,可是提交leetcode会遇到计算超时的问题,因为这种方法本质上还是采用暴力破解的思想,并且使用了效率比较底下的递归。于是开始重新考虑动态规划方法。这个地方分享一点学习心得,学习算法核心还是整理思路,思路没有理清楚,去硬啃别人的代码效果不是很好。这里尝试用图解的方法来讲解这个过程。

分析题目后我们先采取一个简单最小单元来尝试推导到终点步数,假设一个最简单的四个格子的网格,我们要从左上角到右下角有几条路线,很明显,根据只能向下和向右的移动规则,我们总结出只有两条路径,如图所示

那我们再加成一个3*3的格子呢

这个时候规律就比较明显一些了,总结如下

  1. 凡是边上的格子因为只能超一个方向移动,因此只有一条路径。
  2. 中间的格子是相邻两个格子的路径相加

这个时候其实根据这两点我们已经可以写出代码了。我们再观察一下,到这个时候原本的求解路径总数的题目已经被我们抽象成一个二维数组的推导题,并且规律很简单,其实像一个二维的菲波那切数列推导,每一个非边上的格子值等于左边和上面格子值相加。我们如果用二维数组来表示这个网格,数组存的值就是到达当前网格的总路径和,其实就是求解A[x][y]的值。所以这道题抽象出来就是一个简单的推导式A[x][x]=A[x-1]+A[y-1]

好的,此时我们可以用迅雷不及掩耳之势写出我们的python代码

class Solution:
    def uniquePaths(self, m, n):
        """
        :type m: int
        :type n: int
        :rtype: int
        """
        # 初始化一个值全为1的矩阵,这样可以不用再去给边界赋值
        path_martrix = [[1 for i in range(m)] for j in range(n)]
        for line in range(1, n):  # 从第二行第二列开始遍历矩阵
            for col in range(1, m):
                path_martrix[line][col] = path_martrix[line - 1][col] + path_martrix[line][col - 1]  # 推导式
        return path_martrix[n - 1][m - 1]  # 返回矩阵最右下角的值

再次提交,Bang!顺利通过

还是动态规划大法好啊,效率提高几个数量级!

接下来看一个复杂一丢丢的题目:

题目描述

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?



网格中的障碍物和空位置分别用 10 来表示。

说明:mn 的值均不超过 100。

来源:

不同路径 II - 领扣 (LeetCode)leetcode-cn.com图标

思路:

在之前简单迷宫的基础上加了障碍物,通过图形推导发现只需要在上一个A[x][Y]=A[x-1][Y]+A[x][Y-1]的基础上加一个判断,
需要注意,因为有障碍,所以迷宫不像上一个简单迷宫是自己生成,只能更新输入迷宫里面初始化值。因为我们需要每个格子数字作为到达这一格的步数,因此不能用1来表示,我们在初始化迷宫之后把1更新为-1,来表示障碍物。

图形推导:

通过图形推导我们发现跟之前没有障碍物的网格相比,有一点更新:

  • -1表示障碍物,根据移动规则,如果上边和左边的边界上出现-1,则后面格子无法到达,因此为0。
  • 因为有了障碍物,在相加时候加一个判断,因为我们设置障碍为-1,因此只需要判断不是正数即可。

求每个格子的推导方式则不变,跟前面一样A[x][Y]=A[x-1][Y]+A[x][Y-1],具体代码可以从仓库查看,就不添加在这篇文章了。

更简单一种实现方法:

res = [0] * len(obstacleGrid[0])  # 初始化一个长度为列长度的一维数组,
res[0] = 1
for line_idx in obstacleGrid:
    for col_idx, each in enumerate(line_idx):
        if each == 0:  # 如果值为0,不是障碍
            if col_idx != 0:  # 并且索引不为0,不是第一列
                res[col_idx] += res[col_idx - 1]  # 则等于列表当前值加上前一项的值,相当于这个列表再一直循环累加
        else:
            res[col_idx] = 0  # 是障碍时候则设置值为0
return res[-1]
  • 因为是直接遍历,时间复杂度还要跟低一些,核心算法时间复杂度是一样的,是O(m*n),空间上会多出一个为n长度的一维列表
  • 其实本质上还是A[x][Y]=A[x-1][Y]+A[x][Y-1],但是巧妙的利用了一个一维列表来维持每行的步数,这样每次数据存的是当前行每一个格子所需步数
  • 遍历完成之后,res列表最最后一个存储就是结果
  • 遍历第二行的时候,res[col_idx]实际上就表示上一行的格子步数,即A[x-1][Y],res[col_idx - 1]表示前一个格子步数
  • 跟前面算法比,因为只有一个一维数列存储步数,因此到最后只会存储到达最后一行每个格子所需要的步数。

图解如下:

在第一行的时候到达当前格子步数等于前一个格子值加上现在,因此不会变,如果没有障碍,根据公式res[col_idx] += res[col_idx - 1],都赋值会成1

如果不在第一行,表示上一个格子值加上左边一个格子纸,并且加入判断不等一,排除障碍,所以实际效果是一样的,最后返回res[-1]就是我们要的结果





看到这里了,如果觉得不错,点个赞鼓励一下呗~


代码地址:

Danielyan86/LeetcodePython3github.com图标