一、题目描述
======
不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例1:
输入:m = 3, n = 7 输出:28 示例 2:
输入:m = 3, n = 2 输出:3 解释: 从左上角开始,总共有 3 条路径可以到达右下角。
向右 -> 向下 -> 向下 向下 -> 向下 -> 向右 向下 -> 向右 -> 向下 示例 3:
输入:m = 7, n = 3 输出:28 示例 4:
输入:m = 3, n = 3 输出:6
二、思路分析
======
- 又是寻址问题,又是问有多少种走法。又是需要你从小到大一步一步解决问题。我都说道这里了如果你还不知道用动态规划来做,我也不知道该咋说了。
- 当然了,解决问题是有很多种的。笔者这里只是建议通过动态规划来解决问题或者说来加强我们对动态规划的熟悉度。并不是说此题非动归不可
转移方程
- 首先,我们考虑下本题是寻找路径的问题,我们直接接触了没有维度的概念,只有执行与方式的不同。但是本体是一个二维维度的问题,所以我们需要借助二维数组来帮我们对小问题的存储。
- 我们首先规定
dp[i][j]
来表示从地图左上角做到地图(i,j)位置的情况 - 那么我们思考下,如果想在(0,0)位置到大(i,j)位置我们该如果过去呢?题目中也说了我们只能向下或者向右。也就是说我们只能通过增加i或者增加j来进行移动。
- 那么我们换位思考下有哪些点可以通过增加i或者增加j到大(i,j)呢。
- 上面是笔者画的一个图,根据图示应该很明显得出有两个节点可以通过合法的移动到大(i,j) 。
- 也就是存在(i-1,j),(i,j-1)两个节点。到大这两个节点的路径和就是
dp[i][j]
的值了 - 不知道你有没有发现,如果zxhtom这个机器人走在边缘的话情况就有点特殊了,实际上就是边界问题
- 当机器人走在边界上时,他的来源只能是在轴方向的上一个。因为另外一个是不存在的节点,我们可以理解成此路不通的状态。所以最终我们确定的状态转移方程如下
dp[i][j]=dp[i−1][j] dp[i][j−1]dp[i][j]=d p[i-1][j] d p[i][j-1]dp[i][j]=dp[i−1][j] dp[i][j−1]
- 但是这里需要特别说明一点,当
dp[i][j]
表示一个虚拟空间地址时他的值等于0。
初始值
- 初始值就是
dp[0][0]=1
。这里理解可能有点抽象。dp[0][0]
在左上角是无法通过下移或者右移来实现的,因为他就是原地踏步才可以。这有点和题意不符。 - 但是我们还是通过反向理解法解释的话接简单很多,因为
dp[0][1]=dp[0][0]
.。 所以dp[0][0]=1
. - 还有一点我们可以理解成机器人初始位置是需要我们程序放置的。放置可以理解成一步到位。所以
dp[0][0]=1
从小到大执行
- 转移方程确定了初始值也确定了。从小到大就是循环遍历就可以了。
三、AC 代码
=======
代码语言:java复制public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
dp[0][0] = 1;
for (int i = 0; i < m; i ) {
for (int j = 0; j < n; j ) {
if (i == 0 && j == 0) {
continue;
}else if (i == 0) {
dp[i][j] = dp[i][j - 1];
} else if (j == 0) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j] dp[i][j - 1];
}
}
}
return dp[m-1][n-1];
}
- 因为我们是二维地图走路,所以我们需要二维数组存储对应的数据。这里和我们的爬楼梯是一样的到底。我们这里没有必要存储所有细节的数据,我们只需要存储与当前节点相关的左边节点和上边节点就可以了。这种方式笔者这里就不在去实现了。不清楚的可以在主页中找到爬楼梯章节查看具体逻辑
升级
--
- 上面的动态规划解决的太完美了,因为我们不需要考虑复杂的情况,只需要三板斧就可以解决了。而且执行效率真的没得说。美中不足的是在内存消耗上有点不完美。
- 笔者的优化思路上面也提到过,将dp数组进行简化,但是最终在实现上好像dp也没法进行简化所以这种思路放弃了。后来笔者看了下官网的推荐算法。除了动态规划以外他们还有中方法叫排列组合。
- 无论怎么走,因为无法回退所以总步数是固定的,确切的说总横步数和总纵步数都是固定的。
- 我们在精确点就是每次的步数中横向和纵向是相对存在的。比如在总步数7的情况如果3个横步数,则剩下的一定是4个纵步数。那么我们只需要计算出3个横步数所有的排列情况就可以了。
- 因为我们在存储中都是0开始,所以在m*n矩阵中我们总步数是
(m-1) (n-1)
, 所以我们只需要在m-2
中找出m-1
中可能就行了。 下面贴出一张官网的公式推倒
Cm n−2m−1=(m n−2m−1)=⟨m n−2}(m n−3)⋯n(m−1)!=(m n−2)!(m−1)!(n−1)!C_{m n-2}^{m-1}=left(begin{array}{c} m n-2 \ m-1 end{array}right)=frac{langle m n-2}(m n-3) cdots n}{(m-1) !}=frac{(m n-2) !}{(m-1) !(n-1) !}Cm n−2m−1=(m n−2m−1)=(m−1)!⟨m n−2}(m n−3)⋯n=(m−1)!(n−1)!(m n−2)!
- 最后我们只需要编写出上述公式的代码即可。不得不说结合数学的推导的程序真的是很不错的。但是这样的代码给别人理解就很难理解了。这里读者也没有尝试过直接使用的官网的代码。
- 最终执行内存上比我们动归高了很多。这里笔者只是为了进行两者对比的区别。本文的主要内容还是介绍动态规划这个概念如何进行落地。
四、总结
====
- 本文笔者主要通过一个案列介绍如何通过动归来进行问题的分析与落地
- 通过三板斧带你理解动态规划本质的东西。相信通过三板斧你应该多少会理解动态规划的问题了吧
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!