Skip to content

Commit 620738b

Browse files
committed
solve Unique Paths III
1 parent 2c8ff62 commit 620738b

File tree

1 file changed

+153
-1
lines changed

1 file changed

+153
-1
lines changed

problems/unique_paths_iii.py

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,158 @@
1+
"""
2+
Whenever we see the context of grid traversal, the technique of backtracking or DFS
3+
(Depth First Search) should ring a bell. In terms of this problem, it fits the bill
4+
perfectly, with a canonical setting, unlike another similar problem called Robot Room
5+
Cleaner which has certain twists.
6+
As a reminder, backtracking is a general algorithm for finding all (or some) solutions
7+
to some problems with constraints. It incrementally builds candidates to the solutions,
8+
and abandons a candidate as soon as it determines that the candidate cannot possibly
9+
lead to a solution.
10+
Use backtracking whenever you need to enumerate.
11+
In this article, we will showcase how to apply the backtracking algorithm to solve this
12+
problem.
13+
14+
== Backtracking ==
15+
Intuition:
16+
We can consider backtracking as a state machine, where we start off from an initial
17+
state, each action we take will move the state from one to another, and there should be
18+
some final state where we reach our goal.
19+
As a result, let us first clarify the initial and final states of the problem.
20+
21+
Initial State:
22+
- There are different types of squares/cells in a grid.
23+
- There is an origin and a destination cell, which are not given explicitly.
24+
- Initially, all the cells are not visited.
25+
26+
Final State:
27+
- We reach the destination cell, i.e. cell filled with the value 2.
28+
- We have visited all the non-obstacle cells, including the empty cells (i.e. filled
29+
with 0) and the initial cell (i.e. 1).
30+
31+
With the above definition, we can then translate the problem as finding all paths that
32+
can lead us from the initial state to the final state.
33+
34+
More specifically, we could summarise the steps to implement the backtracking algorithm
35+
for this problem in the following pseudo code:
36+
37+
```python
38+
def backtrack(cell):
39+
1. if we arrive at the final state:
40+
path_count ++
41+
return
42+
2. mark the cell as visited
43+
3. for next_cell in 4 directions:
44+
if next_cell is not visited and non-obstacle:
45+
backtrack(next_cell)
46+
4. unmark the current cell
47+
```
48+
49+
Algorithm:
50+
As one can see, backtracking is more of a methodology to solve a specific type of
51+
problems. For a backtracking problem, there are numerous ways to implement the solution.
52+
"""
53+
154
from typing import List
255

356

57+
class OfficialSolution:
58+
"""
59+
Notice how we are using the same grid for tracking visited squares.
60+
61+
Here we would like to highlight some important design decisions we took. As one can
62+
imagine, with different decisions, one would have variations of backtracking
63+
implementations.
64+
65+
1. In-place Modification
66+
- This is an important technique that allows us to save some space in the algorithm.
67+
- In order to mark the cell as visited, often the case we use some matrix or hash
68+
table with boolean values to keep track of the state of each cell, i.e. whether
69+
it is visited or not.
70+
- With the in-place technique, we simply assign a specific value to the cell in the
71+
grid, rather than creating an additional matrix or hash table.
72+
73+
2. Boundary Check
74+
- There are several boundary conditions that we could check during the backtracking,
75+
namely whether the coordinate of a cell is valid or not and whether the current
76+
cell is visited or not.
77+
- We could do the checking right before we make the recursive call, or at the
78+
beginning of the backtrack function.
79+
- We decided to go with the former one, which could save us some recursive calls
80+
when the boundary checks does not pass.
81+
82+
== Complexity Analysis ==
83+
Let N be the total number of cells in the input grid.
84+
- Time Complexity is O(3^N).
85+
- Although technically we have 4 directions to explore at each step, we have at
86+
most 3 directions to try at any moment except the first step. The last direction
87+
is the direction where we came from, therefore we don't need to explore it,
88+
since we have been there before.
89+
- In the worst case where non of the cells is an obstacle, we have to explore
90+
each cell. Hence the time complexity of the algorithm is O(4 * 3 ^ (N-1)), i.e.
91+
O(3 ^ N).
92+
93+
- Space Complexity is O(N).
94+
- Thanks to the in-place technique, we did not use any additional memory to keep
95+
track of the state.
96+
- On the other hand, we apply recursion in the algorithm, which could incur
97+
O(N) space in the function call stack.
98+
- Hence the overall space complexity of the algorithm is O(N).
99+
"""
100+
101+
def uniquePathsIII(self, grid: List[List[int]]) -> int:
102+
rows, cols = len(grid), len(grid[0])
103+
104+
# step 1. initialize the conditions for backtracking
105+
# i.e. initial state and final state
106+
non_obstacles = 0
107+
start_row, start_col = 0, 0
108+
for row in range(0, rows):
109+
for col in range(0, cols):
110+
cell = grid[row][col]
111+
if cell >= 0:
112+
non_obstacles += 1
113+
if cell == 1:
114+
start_row, start_col = row, col
115+
116+
# count of paths as the final result
117+
path_count = 0
118+
119+
# step 2. backtrack on the grid.
120+
def backtrack(row, col, remain):
121+
# we need to modify this external variable
122+
nonlocal path_count
123+
124+
# base case for termination of backtracking
125+
if grid[row][col] == 2 and remain == 1:
126+
# reach the destination
127+
path_count += 1
128+
return
129+
130+
# mark the square as visited. Case: 0, 1, 2
131+
temp = grid[row][col]
132+
grid[row][col] = -4
133+
# now we have 1 less square to visit
134+
remain -= 1
135+
136+
# explore the 4 potential directions around
137+
for ro, co in ((0, 1), (0, -1), (1, 0), (-1, 0)):
138+
next_row, next_col = row + ro, col + co
139+
if not (0 <= next_row < rows and 0 <= next_col < cols):
140+
# invalid coordinate
141+
continue
142+
if grid[next_row][next_col] < 0:
143+
# either obstacle or visited square
144+
continue
145+
146+
backtrack(row=next_row, col=next_col, remain=remain)
147+
148+
# unmark the square after the visit
149+
grid[row][col] = temp
150+
151+
backtrack(row=start_row, col=start_col, remain=non_obstacles)
152+
153+
return path_count
154+
155+
4156
class Solution:
5157
# We have the following grid:
6158
# 1, 0, 0, 0
@@ -13,7 +165,7 @@ class Solution:
13165
# 0, 0, 0, 2
14166
#
15167
# Straight Forward DFS.
16-
# Time: O(4 ^ (r * c)), exponential time.
168+
# Time: O(3 ^ (r * c)), exponential time.
17169
# Space: O(r * c)
18170
# where r and c are the number of rows or columns.
19171
#

0 commit comments

Comments
 (0)