MCD4710 2021 T1 Lect21 Backtracking
MCD4710 2021 T1 Lect21 Backtracking
Introduction to algorithms
and programming
Lecture 21
Backtracking
COMMONWEALTH OF AUSTRALIA
Copyright Regulations 1969
WARNING
This material has been reproduced and communicated to you by or on behalf of Monash
University pursuant to Part VB of the Copyright Act 1968 (the Act).
The material in this communication may be subject to copyright under the Act. Any
further reproduction or communication of this material by you may be the subject of
copyright protection under the Act.
Do not remove this notice.
2
Objectives
• Getting to know the backtracking design paradigm
• Seeing backtracking implementations for specific problems as
well as a generic implementation
• Recap of graph nomenclature and introduction to the
Hamiltonian cycle problem
N-Queens Problem
8-Queens Puzzle
Q
Q
Q
Q
Q
…
…
Input: Positive integer 𝑛
Output: All configurations of 𝑛 queens on an 𝑛 × 𝑛 chessboard such that
none of them are attacking each other
Representation of configuration
columns
0 1 2 3
Q Q 0
1
rows
Q 2
Q 3
[2, 3, 0, 0]
1. Visit https://flux.qa
def valid(conf):
for i in range(len(conf)):
for j in range(i+1, len(conf)):
if attacks(conf[i], i, conf[j], j):
return False
return True
Brute force solution
def queens(n):
confs = bounded_lists([n]*n)
return list(filter(valid, confs))
n=6
#bounded lists 𝑛𝑛
[0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 1]
[0, 0, 0, 0, 0, 2]
[0, 0, 0, 0, 0, 3]
[0, 0, 0, 0, 0, 4]
[0, 0, 0, 0, 0, 5]
[0, 0, 0, 0, 1, 0]
[0, 0, 0, 0, 1, 1]
[0, 0, 0, 0, 1, 2]
…
…
[5, 5, 5, 5, 5, 5]
Improved brute force solution
def queens(n):
confs = permutations(0, n)
return list(filter(valid, confs))
n=6
[0, 1, 2, 3, 4, 5]
[0, 1, 2, 3, 5, 4],
[0, 1, 2, 4, 3, 5],
[0, 1, 2, 4, 5, 3],
[0, 1, 2, 5, 3, 4],
[0, 1, 2, 5, 4, 3],
[0, 1, 3, 2, 4, 5],
[0, 1, 3, 2, 5, 4],
[0, 1, 3, 4, 2, 5],
…
…
[5, 4, 3, 2, 1, 0]
Improved brute force solution
def queens(n):
confs = permutations(0, n)
return list(filter(valid, confs))
[0, 1, 2, 3] [2, 0, 1, 3]
[0,1,2,3] [0, 1, 3, 2] [2, 0, 3, 1]
[0, 2, 1, 3] [2, 1, 0, 3]
[0, 2, 3, 1] [2, 1, 3, 0]
[0, 3, 1, 2] [2, 3, 0, 1]
[0, 3, 2, 1] [2, 3, 1, 0]
[1, 0, 2, 3] [3, 0, 1, 2]
[1, 0, 3, 2] [3, 0, 2, 1]
[1, 2, 0, 3] [3, 1, 0, 2]
[1, 2, 3, 0] [3, 1, 2, 0]
[1, 3, 0, 2] [3, 2, 0, 1]
4x3x2x1 = 24 [1, 3, 2, 0] [3, 2, 1, 0]
15
What is Backtracking?
Exit
and so on
…
Start
Possible Positions: [0,1,2,3,4,5,6,7]
0 Q
1
2
3
4
5
6
7
17
Possible Positions: [0,1,2,3,4,5,6,7]
0 Q
1
2
3
4
5
6
7
0 Q
1
2
3
4
5
6
7
0 Q
1
2 Q
3
4
5
6
7
0 Q
1
2 Q
3
4 Q
5
6
7
0 Q
1 Q
2 Q
3
4 Q
5
6
7
0 Q
1 Q
2 Q
3 [0, 2, 4, 1, 3]
4 Q
5
6
7
0 Q
1 Q
2 Q
3
4 Q
5
6
7
0 Q
1
2 Q
3
4 Q
5
6
7
34
[]
[0]
Partial [0,1,2,3,4,5,6,7]
Solutions [2,3,4,5,6,7]
[0,2]
[4,5,6,7]
[0,2,4] Possible
[1,6,7]
Positions
[0,2,4,1] [0,2,4,6]
[3,7]
…
[0,2,4,1,3] [0,2,4,1,7] 35
Requirements
• Keep track of partial solution.
• Build a set of positions selected.
• Keep track of possible next positions.
• Be able to undo steps.
• Be able to check whether the
partial solution is a (complete) solution.
36
For the 8 Queen problem, a partial
solution is a solution if its length is 8.
a. True
b. False
1. Visit https://flux.qa
Partial Solution: []
38
[]
Q
[0]
Q 2, 3
[0]
Q 2, 3
[0]
[0, 2]
Q 2, 3
[0]
-
[0, 2]
Q 2, 3
[0]
-
[0, 2]
Backtrack
43
[]
Q 2, 3
[0]
-
[0, 2] [0, 3]
Q 2, 3
[0]
- 1
[0, 2] [0, 3]
Q 2, 3
[0]
Q - 1
[0, 2] [0, 3]
[0, 3, 1]
Q
Q 2, 3
[0]
Q - 1
[0, 2] [0, 3]
-
[0, 3, 1]
Q
Q 2, 3
[0]
- 1
[0, 2] [0, 3]
-
[0, 3, 1]
Q
Backtrack
48
[]
Q 2, 3
[0]
- 1
[0, 2] [0, 3]
-
[0, 3, 1]
Backtrack
49
[]
Q 2, 3
[0]
- 1
[0, 2] [0, 3]
-
[0, 3, 1]
Backtrack
50
[]
2, 3
[0] [1]
Q - 1
[0, 2] [0, 3]
-
[0, 3, 1]
2, 3 3
[0] [1]
Q - 1
[0, 2] [0, 3]
-
[0, 3, 1]
2, 3 3
[0] [1]
Q - 1
[0, 2] [0, 3] [1, 3]
-
[0, 3, 1]
Q
2, 3 3
[0] [1]
Q - 1 0
[0, 2] [0, 3] [1, 3]
-
[0, 3, 1]
Q
Q 2, 3 3
[0] [1]
Q - 1 0
[0, 2] [0, 3] [1, 3]
-
[0, 3, 1] [1, 3, 0]
Q
Q 2, 3 3
[0] [1]
Q - 1 0
[0, 2] [0, 3] [1, 3]
- 2
[0, 3, 1] [1, 3, 0]
Q
Q 2, 3 3
[0] [1]
Q - 1 0
[0, 2] [0, 3] [1, 3]
Q - 2
[0, 3, 1] [1, 3, 0]
Q
[1, 3, 0, 2]
for k in range(board_size):
if k not in not_possible: Whatever is not
possible.append(k) in not_possible
return possible is possible
58
Backtracking Algorithm – Queen’s
def getPossible(partial, board_size):
# Finds the next possible options given the partial solution
not_possible = []
possible = []
if partial == []:
possible = [k for k in range(board_size)]
else:
for k in partial:
not_possible.append(k)
for k in range(board_size):
if k not in not_possible:
possible.append(k)
return possible
59
Backtracking Algorithm – Queen’s
def getPossible(partial, board_size):
# Finds the next possible options given the partial solution
not_possible = []
possible = []
if partial == []:
possible = [k for k in range(board_size)]
else:
for k in partial:
not_possible.append(k)
for k in range(len(partial)):
j = (k * -1) - 1
if (partial[j] + j) not in not_possible:
not_possible.append(partial[j] + j)
if (partial[j] - j) not in not_possible:
not_possible.append(partial[j] - j)
for k in range(board_size):
if k not in not_possible:
possible.append(k)
return possible 60
Backtracking Algorithm – Queen’s
Find a set of next
positions
def queens(partial, board_size):
possibleItems = getPossible(partial, board_size)
print(partial, " || ", possibleItems)
Check if
if possibleItems == []: partialSolution is
updateSolution(partial, board_size) a solution
else:
for item in possibleItems: Represent and
partial.append(item) keep track of
queens(partial, board_size) solutions
partial.pop()
Keep track of
return True possible next
positions
Backtrack
61
Backtracking Algorithm – Queen’s
def queens(partial, board_size):
possibleItems = getPossible(partial, board_size)
print(partial, " || ", possibleItems)
if possibleItems == []:
updateSolution(partial, board_size)
else:
for item in possibleItems:
partial.append(item)
queens(partial, board_size)
partial.pop()
return True
62
queens([],4)
[]
[]
item = 1 :
item = 2 :
item = 3 :
item = 1 :
[0]
item = 2 :
item = 3 :
item = 1 :
[0]
item = 2 :
item = 3 :
queens([0],4) item = 1 :
[0]
item = 2 :
item = 3 :
item = 2 :
item = 3 :
item = 2 :
item = 3 :
item = 3 :
item = 3 :
item = 3 :
item = 3 :
item = 3 :
item = 3 :
item = 3 :
item = 3 :
queens([0,2],4)
item = 3 :
item = 3 :
queens([0,2],4)
possibleItems = []
item = 3 :
item = 3 :
queens([0,2],4)
possibleItems = []
item = 3 :
item = 3 :
queens([0,2],4)
possibleItems = []
updateSolution([0,2],4)
item = 3 :
item = 3 :
queens([0,2],4)
possibleItems = []
updateSolution([0,2],4)
return True
item = 3 :
item = 3 :
TRUE
queens([0,3],4)
queens([0,3],4)
possibleItems = [1]
queens([0,3],4)
possibleItems = [1]
queens([0,3],4)
possibleItems = [1]
item = 1 :
queens([0,3],4)
possibleItems = [1]
item = 1 :
partial = [0,3,1]
queens([0,3],4)
possibleItems = [1]
item = 1 :
partial = [0,3,1]
queens([0,3,1],4)
queens([0,3],4)
possibleItems = [1]
item = 1 :
partial = [0,3,1]
queens([0,3,1],4)
queens([0,3,1],4)
queens([0,3],4)
possibleItems = [1]
item = 1 :
partial = [0,3,1]
queens([0,3,1],4)
queens([0,3,1],4)
possibleItems = []
def queens(partial, board_size):
possibleItems = getPossible(partial, board_size)
print(partial, " || ", possibleItems)
if possibleItems == []:
updateSolution(partial, board_size)
else:
for item in possibleItems:
partial.append(item)
queens(partial, board_size)
partial.pop()
93
return True
queens([],4)
possibleItems = [0,1,2,3]
item = 0 :
[]
partial = [0]
queens([0],4) 2, 3
queens([0],4) item = 1 :
[0]
possibleItems = [2,3] 1
item = 2 : [0, 3]
partial = [0,2]
item = 2 :
queens([0,2],4) -
partial = [0] [0, 3, 1]
item = 3 :
partial = [0,3]
item = 3 :
queens([0,3],4)
queens([0,3],4)
possibleItems = [1]
item = 1 :
partial = [0,3,1]
queens([0,3,1],4)
queens([0,3,1],4)
possibleItems = []
def queens(partial, board_size):
possibleItems = getPossible(partial, board_size)
print(partial, " || ", possibleItems)
if possibleItems == []:
updateSolution(partial, board_size)
else:
for item in possibleItems:
partial.append(item)
queens(partial, board_size)
partial.pop()
94
return True
queens([],4)
possibleItems = [0,1,2,3]
item = 0 :
[]
partial = [0]
queens([0],4) 2, 3
queens([0],4) item = 1 :
[0]
possibleItems = [2,3] 1
item = 2 : [0, 3]
partial = [0,2]
item = 2 :
queens([0,2],4) -
partial = [0] [0, 3, 1]
item = 3 :
partial = [0,3]
item = 3 :
queens([0,3],4)
queens([0,3],4)
possibleItems = [1]
item = 1 :
partial = [0,3,1]
queens([0,3,1],4)
queens([0,3,1],4)
possibleItems = []
updateSolution([0,3,1],4) def queens(partial, board_size):
possibleItems = getPossible(partial, board_size)
print(partial, " || ", possibleItems)
if possibleItems == []:
updateSolution(partial, board_size)
else:
for item in possibleItems:
partial.append(item)
queens(partial, board_size)
partial.pop()
95
return True
queens([],4)
possibleItems = [0,1,2,3]
item = 0 :
[]
partial = [0]
queens([0],4) 2, 3
queens([0],4) item = 1 :
[0]
possibleItems = [2,3] 1
item = 2 : [0, 3]
partial = [0,2]
item = 2 :
queens([0,2],4) -
partial = [0] [0, 3, 1]
item = 3 :
partial = [0,3]
item = 3 :
queens([0,3],4)
queens([0,3],4)
possibleItems = [1]
item = 1 :
partial = [0,3,1]
queens([0,3,1],4)
queens([0,3,1],4)
possibleItems = []
updateSolution([0,3,1],4) def queens(partial, board_size):
return True possibleItems = getPossible(partial, board_size)
print(partial, " || ", possibleItems)
if possibleItems == []:
updateSolution(partial, board_size)
else:
for item in possibleItems:
partial.append(item)
queens(partial, board_size)
partial.pop()
96
return True
queens([],4)
possibleItems = [0,1,2,3]
item = 0 :
[]
partial = [0]
queens([0],4) 2, 3
queens([0],4) item = 1 :
[0]
possibleItems = [2,3] 1
item = 2 : [0, 3]
partial = [0,2]
item = 2 :
queens([0,2],4) -
partial = [0] [0, 3, 1]
item = 3 :
partial = [0,3]
item = 3 :
queens([0,3],4)
queens([0,3],4)
possibleItems = [1]
item = 1 :
partial = [0,3,1]
queens([0,3,1],4)
queens([0,3],4)
possibleItems = [1]
item = 1 :
partial = [0,3,1]
queens([0,3,1],4)
partial = [0,3]
queens([0,3],4)
possibleItems = [1]
item = 1 :
partial = [0,3,1]
queens([0,3,1],4)
partial = [0,3]
return True
item = 1 :
[0]
item = 2 :
item = 3 :
item = 2 :
item = 3 :
item = 2 :
item = 3 :
item = 2 :
item = 3 :
queens([1,3,0],4)
possibleItems = [2]
queens([1,3,0,2],4) item = 2 :
possibleItems = [] partial = [1,3,0,2]
updateSolution([1,3,0,2],4) queens([1,3,0,2],4)
partial = [1,3,0]
def queens(partial, board_size):
possibleItems = getPossible(partial, board_size)
print(partial, " || ", possibleItems)
if possibleItems == []:
updateSolution(partial, board_size)
else:
for item in possibleItems:
partial.append(item)
queens(partial, board_size)
partial.pop()
107
return True
queens([],4)
possibleItems = [0,1,2,3]
item = 0 :
[]
partial = [0]
queens([0],4)
partial = []
2, 3 3
queens([0],4) item = 1 :
[0] [1]
partial = [1]
possibleItems = [2,3] - 1 0
queens([1],4)
item = 2 : [0, 2] [0, 3] [1, 3]
partial = [] queens([1],4)
partial = [0,2]
item = 2 :
queens([0,2],4) possibleItems = [3] - 2
partial = [2]
partial = [0] item = 3 : [0, 3, 1] [1, 3, 0]
queens([2],4)
item = 3 : partial = [1,3]
partial = []
partial = [0,3] queens([1,3],4)
item = 3 :
queens([0,3],4) partial = [1] [1, 3, 0, 2]
partial = [3]
partial = [0]
queens([3],4) queens([1,3],4)
partial = []
possibleItems = [0]
item = 0 :
queens([0,2],4) queens([0,3],4) partial = [1,3,0]
possibleItems = [] possibleItems = [1] queens([1,3,0],4)
updateSolution([0,2],4) item = 1 : partial = [1,3]
partial = [0,3,1]
queens([0,3,1],4) queens([1,3,0],4)
partial = [0,3]
possibleItems = [2]
queens([1,3,0,2],4) item = 2 :
possibleItems = [] partial = [1,3,0,2]
queens([0,3,1],4) updateSolution([1,3,0,2],4) queens([1,3,0,2],4)
possibleItems = [] partial = [1,3,0]
updateSolution([0,3,1],4) def queens(partial, board_size):
possibleItems = getPossible(partial, board_size)
print(partial, " || ", possibleItems)
if possibleItems == []:
updateSolution(partial, board_size)
else:
for item in possibleItems:
partial.append(item)
queens(partial, board_size)
partial.pop()
108
return True
Backtracking reduces considered
configurations substantially…
…yet is still an exponential time
algorithm
…yet is still an exponential time
algorithm
General Backtracking
Implementation
Backtracking
General approach
• Find a representation of partial solutions such that all valid options for
augmenting a partial solution can be identified as well as when a
partial solution is complete
• Extend problem to find finding all solutions sols(part) that can be
reached by a sequence of valid decisions from a specific partial
solution part
• Construct all solutions via partial solutions using recurrence relation:
sols(part)=sols(part+[a1])+…+sols(part+[ak])
where [a1,…,ak] are all valid options to augment part
def options(part):
…
𝑉 = {0, … , 5}
1 𝐸 = { 0,1 , 0,2 , (0,3)
0 5 1,2 , 1,3 , 1,5 ,
2,4 , 3,5 , 4,5 }
4
2
Definitions:
• A path is a non-self-intersecting sequence of successively
connected vertices.
Recall some graph concepts
3
𝑉 = {0, … , 5}
1 𝐸 = { 0,1 , 0,2 , (0,3)
0 5 1,2 , 1,3 , 1,5 ,
2,4 , 3,5 , 4,5 }
4
2
Definitions:
• A path is a non-self-intersecting sequence of successively
connected vertices. E.g., [3, 1, 2, 4]
Recall some graph concepts
3
𝑉 = {0, … , 5}
1 𝐸 = { 0,1 , 0,2 , (0,3)
0 5 1,2 , 1,3 , 1,5 ,
2,4 , 3,5 , 4,5 }
4
2
Definitions:
• A path is a non-self-intersecting sequence of successively
connected vertices. E.g., [3, 1, 2, 4]
• A cycle is path with same start and end vertex.
Recall some graph concepts
3
𝑉 = {0, … , 5}
1 𝐸 = { 0,1 , 0,2 , (0,3)
0 5 1,2 , 1,3 , 1,5 ,
2,4 , 3,5 , 4,5 }
4
2
Definitions:
• A path is a non-self-intersecting sequence of successively
connected vertices. E.g., [3, 1, 2, 4]
• A cycle is path with same start and end vertex. E.g., [0, 3, 1, 2, 0]
Recall some graph concepts
3
𝑉 = {0, … , 5}
1 𝐸 = { 0,1 , 0,2 , (0,3)
0 5 1,2 , 1,3 , 1,5 ,
2,4 , 3,5 , 4,5 }
4
2
Definitions:
• A path is a non-self-intersecting sequence of successively
connected vertices. E.g., [3, 1, 2, 4]
• A cycle is path with same start and end vertex. E.g., [0, 3, 1, 2, 0]
• A Hamiltonian cycle is a cycle that contains all vertices
Recall some graph concepts
3
𝑉 = {0, … , 5}
1 𝐸 = { 0,1 , 0,2 , (0,3)
0 5 1,2 , 1,3 , 1,5 ,
2,4 , 3,5 , 4,5 }
4
2
Definitions:
• A path is a non-self-intersecting sequence of successively
connected vertices. E.g., [3, 1, 2, 4]
• A cycle is path with same start and end vertex. E.g., [0, 3, 1, 2, 0]
• A Hamiltonian cycle is a cycle that contains all vertices
Not every graph has Hamiltonian cycle
3 3
?
1 1
0 5 0 5
4 4
2 2
Definitions:
• A path is a non-self-intersecting sequence of successively
connected vertices. E.g., [3, 1, 2, 4]
• A cycle is path with same start and end vertex. E.g., [0, 3, 1, 2, 0]
• A Hamiltonian cycle is a cycle that contains all vertices
Puzzle Variant of Traveling
Salesperson Problem 3
3
?
1 1
0 5 0 5
4 4
2 2
2 4
3 3
3
[2,3,5] [1,4] [1,5]
1 1
1 0 5 0 5
0 5
2 4 2 4
2 4
3 3
3
[4] [5] [3,4]
1 1
1 0 5 0 5
0 5
2 4 2 4
2 4
When is a partial solution
complete?
3
[5]
1
0 5
2 4
3
[3]
1
0 5
2 4
3
[]
1
0 5
2 4
When is a partial solution
complete?
3 3
[5] [3,4]
1 1
0 5 0 5
2 4 2 4
3 3
[3] []
1 1
0 5 0 5
2 4 2 4
3
[]
1
0 5
2 4
When is a partial solution
complete?
3 3 3
[5] [3,4] [4]
1 1 1
0 5 0 5 0 5
4 2 4 2 4
2
3 3 3
[3] [] []
1 1 1
0 5 0 5 0 5
2 4 2 4 2 4
3
[]
def complete(part):
1
0 5 return len(part) == len(graph) \
and graph[part[-1]][0]:
2 4
Putting everything together
def hamiltonian_cycles(graph):
def completed(part):
return len(part) == len(graph) \
and graph[part[-1]][0]
def options(part):
res = []
path = [0] + part
for v in neighbours(path[-1], graph):
if v not in path:
res += [v]
return res
3 3 3 3
1 1 1 1
0 5 0 5 0 5 0 5 …
2 4 2 4 2 4 2 4
Brute force solution
def cycle(vertices, graph):
for i in range(len(vertices)):
if not graph[vertices[i-1]][vertices[i]]:
return False
return True
def brute_force_hamiltonian_cycles(graph):
perms = permutations(1, len(graph))
res = []
for perm in perms:
walk = [0] + perm
if cycle(walk, graph):
res += [walk]
return res
3
2 4
Brute force solution
def cycle(vertices, graph):
for i in range(len(vertices)):
if not graph[vertices[i-1]][vertices[i]]:
return False
return True
def brute_force_hamiltonian_cycles(graph):
perms = permutations(1, len(graph))
res = []
for perm in perms:
walk = [0] + perm
if cycle(walk, graph):
res += [walk]
return res
3 7
2 4 9
Brute force solution
def cycle(vertices, graph):
for i in range(len(vertices)):
if not graph[vertices[i-1]][vertices[i]]:
return False
return True
def brute_force_hamiltonian_cycles(graph):
perms = permutations(1, len(graph))
res = []
for perm in perms:
walk = [0] + perm
if cycle(walk, graph):
res += [walk]
return res
3 7
0 4
0 1
0 1 0 1
0 9 4 13
00 01 10 11
0 1 0 1 0 1 0
0 10 9 19 4 14 13
00 0 00 1 01 0 01 1 10 0 10 1 11 0
0 1 0 0 0 0 0 0
0 20 10 9 19 4 14 13
00 0 0 00 01 00 1 0 01 0 0 01 1 0 10 0 0 10 1 0 11 0 0
0 1 0 0 1 0 1 0 0 1 0 10 1
… … … … … … … …
Complete solutions are of length n
0 4
0 1
0 1 0 1
0 9 4 13
00 01 10 11
0 1 0 1 0 1 0
0 10 9 19 4 14 13
00 0 00 1 01 0 01 1 10 0 10 1 11 0
0 1 0 0 0 0 0 0
… … … … … … …
0 1 15 16
0 0 0 0 0 0 0 0 0 0 0 1 … 1 1 0 0 1 0 1 1 0 0 1 1
Let’s use our backtracking function
def knapsack(values, weights, capacity):
def weight(part):
…
def value(part):
…
def options(part):
if weight(part)+weights[len(part)] <= capacity:
return [0, 1]
else:
return [0]
def completed(part):
return len(part) == len(values)
4kg 9kg 10kg 20kg 2kg 1kg 6kg 11kg 7kg 15kg
$400 $1800 $3500 $4000 $1000 $200 $700 $3600 $800 $1500
Number of solutions (bitlists): 1024
Number of feasible solutions: 158
Gain depends on number of items, weights, and capacity
Idea for further improvement: branch-and-bound
(upper bound best possible value with partial solution)
Further Reading
Levitin, Ch 21.1: Backtracking
140
Coming Up
• NP-completeness
• Revision