# Long Refactoring: Streamline the Algorithm

The code in tree_to_solution_string mixes the logic of solving the puzzle with the management of a linked list. Splitting your attention between these two levels can make it hard to track down errors. To continue teasing these two aspects apart, we need to make heavier use of the iterator. We’re in the middle of it now, and the code might actually feel like it is more of a mess than when we started. That is common, natural, and nothing to be afraid of.

Well, unless we get directed on to a different task tomorrow. Lets finish this up tonight.

Remember to run the tests after every change.

The allocation of new nodes is done in this code:

``` new_node = Tree_Node( self.board_index.table[index], curr_node) new_node.check_solved(test_board) curr_node.next_node = new_node curr_node = new_node```

Lets pull that out into its own function.

And…as we do it, we trip over that board_index table again. I am going to pull that out into a global variable for the moment. I have a feeling that I am going to end up encapsulating it inside our Node class eventually, but we are not there yet.

```--- a/treesudoku/tree_sudoku.py +++ b/treesudoku/tree_sudoku.py @@ -29,15 +29,20 @@ class SudokuSolver: self.board_strings = board_strings self.boards_dict = self.strings_to_board_dict(self.board_strings) self.box_index = BoxIndex() - self.board_index = BoardIndexTable() self.solved_board_strings = dict() for key, value in self.boards_dict.items(): return_string = self.tree_to_solution_string(value) self.solved_board_strings[key] = return_string   def tree_to_solution_string(self, original_board): + def advance(node, test_board): + new_node = Tree_Node( + board_index.table[index], node) + new_node.check_solved(test_board) + node.next_node = new_node + return new_node index = 0 - head_node = Tree_Node(self.board_index.table[index]) + head_node = Tree_Node(board_index.table[index]) curr_node = head_node while index < MAX: curr_board_filling_node = head_node @@ -51,11 +56,8 @@ class SudokuSolver: index += 1 if index >= MAX: continue - new_node = Tree_Node( - self.board_index.table[index], curr_node) - new_node.check_solved(test_board) - curr_node.next_node = new_node - curr_node = new_node + curr_node = advance(curr_node, test_board) + curr_node.check_solved(test_board) else: if len(curr_node.possible_values) == 0: # backtrack @@ -194,6 +196,9 @@ class BoardIndexTable: return return_list     +board_index = BoardIndexTable() + + class Tree_Node: def __init__(self, board_spot, last_node=None, next_node=None): self.possible_values = ['1', '2', '3', '4', '5', '6', '7', '8']```

The advance function belongs in the Node class. I don’t think there is any benefit to keeping it in this interim form, even in git, so I am going to move it straight away. I do have to add an index to it. Still messy.

```--- a/treesudoku/tree_sudoku.py +++ b/treesudoku/tree_sudoku.py @@ -29,7 +29,6 @@ class SudokuSolver: self.board_strings = board_strings self.boards_dict = self.strings_to_board_dict(self.board_strings) self.box_index = BoxIndex() - self.board_index = BoardIndexTable() self.solved_board_strings = dict() for key, value in self.boards_dict.items(): return_string = self.tree_to_solution_string(value) @@ -37,7 +36,7 @@ class SudokuSolver:   def tree_to_solution_string(self, original_board): index = 0 - head_node = Tree_Node(self.board_index.table[index]) + head_node = Tree_Node(board_index.table[index]) curr_node = head_node while index < MAX: curr_board_filling_node = head_node @@ -51,11 +50,8 @@ class SudokuSolver: index += 1 if index >= MAX: continue - new_node = Tree_Node( - self.board_index.table[index], curr_node) - new_node.check_solved(test_board) - curr_node.next_node = new_node - curr_node = new_node + curr_node = curr_node.advance(test_board, index) + curr_node.check_solved(test_board) else: if len(curr_node.possible_values) == 0: # backtrack @@ -194,6 +190,9 @@ class BoardIndexTable: return return_list     +board_index = BoardIndexTable() + + class Tree_Node: def __init__(self, board_spot, last_node=None, next_node=None): self.possible_values = ['1', '2', '3', '4', '5', '6', '7', '8'] @@ -222,6 +221,14 @@ class Tree_Node: self.possible_values = [] self.solved = True   + def advance(self, test_board, index): + node = self + new_node = Tree_Node( + board_index.table[index], node) + new_node.check_solved(test_board) + node.next_node = new_node + return new_node +   start = time.time() solver = SudokuSolver(import_csv())```

Lets do the same with the code that deletes the node.

``` if len(curr_node.possible_values) == 0: # backtrack while len(curr_node.possible_values) == 0: curr_node = curr_node.last_node curr_node.next_node = None index -= 1```

Move that to a helper function….

```index 165948e..8d58d6f 100644 --- a/treesudoku/tree_sudoku.py +++ b/treesudoku/tree_sudoku.py @@ -35,6 +35,11 @@ class SudokuSolver: self.solved_board_strings[key] = return_string   def tree_to_solution_string(self, original_board): + def retreat(node): + node = curr_node.last_node + node.next_node = None + return node + index = 0 head_node = Tree_Node(board_index.table[index]) curr_node = head_node @@ -56,8 +61,7 @@ class SudokuSolver: if len(curr_node.possible_values) == 0: # backtrack while len(curr_node.possible_values) == 0: - curr_node = curr_node.last_node - curr_node.next_node = None + curr_node = retreat(curr_node) index -= 1 curr_node.next() return self.build_solution_string(head_node)```

And then move it to the Node class

```--- a/treesudoku/tree_sudoku.py +++ b/treesudoku/tree_sudoku.py @@ -56,8 +56,7 @@ class SudokuSolver: if len(curr_node.possible_values) == 0: # backtrack while len(curr_node.possible_values) == 0: - curr_node = curr_node.last_node - curr_node.next_node = None + curr_node = curr_node.retreat() index -= 1 curr_node.next() return self.build_solution_string(head_node) @@ -229,6 +228,11 @@ class Tree_Node: node.next_node = new_node return new_node   + def retreat(self): + node = self.last_node + node.next_node = None + return node +   start = time.time() solver = SudokuSolver(import_csv())```

Lets do something about that index. What is clear is that there should only ever be 81 nodes, max, and each node knows its index. Having both the index and the board location is overkill, but you can never have too much overkill. First, lets clear up the parameter list for the Node initialization.

```--- a/treesudoku/tree_sudoku.py +++ b/treesudoku/tree_sudoku.py @@ -36,7 +36,7 @@ class SudokuSolver:   def tree_to_solution_string(self, original_board): index = 0 - head_node = Tree_Node(board_index.table[index]) + head_node = Tree_Node(board_index.table[index], None) curr_node = head_node while index < MAX: curr_board_filling_node = head_node @@ -193,10 +193,9 @@ board_index = BoardIndexTable()     class Tree_Node: - def __init__(self, board_spot, last_node=None, next_node=None): + def __init__(self, board_spot, last_node): self.possible_values = ['1', '2', '3', '4', '5', '6', '7', '8'] self.board_spot = board_spot - self.next_node = next_node self.last_node = last_node self.value = '9' self.solved = False```

Now we can add an index parameter to the end,. Yes, I know I removed it before, I was wrongish.

With the index parameter back, we can encapsulate the BlockIndexTable inside the node object constructor.

` self.board_spot = board_index.table[index]`

And then remove it from the parameter list.

``` --- a/treesudoku/tree_sudoku.py +++ b/treesudoku/tree_sudoku.py @@ -36,7 +36,7 @@ class SudokuSolver:   def tree_to_solution_string(self, original_board): index = 0 - head_node = Tree_Node(board_index.table[index]) + head_node = Tree_Node(None, index) curr_node = head_node while index < MAX: curr_board_filling_node = head_node @@ -193,13 +193,14 @@ board_index = BoardIndexTable()     class Tree_Node: - def __init__(self, board_spot, last_node=None, next_node=None): + def __init__(self, last_node, index): self.possible_values = ['1', '2', '3', '4', '5', '6', '7', '8'] - self.board_spot = board_spot - self.next_node = next_node + self.board_spot = board_index.table[index] self.last_node = last_node + self.next_node = None self.value = '9' self.solved = False + self.index = index   def next(self): self.value = self.possible_values.pop() @@ -222,8 +223,7 @@ class Tree_Node:   def advance(self, test_board, index): node = self - new_node = Tree_Node( - board_index.table[index], node) + new_node = Tree_Node(node, self.index + 1) new_node.check_solved(test_board) node.next_node = new_node return new_node```

```--- a/treesudoku/tree_sudoku.py +++ b/treesudoku/tree_sudoku.py @@ -50,7 +50,7 @@ class SudokuSolver: index += 1 if index &gt;= MAX: continue - curr_node = curr_node.advance(test_board, index) + curr_node = curr_node.advance(test_board) curr_node.check_solved(test_board) else: if len(curr_node.possible_values) == 0: @@ -221,7 +221,7 @@ class Tree_Node: self.possible_values = [] self.solved = True   - def advance(self, test_board, index): + def advance(self, test_board): node = self new_node = Tree_Node(node, self.index + 1) new_node.check_solved(test_board)```

Get rid of the index variable in the code, and use the value from the current node pointer instead. Yeah, I called it a pointer. The one place we have to watch is where it is used for early exit from the loop, but there we can use curr_node.index + 1.

What is trickier is that check at the top.

` while index < MAX:`

This controls the whole loop. But curr_node is not always equal to the maximum node when we reach this point. We can’t just check for curr_node.index < MAX because the continue statement kicks us up here when we pass beyond the last node, we’ll never break. If we use a break statement instead of the the continue, it works.

```--- a/treesudoku/tree_sudoku.py +++ b/treesudoku/tree_sudoku.py @@ -35,10 +35,9 @@ class SudokuSolver: self.solved_board_strings[key] = return_string   def tree_to_solution_string(self, original_board): - index = 0 - head_node = Tree_Node(None, index) + head_node = Tree_Node(None, 0) curr_node = head_node - while index < MAX: + while curr_node.index < MAX: curr_board_filling_node = head_node test_board = copy.deepcopy(original_board) curr_board_filling_node.write(test_board) @@ -47,9 +46,8 @@ class SudokuSolver: curr_board_filling_node.write(test_board)   if self.box_index.is_value_valid(test_board, curr_node): - index += 1 - if index >= MAX: - continue + if curr_node.index + 1 >= MAX: + break curr_node = curr_node.advance(test_board) curr_node.check_solved(test_board) else: @@ -57,7 +55,6 @@ class SudokuSolver: # backtrack while len(curr_node.possible_values) == 0: curr_node = curr_node.retreat() - index -= 1 curr_node.next() return self.build_solution_string(head_node)```

At this point, the statement at the top of the loop will never evaluate to False. We could replace it with while True.

It should be clear by now that the code writes the partial solution on a clean board, and then rolls forward with the next thing it is trying. Now that we have a backtrack function, we could reset the nodes to their default state and try again.

And that would mean we don’t even need the early steps of the algorithm that roll forward. This is a pretty big jump. But the break in the middle of the code is the trigger to show that this is where we can change things

Seems I had a bug in advance. I did not find it until I wrote a unit test.

Here is the unit test change:

```--- a/treesudoku/test_tree_soduku.py +++ b/treesudoku/test_tree_soduku.py @@ -31,7 +31,38 @@ puzzles = { }     +puzzle0 = ("003020600" + + "900305001" + + "001806400" + + "008102900" + + "700000008" + + "006708200" + + "002609500" + + "800203009" + + "005010300") + + def test_sudoku_solver(): solver = tree_sudoku.SudokuSolver(tree_sudoku.import_csv()) for key, solution in solver.solved_board_strings.items(): assert solver.solved_board_strings[key] == puzzles[key] + + +def test_advance(): + test_board = tree_sudoku.build_board(puzzle0) + node = tree_sudoku.Tree_Node(None, 0) + node.write(test_board) + assert(test_board == '9') + node = node.advance(test_board) + node = node.advance(test_board) + node.write(test_board) + assert(test_board == '0') + node = node.advance(test_board) + node.write(test_board) + assert(test_board == '9') + back_node = node.retreat(test_board) + assert(test_board == '0') + assert(node.value == "9") + back_node.write(test_board) + assert(test_board == '3') + assert(back_node.board_spot == '02')```

And here is the change to the algorithm, including the fix for the advance method.

```--- a/treesudoku/tree_sudoku.py +++ b/treesudoku/tree_sudoku.py @@ -38,24 +38,21 @@ class SudokuSolver: head_node = Tree_Node(None, 0) curr_node = head_node test_board = copy.deepcopy(original_board) + curr_node.check_solved(test_board) while True: - curr_board_filling_node = head_node - curr_board_filling_node.write(test_board) - while curr_board_filling_node.next_node: - curr_board_filling_node = curr_board_filling_node.next_node - curr_board_filling_node.write(test_board) - + if not curr_node.solved: + curr_node.write(test_board) if self.box_index.is_value_valid(test_board, curr_node): if curr_node.index + 1 &gt;= MAX: break curr_node = curr_node.advance(test_board) curr_node.check_solved(test_board) else: - if len(curr_node.possible_values) == 0: - # backtrack - while len(curr_node.possible_values) == 0: - curr_node = curr_node.retreat(test_board) + # backtrack + while len(curr_node.possible_values) == 0: + curr_node = curr_node.retreat(test_board) curr_node.next() + curr_node.write(test_board) return self.build_solution_string(head_node)   def build_solution_string(self, head_node): @@ -227,9 +224,9 @@ class Tree_Node: node = self if node.next_node is None: new_node = Tree_Node(node, self.index + 1) - new_node.check_solved(test_board) - node.next_node = new_node - return new_node + new_node.check_solved(test_board) + node.next_node = new_node + return node.next_node   def retreat(self, test_board): node = self```

It took a lot of trial and error to get this to run. Here is roughly what I learned:

The original code was rewriting the potential values on the board. This should not have been necessary if the backtracking was correct. IN general, the code was selecting a value on one trip through the loop, and writing, checking it and backtracking on the next time.

With backtracking erasing the old values, there was no need to re-write the potential answer every time. You also don’t need to free and reallocate the nodes on back track. Right now, the linked list is built on demand in a forward only manner.

We are not done here, not by a long shot, but this is the biggest change in behavior. There should not be a need to change the functioning of the algorithm itself.

Next Up: Completing the iterator

This site uses Akismet to reduce spam. Learn how your comment data is processed.