COMP3010: Algorithm Theory and Design
Daniel Sutantyo, Department of Computing, Macquarie University
3.3 - Recursive Backtracking
3.3 - Recursive Backtracking
Recursive backtracking
-
Recursive backtracking (or just backtracking) is a technique to produce a solution by iterating through all possible configurations of the search space
- i.e. what we have been doing in the previous video
- At each node of the search tree we check if we have the solution:
- if yes, great and we can stop
- unless we have to find more solutions
- if not, then we can try to extend our solution
- if we cannot go anywhere else, then we backtrack, going to a more primitive solution and starting again from there
- if yes, great and we can stop
Generating Combinations - 3-Letter Combinations
3.3 - Recursive Backtracking
"a"
""
"ab"
"a"
"b"
""
"abc"
"ab"
"ac"
"a"
"bc"
"b"
"c"
a ?
b ?
c ?
""
""
d ?
"abd"
"ab"
"bcd"
"bc"
"d"
""
... ...
... ...
(and so on)
Generating Combinations - 3-Letter Combinations
3.3 - Recursive Backtracking
// generate combinations of length 'size' of the characters in the array 'characters'
public static void generate_combinations_of_size(char[] characters, int i,
ArrayList<Character> result, int size) {
// if i reaches the end of the array, we're done
if (i >= characters.length) {
// print it out if we have the required size
if (result.size() == size) System.out.println(result);
}
else {
// current element
Character ch = characters[i];
// recurse, include the i-th character
result.add(ch);
generate_combinations_of_size(characters,i+1,result,limit);
// recurse, don't include the i-th character
result.remove(ch);
generate_combinations_of_size(characters,i+1,result,limit);
}
}
Generating Combinations - Rod Making
3.3 - Recursive Backtracking
public static boolean solve(int[] b, int i, int L) {
// if L == 0, we are done
if (L == 0){
return true;
}
// if we get to the end of array but still can't make the sum,
// return false
if (i >= b.length) {
return false;
}
// else keep on going with the remaining sum
return solve(b,i+1,L-b[i]) || solve(b,i+1,L);
}
// recurse, include the i-th character
result.add(ch);
generate_combinations_of_size(characters,i+1,result,limit);
// recurse, don't include the i-th character
result.remove(ch);
generate_combinations_of_size(characters,i+1,result,limit);
Generating Combinations
3.3 - Recursive Backtracking
Generating Combinations
3.3 - Recursive Backtracking
"a"
""
"ab"
"a"
"b"
""
"abc"
"ab"
"ac"
"a"
"bc"
"b"
"c"
a ?
b ?
c ?
""
""
d ?
"abcd"
(and so on)
"abcde"
e ?
Generating Combinations
3.3 - Recursive Backtracking
Generating Combinations
3.3 - Recursive Backtracking
// generate combinations of length 'size' of the characters in the array 'characters'
public static void generate_combinations_of_size(char[] characters, int i,
ArrayList<Character> result, int size) {
// if i reaches the end of the array, we're done
if (i >= characters.length) {
// print it out if we have the required size
if (result.size() == size) System.out.println(result);
}
else {
// current element
Character ch = characters[i];
// recurse, include the i-th character
result.add(ch);
generate_combinations_of_size(characters,i+1,result,limit);
// recurse, don't include the i-th character
result.remove(ch);
generate_combinations_of_size(characters,i+1,result,limit);
}
}
Generating Permutations
3.3 - Recursive Backtracking
// generate all permutations of the characters in the set h
public static void generate_permutation_using_set(HashSet<Character> h,
ArrayList<Character> result) {
// no character left in the set, so we are done
if (h.isEmpty()) {
System.out.println(result);
}
HashSet<Character> temp = new HashSet<Character>(h);
for(Character c : temp) {
// choose a character to add
h.remove(c);
result.add(c);
generate_permutation_using_set(h,result);
// backtracking
result.remove(c);
h.add(c);
}
}
Generating Permutations
3.3 - Recursive Backtracking
"a"
"b"
"z"
"c"
""
"d"
... ...
""
"a"
"b"
"y"
"c"
"d"
... ...
"b"
"c"
"z"
"d"
... ...
HashSet<Character> temp = new HashSet<Character>(h);
for(Character c : temp) {
// choose a character to add
h.remove(c);
result.add(c);
generate_permutation_using_set(h,result);
// backtracking
result.remove(c);
h.add(c);
}
Generating Permutations
3.3 - Recursive Backtracking
// generate all permutations of the array result
// start with call to generate_permutation(result,0,result.length-1)
// where result is the array that you want to permute
public static void generate_permutation(char[] result, int start, int end) {
// print out the result
if (start == end)
System.out.println(result);
else {
for(int i = start; i <= end; i++) {
// swap the elements in start and i
swap(result,start,i);
generate_permutation(result,start+1,end);
swap(result,start,i);
}
}
}
Generating Permutations
3.3 - Recursive Backtracking
- Example: A B C D
- A B C D
- B C D
- C D
- D print [ A B C D ]
- D C
- C print [ A B D C ]
- C D
- C B D
- B D
- D print [ A C B D ]
- D B
- B print [ A C D B ]
- B D
- B C D
- A B C D
for(int i = start; i <= end; i++) {
swap(result,start,i);
generate_permutation(result,start+1,end);
swap(result,start,i);
}
Generating Permutations
3.3 - Recursive Backtracking
for(int i = start; i <= end; i++) {
swap(result,start,i);
generate_permutation(result,start+1,end);
swap(result,start,i);
}
ABCD
ABCD
BACD
DBCA
CBAD
ABCD
ACBD
ADCB
ABCD
ABDC
BACD
BCAD
BDCA
CBAD
CABD
CDAB
DBCA
DCBA
DACB
DACB
DABC
(swap A with A)
(swap A with B)
(swap A with C)
(swap A with D)
B ~ B
B ~ C
B ~ D
C ~ C
C ~ D
A ~ A
A ~ C
A ~ D
B ~ B
B ~ A
B ~ D
B ~ B
B ~ C
B ~ D
C ~ C
C ~ B
3.3 - Recursive Backtracking
Recursive backtracking
- Recursive backtracking is essentially just a DFS traversal on the search space
- Can you do BFS instead?
"a"
""
"ab"
"a"
"b"
""
"abc"
"ab"
"ac"
"a"
"bc"
"b"
"c"
a ?
b ?
c ?
""
""
3.3 - Recursive Backtracking
Recursive backtracking
- Recursive backtracking is essentially just a DFS traversal on the search space
- Can you do BFS instead?
- Remember that we want to generate all combinations of n bits, e.g. 000, 001, 010, 011, etc
- We add a bit on every level
"a"
""
"ab"
"a"
"b"
""
"abc"
"ab"
"ac"
"a"
"bc"
"b"
"c"
a ?
b ?
c ?
""
""
1
0
11
10
01
11
111
110
101
100
011
010
011
000
3.3 - Recursive Backtracking
Recursive backtracking
- DFS: a \(\rightarrow\) ab \(\rightarrow\) abc \(\rightarrow\) abcd \(\rightarrow \dots \rightarrow\) (backtrack) \(\rightarrow\) ab
- BFS :
- generate a
- generate ab, a, b
- generate abc, ab, etc
"a"
""
"ab"
"a"
"b"
""
"abc"
"ab"
"ac"
"a"
"bc"
"b"
"c"
a ?
b ?
c ?
""
""
1
0
11
10
01
11
111
110
101
100
011
010
011
000
3.3 - Recursive Backtracking
Recursive backtracking
- BFS: you can just generate 111, 0111, 1101, 1011, 1110
- i.e. generate bits of increasing length
- generally DFS with recursion is just easier
"a"
""
"ab"
"a"
"b"
""
"abc"
"ab"
"ac"
"a"
"bc"
"b"
"c"
a ?
b ?
c ?
""
""
1
0
11
10
01
11
111
110
101
100
011
010
011
000
3.3 - Recursive Backtracking
Branch and Bound
- The code we had was inefficient:
// if i reaches the end of the array, we're done
if (i >= characters.length) {
// print it out if we have the required size
if (result.size() == size) System.out.println(result);
}
"a"
""
"ab"
"a"
"b"
""
"abc"
"ab"
"ac"
"a"
"bc"
"b"
"c"
a ?
b ?
c ?
""
""
d ?
"abcd"
3.3 - Recursive Backtracking
Branch and Bound
- Quick fix:
// generate combinations of length 'size' of the characters in the array 'characters'
public static void generate_combinations_of_size(char[] characters, int i,
ArrayList<Character> result, int size) {
// if we already have more than 'size' characters in result, then we don't need
// to go any further
if (result.size() > size)
return;
// if i reaches the end of the array, we're done
if (i >= characters.length) {
// print it out if we have the required size
if (result.size() == size) System.out.println(result);
}
else { ... }
}
3.3 - Recursive Backtracking
Branch and Bound
- Four things to watch out for:
- Have I got my answer?
- Should I stop here? Should I move on
- Can I go any further? Am I at the end?
- Can I improve my answer?
- Add things, remove things?
- Is there any point of improving my answer?
- Have I got my answer?
Example - Rod Making
3.3 - Recursive Backtracking
public static boolean solve(int[] b, int i, int L) {
// if L == 0, we are done
if (L == 0){
return true;
}
// if we get to the end of array but still can't make the sum,
// return false
if (i >= b.length) {
return false;
}
// else keep on going with the remaining sum
return solve(b,i+1,L-b[i]) || solve(b,i+1,L);
}
Example - Rod Making
3.3 - Recursive Backtracking
[ 10 , 12 , 5 , 7 , 11 ] L = 25
[ 12 , 5 , 7 , 11 ] L = 15
[ 5 , 7 , 11 ] L = 3
pick 12
don't pick 12
...
...
...
...
...
...
[ 5 , 7 , 11 ] L = 15
...
pick 10
pick 12
don't pick 10
don't pick 12
...
[ 12 , 5 , 7 , 11 ] L = 25
[ 5 , 7 , 11 ] L = 13
[ 5 , 7 , 11 ] L = 25
Example - Rod Making
3.3 - Recursive Backtracking
[ 50 , 2 , 18 , 11 , 9 , 23 , 5 , 10 , 30 , 6 , 17 ] L = 27
[ 2 , 18 , 11 , ... , 17 ] L = -23
[ 2 , 18 , 11 , ... , 17 ] L = 27
[18 , 11 , ... , 17 ] L = -25
pick 2
don't pick 2
...
...
...
...
...
...
[ 18 , 11, ... , 17] L = -23
[18 , 11, ... , 17 ] L = 25
[ 18 , 11 , ... , 17 ] L = 27
...
...
pick 50
pick 2
don't pick 50
don't pick 2
Branch and Bound
3.3 - Recursive Backtracking
- Some literature make a distinction between backtracking and branch-and-bound (or pruning, or early exit)
- The idea is the same, you perform a DFS on the search tree, and at every node, you can make the decision to stop and backtrack
- backtracking is a more general, you generally keep on going until it is no longer feasible (e.g. searching problem)
- branch-and-bound adds an additional stopping constraint: if you have found a better solution (e.g. optimisation problem)
Branch and Bound
3.3 - Recursive Backtracking
- With branch-and-bound method, we can apply a bounding function at every node to see if there is any point in doing further recursion
- If there is no point in doing further recursions, then we can prune the search tree, reducing the number of cases that we need to evaluate
- I have also seen pruning function and feasibility function being used instead of bounding function
Branch and Bound
3.3 - Recursive Backtracking
public static boolean solve(int[] b, int i, int L) {
// if L == 0, we are done
if (L == 0){
return true;
}
// if we get to the end of array but still can't make the sum,
// return false
if (i >= b.length) {
return false;
}
// else keep on going with the remaining sum
return solve(b,i+1,L-b[i]) || solve(b,i+1,L);
}
public static boolean solve(int[] b, int i, int L) {
// if L == 0, we are done
if (L == 0){
return true;
}
// if L is negative or we get to the end of the array
// return false
if (i >= b.length || L < 0) {
return false;
}
// else keep on going with the remaining sum
return solve(b,i+1,L-b[i]) || solve(b,i+1,L);
}
Branch and Bound
3.3 - Recursive Backtracking
- Be careful not to write an inefficient pruning function
- our pruning function in the previous example is just an integer comparison, so it is O(1)
- can you think of a bad pruning function?
- our pruning function in the previous example is just an integer comparison, so it is O(1)
public static boolean solve(int[] b, int i, int L) {
// if L == 0, we are done
if (L == 0){
return true;
}
// if L is negative or we get to the end of the array
// return false
if (i >= b.length || L < 0) {
return false;
}
// else keep on going with the remaining sum
return solve(b,i+1,L-b[i]) || solve(b,i+1,L);
}
Branch and Bound
3.3 - Recursive Backtracking
public static boolean solve(int[] b, int i, int L) {
// if L == 0, we are done
if (L == 0){
return true;
}
// if L is negative or we get to the end of the array
// return false
if (i >= b.length || L < 0 || sum(b,i) < L) {
return false;
}
// else keep on going with the remaining sum
return solve(b,i+1,L-b[i]) || solve(b,i+1,L);
}
General Improvements
3.3 - Recursive Backtracking
[ 50 , 2 , 18 , 11 , 9 , 23 , 5 , 10 , 30 , 6 , 17 ] L = 27
[ 2 , 18 , 11 , ... , 17 ] L = -23
[ 2 , 18 , 11 , ... , 17 ] L = 27
[18 , 11 , ... , 17 ] L = -25
pick 2
don't pick 2
[ 18 , 11, ... , 17] L = -23
[18 , 11, ... , 17 ] L = 25
[ 18 , 11 , ... , 17 ] L = 27
...
pick 50
pick 2
don't pick 50
don't pick 2
- Can you make some more improvement?
- this algorithm \(O(2^n)\), so there's probably not much more you can do about the code, but can change the data to make the pruning more efficient?
General Improvements
3.3 - Recursive Backtracking
- If you sort the input in descending order, we can prune earlier:
-
[ 50, 2, 18, 11, 9, 23, 5, 10, 30, 6, 17 ] L = 27
- we are going to try a lot of combinations starting with 2
-
[ 50, 30, 23, 18, 17, 11, 10, 9, 6, 5, 2 ] L = 27
- we pretty much ignore the first 2 entries
- we get our answer quite early
-
[ 50, 2, 18, 11, 9, 23, 5, 10, 30, 6, 17 ] L = 27
General Improvements
3.3 - Recursive Backtracking
- So should we always sort the data?
- [ 50, 30, 23, 18, 17, 11, 10, 9, 6, 5, 2 ]
- [ 50, 2, 18, 11, 9, 23, 5, 10, 30, 6, 17 ]
- L = 27, which input gives you the smaller search tree?
-
L = 12, which input gives you the smaller search tree?
- We cannot be sure that sorting the input in descending order will always give the best performance
- but it probably is a good idea
- this is a form of heuristics
Heuristics
3.3 - Recursive Backtracking
- "Heuristic search algorithms have an air of voodoo about them" - Skiena, page 247
- A heuristic is a problem solving approach that is practical, but not guaranteed to be optimal (or even rational)
- educated guess, rule of thumb, common sense, experience
- the most fundamental form of heuristic is trial and error
- you can read more about heuristics in Chapter 7 of Skiena
Heuristics
3.3 - Recursive Backtracking
- How should you pick your next case?
- random sampling
- greedy method (pick best value for now) - sorting is a bit like this
- prior knowledge or experience
- do a quick local search
- simulated annealing
General Improvements
3.3 - Recursive Backtracking
- Work out what a good pruning function is
- use heuristics
- Use precomputation (will discuss in the workshop)
- See if you can work backward (will also discuss in the workshop)
- these are my input, what kind of search space do I have
- this is the search space, what kind of input can produce this
Summary
3.3 - Recursive Backtracking
- Recursive backtracking
- Code using DFS approach
- Branch and bound, Pruning function
- Heuristics and other general improvements
- Brute-force method can actually be hard
COMP3010 - 3.3 - Recursive Backtracking
By Daniel Sutantyo
COMP3010 - 3.3 - Recursive Backtracking
Recursive backtracking and branch-and-bound
- 148