Unique Paths

There is a robot on anĀ m x nĀ grid. The robot is initially located at theĀ top-left cornerĀ (i.e.,Ā grid[0][0]). The robot tries to move to theĀ bottom-right cornerĀ (i.e.,Ā grid[m - 1][n - 1]). The robot can only move either down or right at any point in time.

Given the two integersĀ mĀ andĀ n, returnĀ the number of possible unique paths that the robot can take to reach the bottom-right corner.

The test cases are generated so that the answer will be less than or equal toĀ 2 * 109.

Example 1
Pasted image 20231025233723.png

Input: m = 3, n = 7
Output: 28

Example 2

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 -> Down -> Down
2. Down -> Down -> Right
3. Down -> Right -> Down

Intuition

For example if we have a board here and need to calculate the unique moves from A to B:

Pasted image 20231025233957.png

We can work from B to A since at B we know the goal already

As it can be seen from B, we can move to 2 two cells around with 1 unique move:

Pasted image 20231025234115.png

So the what would be the values at the ?. Since we can only move down and right. It should be the value of down and right.

So should be 2

Pasted image 20231025234254.png

Similarly, in here we can only move down and right. Since we cannot move down, we only take the one from the right. There fore it's 1

Pasted image 20231025234343.png

In here, since you can go to it by moving down and right. On the right there are 2 ways to move to it, on the bottom there is 1 way to move to it. Therefore it should be 3

So we have the formular

$$ dp[row][col] = dp[down] + dp[right] $$
Or the same as
$$ dp[row][col] = dp[row + 1][col] + dp[row][col + 1] $$

BFS way (Slower)

An obvious way to think of this is to do BFS. Suppose we start from these cells

Pasted image 20231025234737.png

We can then start expanding out:

Pasted image 20231025234812.png

And then continue.

Implementation

Here we can use a queue and pop level by level so that we make sure we have the bottom and the right before we calculate

class AppSlow:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [[0 for _ in range(n)] for _ in range(m)]

        startPoint = (m - 1, n - 1)
        queue = deque([startPoint])
        visited = set()

        while queue:
            for _ in range(len(queue)):
                currentPosition = queue.popleft()
                visited.add(currentPosition)

                row, col = currentPosition

                if self.isInitialPosition(dp, currentPosition):
                    dp[row][col] = 1
                else:
                    dp[row][col] = self.getPaths(dp, row, col)
                
                for dr, dc in [(-1, 0), (0, -1)]:
                    newRow, newCol = row + dr, col + dc

                    if self.withinBoundary(dp, newRow, newCol) and \
                        (newRow, newCol) not in visited:
                        queue.append((newRow, newCol))
        return dp[0][0]

    def withinBoundary(self, dp, newRow, newCol):
        return newRow >= 0 and newCol >= 0 and newRow < len(dp) and newCol < len(dp[0])

    def isInitialPosition(self, dp, currentPosition):
        row, col = currentPosition
        goalRow, goalCol = len(dp) - 1, len(dp[0]) - 1

        return (row == goalRow and col == goalCol) or\
               (row == goalRow - 1 and col == goalCol) or\
               (row == goalRow and col == goalCol - 1)

    def getPaths(self, dp, row, col):
        bottomResult = dp[row + 1][col] if row + 1 < len(dp) else 0
        rightResult = dp[row][col + 1] if col + 1 < len(dp[0]) else 0
        return bottomResult + rightResult

Time complexity: $O(|V| + |E|)$

  • in which $|V|$ is the number of nodes ($m \times n$)
  • |E| is the number of edges ($m \times (n - 1) + n \times (m - 1)$)

Space complexity: $O(m \times n)$ + $O(m \times n)$

  • Store the queue
  • Store the dp array

For loop way (Faster)

It's not necessary to do BFS, we can just do it for loop way and loop for each row, and col.

This works because to find an answer of a cell, we only need the bottom and right. Which essentially can be done with a loop.

Pasted image 20231025235418.png

In here if we perform a loop from row to column go reverse, we should already have what we need to calculate ?

Implementation

class App:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [[0 for _ in range(n)] for _ in range(m)]
        startRow, startCol = (m - 1, n - 1)

        dp[startRow][startCol] = 1
        dp[startRow - 1][startCol] = 1
        dp[startRow][startCol - 1] = 1

        for row in range(startRow, -1, -1):
            for col in range(startCol, -1, -1):
                if dp[row][col] != 0: continue
                bottomRow = dp[row + 1][col] if row + 1 < m else 0
                rightRow = dp[row][col + 1] if col + 1 < n else 0

                dp[row][col] = bottomRow + rightRow
        
        return dp[0][0]

Time complexity: $O(m \times n)$
Space complexity: $O(m \times n)$ ā€” store the dp array

Forward way (Calculate from A)

Similar idea but calculating from A. We need to however calculate the cell of left and top since we move down and right:

Pasted image 20240408203007.png

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        board = [[0 for col in range(n)] for row in range(m)]

        for row in range(len(board)):
            for col in range(len(board[0])):
                board[row][col] = self.calculateMaximumPath(board, row, col)
        
        return board[-1][-1]
    
    def calculateMaximumPath(self, board: List[List[int]], row: int, col: int):
        if row == 0 and col == 0: return 1

        leftValue = board[row][col - 1] if col > 0 else 0
        topValue = board[row - 1][col] if row > 0 else 0

        return leftValue + topValue