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+
154from 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+
4156class 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