原题网址:https://leetcode.com/problems/unique-paths/
A robot is located at the top-left corner of a m x n grid (marked ‘Start’ in the diagram below).
The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked ‘Finish’ in the diagram below).
How many possible unique paths are there?
Above is a 7 x 3 grid. How many possible unique paths are there?
Note: m and n will be at most 100.
Example 1:
Input: m = 3, n = 2
Output: 3
Explanation:
From the top-left corner, there are a total of 3 ways to reach the bottom-right corner:
1. Right -> Right -> Down
2. Right -> Down -> Right
3. Down -> Right -> Right
Example 2:
Input: m = 7, n = 3
Output: 28
题目给了一个n行m列的网格,一个机器人要从左上角走到右下角,并且机器人每次只能向右走或者向下走,问有多少种走法。
很明显,我们知道机器人一共有m-1次向右走,n-1次向下走,每一种走法就是n - 1个Down动作和m - 1个Right动作的排列,也就是 C n − 1 + m − 1 n − 1 或 C n − 1 + m − 1 m − 1 C_{n - 1 + m - 1}^{n - 1}或C_{n - 1 + m - 1}^{m - 1} Cn−1+m−1n−1或Cn−1+m−1m−1。
因此我们可以只要计算出 C n − 1 + m − 1 n − 1 或 C n − 1 + m − 1 m − 1 C_{n - 1 + m - 1}^{n - 1}或C_{n - 1 + m - 1}^{m - 1} Cn−1+m−1n−1或Cn−1+m−1m−1就可以得到答案,但是又有一个问题就是我们应该如何高效的计算一个组合排列数 C m n C_{m }^{n} Cmn。
如果我们直接按公式:
C m n = m ! ( m − n ) ! ∗ n ! C_m^n = \frac{m!}{(m-n)!*n!} Cmn=(m−n)!∗n!m!
来计算,那么当m和n比较大时,很容易在计算分子的时候就溢出了,并不是很通用。
既然一次性算出 C m n C_{m }^{n} Cmn容易溢出,那我们可以先从更小的子问题开始。我们可以用动态规划的思想来逐步求解出 C m n C_{m }^{n} Cmn。动态规划思想中最核心的地方就是如何划分子问题,以及如何通过子问题得到更大的问题的解。
通过公式:
C m n = C m − 1 n − 1 + C m − 1 n C_m^n = C_{m-1}^{n-1} + C_{m-1}^{n} Cmn=Cm−1n−1+Cm−1n
我们可以将 C m n C_{m }^{n} Cmn分解为更小的两个子问题,我们只要算出了 C m − 1 n − 1 和 C m − 1 n C_{m-1}^{n-1} 和 C_{m-1}^{n} Cm−1n−1和Cm−1n的值,便可以计算出 C m n C_{m }^{n} Cmn。具体实现如下:
// golang 0ms 100%
func C(m int, n int) int {
//special cases
if m == 0 || n == 0 || m == n {
return 1
}
solutions := make([][]int, m + 1)
for i := 0;i < m + 1;i++ {
size := n + 1
if m < n {
size = m + 1
}
solutions[i] = make([]int,size)
}
//initialize
for i := 0;i < m + 1;i++ {
solutions[i][0] = 1
}
solutions[1][1] = 1
//dynamic programming
for i := 2;i < m + 1;i++ {
for j := 1;j < len(solutions[m]);j++ {
solutions[i][j] = solutions[i - 1][j] + solutions[i - 1][j - 1]
if i == m && j == n {
break
}
}
}
return solutions[m][n]
}
func uniquePaths(m int, n int) int {
total := m + n -2
sel := m - 1
if n - 1 < sel {
sel = n - 1
}
return C(total, sel)
}
代码中函数C即是来计算组合数的公式,它通过一个m+1行n+1列的矩阵solutions来存储计算结果,solutions[m][n]代表 C m n C_{m }^{n} Cmn的值。算法先计算出m=0和m=1时的所有组合数,之后不断迭代直至计算出 C m n C_{m }^{n} Cmn。
这种解法依然是用动态规划策略,但更加直观。假设到达位置(i,j)的路径数位solutions[i][j],那么有:
s o l u t i o n s [ i ] [ j ] = s o l u t i o n s [ i − 1 ] [ j ] + s o l u t i o n s [ i ] [ j − 1 ] solutions[i][j] = solutions[i-1][j] + solutions[i][j-1] solutions[i][j]=solutions[i−1][j]+solutions[i][j−1]
这是因为机器人只能向右或向下移动,到达位置(i,j)必须经过(i-1,j)或(i,j-1)。算法实现如下:
// golang 0ms 100%
func uniquePaths(m int, n int) int {
//special cases
if m == 1 || n == 1 {
return 1
}
solutions := make([][]int, n)
for i := 0;i < n;i++ {
solutions[i] = make([]int, m)
}
//initialize
for i := 0;i < n;i++ {
solutions[i][0] = 1
}
for j := 0;j < m;j++ {
solutions[0][j] = 1
}
//dynamic programming
for i := 1;i < n;i++ {
for j := 1;j < m;j++ {
solutions[i][j] = solutions[i - 1][j] + solutions[i][j - 1]
}
}
return solutions[n - 1][m - 1]
}
第二个解法通过稍加改动,便可以来更进一步解决unique-path-ii这道题。unique-path-ii在上面这道题的基础上,在一些位置设置了障碍物。对于有障碍物的位置(i,j),令solutions[i][j]=0即可。
// golang 4ms 100%
func uniquePathsWithObstacles(obstacleGrid [][]int) int {
n := len(obstacleGrid)
m := len(obstacleGrid[0])
solutions := make([][]int, n)
for i := 0;i < n;i++ {
solutions[i] = make([]int, m)
}
//initialize
for i := 0;i < n;i++ {
if obstacleGrid[i][0] == 1 {
for j := i;j < n;j++ {
solutions[j][0] = 0
}
break
}
solutions[i][0] = 1
}
for j := 0;j < m;j++ {
if obstacleGrid[0][j] == 1 {
for i := j;i < m;i++ {
solutions[0][i] = 0
}
break
}
solutions[0][j] = 1
}
//dynamic programming
for i := 1;i < n;i++ {
for j := 1;j < m;j++ {
if obstacleGrid[i][j] == 1 {
solutions[i][j] = 0
continue
}
solutions[i][j] = solutions[i - 1][j] + solutions[i][j - 1]
}
}
return solutions[n - 1][m - 1]
}