Unique Paths
DESCRIPTION (credit Leetcode.com)
You are given a robot that starts at the top-left corner of a grid with dimensions m x n. The robot can only move either down or right at any point in time. The goal is for the robot to reach the bottom-right corner of the grid.
Given the dimensions of the board m and n, write a function to return the number of unique paths the robot can take to reach the bottom-right corner.
EXAMPLES
Example 1
Input:
m = 3 n = 2
Output: 3
Explanation: The robot starts at the top-left corner and can move right or down. There are 3 unique ways to reach the bottom-right corner of a 3 x 2 grid.
Example 2
Input:
m = 3 n = 7
Output: 28
Run your code to see results here
Explanation
The first step to solving this problem is to identify the recurrence relation, which is a way of representing the problem in terms of smaller subproblems.
Let's define dp[m][n] as the number of unique paths the robot can take from the top-left corner to the bottom-right corner in a m x n grid. Our goal is to express dp in terms of the number of unique paths of a smaller grid.
Since the robot can only move right or down, the number of paths through a m x n grid is equal to to the number of paths in a (m - 1) x n grid + the number of paths in a m x (n - 1) grid.
For example, the diagram below shows how dp[3][2] = dp[3][1] (left) + dp[2][2] (right).
This gives us our recurrence relation:
dp[m][n] = dp[m - 1][n] + dp[m][n - 1]
This recurrence relation is the core to our solution. Once we have it, we have two options to solve the problem:
Recursive Solution (Top-Down)
The recursive solution is the most intuitive way to solve this problem. We can define a recursive function uniquePaths(m, n) that calculates the number of unique paths the robot can take in a m x n grid.
The body of the function uses our recurrence relation to calculate the number of unique paths in a m x n grid based on the number of unique paths in a (m - 1) x n grid and m x (n - 1) grid.
The base cases for the recursion are when the grid has only one row (m == 1) or only one column (n == 1), in which case there is only one unique path that the robot can take through these grids.
def uniquePaths(m: int, n: int) -> int:if m == 1 or n == 1:return 1else:return uniquePaths(m - 1, n) + uniquePaths(m, n - 1)
Overlapping Subproblems
If we were to visualize the call tree of this recursive solution, we would see that are a few overlapping subproblems. For example, during a call to uniquePaths(3, 3), we end up calling uniquePaths(2, 2) twice.
To address this issue, we can use memoization to store the results of subproblems in a dictionary. If we make a recursive call that we have already answer for, then we can look it up in the dictionary directly without having to recompute it.
def uniquePaths(m: int, n: int, memo: dict = {}) -> int:if m == 1 or n == 1:return 1elif (m, n) in memo:return memo[(m, n)]else:memo[(m, n)] = uniquePaths(m - 1, n, memo) + uniquePaths(m, n - 1, memo)return memo[(m, n)]
This allows us to "prune" parts of the call tree to avoid redundant calculations, which reduces the time complexity to O(m * n).
Iterative Solution (Bottom-Up)
The other way to solve this problem is by using an iterative, "bottom-up" approach.
We'll initialize a 2D integer-array of size m x n called dp. Each entry in dp will hold the answer to a smaller subproblem. Since arrays are 0-indexed, dp[0][0] represents the number of unique paths the robot can take through a 1 x 1 grid, and dp[m - 1][n - 1] represents the number of unique paths the robot can take through a m x n grid. For this reason, it might be helpful to think of dp[i][j] as representing the number of unique paths a robot can take from cell (0, 0) to cell (i, j), which we'll do from now on.
In an iterative approach, we start by calculating the number of paths to reach cell [1][1], and then use that to calculate the number of paths to reach cell [1][2], [1][3], ..., [2][1], [2][2], [2][3], and so forth.
Step 1 is to define the base cases. We know that there is only one way to reach any cell in the top row (i == 0, j), or the leftmost column (i, j == 0), so we can initialize those values in dp directly.
# Set base case: there is only one way to reach any cell in the first row (moving only right) for i in range(m): dp[0][i] = 1
# Set base case: there is only one way to reach any cell in the first column (moving only down) for j in range(n): dp[j][0] = 1
Next, we can use a nested for-loop and our recurrence relation to calculate the number of paths to cells (1, 1), (1, 2), (1, 3), ... until cell m - 1, n - 1. After that is complete, we can return the value of dp[m - 1][n - 1] as the answer to our question.
Solution
class Solution:def unique_paths(self, m: int, n: int) -> int:# Initialize a 2D array with dimensions m x ndp = [[0] * n for _ in range(m)]# base case: there is only one way to reach any cell in the first row (moving only right)for i in range(n):dp[0][i] = 1# Set base case: there is only one way to reach any cell in the first column (moving only down)for j in range(m):dp[j][0] = 1# Fill the rest of the dp arrayfor i in range(1, m):for j in range(1, n):dp[i][j] = dp[i - 1][j] + dp[i][j - 1]return dp[m - 1][n - 1]
Complexity Analysis
Time Complexity: The time complexity for both the recursive and iterative solutions is O(m * n) since we need to fill in a table of size m x n. Space Complexity: The space complexity for the recursive solution is O(m * n) due to the call stack, while the iterative solution has a space complexity of O(m * n) due to the table dp.
Loading comments...