Browse Source

solver for sudoku added. that's it. RIP me

tarfeef101 6 years ago
commit
d1f996285b
1 changed files with 300 additions and 0 deletions
  1. 300 0
      a1/sudoku.py

+ 300 - 0
a1/sudoku.py

@@ -0,0 +1,300 @@
+import sys
+import copy
+from contextlib import suppress
+from heapq import heappush, heappop
+
+board = sys.argv[1]
+algotype = sys.argv[2]
+
+# import board
+with open(board) as file:
+    board = file.read().splitlines()
+    board = board[:-1]
+
+# convert to list of list of ints
+for l in board:
+    board[board.index(l)] = list(map(lambda x: int(x), l.split()))
+
+# return a board that is like the board b, but has domains for each element of b (always 1-9)
+def genDomains(b):
+    for row in range(0, 9):
+        for cell in range(0, 9):
+            if (b[row][cell] == 0):
+                b[row][cell] = list(range(1, 10))
+    return b
+    
+# returns True if value is valid
+def valid(brd, row, col, val):
+    # check row
+    if (val in brd[row]):
+        return False
+
+    # check column
+    for i in range(0, 9):
+        if (brd[i][col] == val):
+            return False
+    
+    # check "box"
+    rownum = int(row / 3)
+    colnum = int(col / 3)
+    
+    for i in range(rownum * 3, rownum * 3 + 3):
+        for j in range(colnum * 3, colnum * 3 + 3):
+            if (brd[i][j] == val):
+                return False
+    
+    return True
+
+# naive backtracking solver
+def naive(start):
+    working = copy.deepcopy(start) # this is only "filled in values" and 0s
+    solution = genDomains(start)
+    # unassigned will be a list of positions we have to fill
+    unassigned = []
+    for i in range(0, 9):
+        for j in range(0, 9):
+            if (isinstance(solution[i][j], list)):
+                unassigned.append((i, j))
+                
+    assumptions = []
+                    
+    if(len(unassigned) == 0):
+        return True
+    
+    while(len(unassigned)):
+        index = unassigned[-1]
+        success = False
+        
+        # iterate over all values in the domain list
+        while solution[index[0]][index[1]]:
+            i = solution[index[0]][index[1]].pop()
+            if (valid(working, index[0], index[1], i)):
+                solution[index[0]][index[1]].append(i) # keep in the domain
+                working[index[0]][index[1]] = i
+                assumptions.append(index)
+                unassigned.pop()
+                success = True
+                break
+        
+        if (success):
+            continue
+        else:
+            # restore domain to full since we failed
+            solution[index[0]][index[1]] = list(range(1, 10))
+            working[index[0]][index[1]] = 0
+            lastdex = assumptions.pop()
+            solution[lastdex[0]][lastdex[1]].remove(working[lastdex[0]][lastdex[1]])
+            working[lastdex[0]][lastdex[1]] = 0
+            unassigned.append(lastdex)
+    
+    
+    if (unassigned): return False
+
+    return working
+
+
+# returns a board (domains) where inferences are made for the cell at row, col
+def infer(solutions, brd, row, col, val):
+    domains = copy.deepcopy(solutions)
+    # remove from same row & col
+    for i in range(0, 9):
+        if (val in domains[row][i] and i != col):
+            domains[row][i].remove(val)
+        if (val in domains[i][col] and i != row):
+            domains[i][col].remove(val)
+            
+    # remove for "box"
+    rownum = int(row / 3)
+    colnum = int(col / 3)
+    
+    for i in range(rownum * 3, rownum * 3 + 3):
+        for j in range(colnum * 3, colnum * 3 + 3):
+            if (val in domains[i][j] and (i != row and j != col)):
+                domains[i][j].remove(val)
+                
+    return domains
+
+
+# generates domains in a format supporting forward checking
+def gen2Domains(b):
+    for row in range(0, 9):
+        for cell in range(0, 9):
+            if (b[row][cell] == 0):
+                b[row][cell] = list(range(1, 10))
+            else:
+                b[row][cell] = [b[row][cell]]
+    return b
+
+
+# recursive solver for forward-checking
+def solve(working, domains, unassigned):
+    if (not unassigned):
+        return working
+    
+    index = unassigned.pop()
+    
+    for i in domains[index[0]][index[1]]:
+        working[index[0]][index[1]] = i
+        newdomains = infer(domains, working, index[0], index[1], i)
+        result = solve(working, newdomains, copy.deepcopy(unassigned))
+        if (result):
+            return result
+        else:
+            continue
+    
+    return False
+
+# forward checking solver
+def forward(start):
+    working = copy.deepcopy(start) # this is only "filled in values" and 0s
+    domains = gen2Domains(start)
+    # unassigned will be a list of positions we have to fill
+    unassigned = []
+    for i in range(0, 9):
+        for j in range(0, 9):
+            if (len(domains[i][j]) == 9):
+                unassigned.append((i, j))
+    
+    for i in range(0, 9):
+        for j in range(0, 9):
+            if (working[i][j] != 0):
+                domains = infer(domains, working, i, j, working[i][j])
+            
+    return solve(working, domains, unassigned)
+
+
+# returns size of domain for a given index
+def domsize(domains, index):
+    return (len(domains[index[0]][index[1]]))
+
+
+# returns the # of 0s that are in the same row, col, or box as index
+def related(brd, index):
+    count = 0
+    # count 0s in row + col
+    for i in range(0, 9):
+        if (brd[index[0]][i] == 0 and i != index[1]):
+            ++count
+        if (brd[i][index[1]] == 0 and i != index[0]):
+            ++count
+            
+    # count for "box" as well
+    rownum = int(index[0] / 3)
+    colnum = int(index[1] / 3)
+    
+    for i in range(rownum * 3, rownum * 3 + 3):
+        for j in range(colnum * 3, colnum * 3 + 3):
+            if (brd[i][j] == 0 and (i != index[0] and j != index[1])):
+                ++count
+                
+    return count
+
+
+# returns the # of constraints that will follow from assigning index with val
+def lcv(solutions, index, val):
+    count = 0
+    # count 0s in row + col
+    for i in range(0, 9):
+        if (val in solutions[index[0]][i] and i != index[1]):
+            ++count
+        if (val in solutions[i][index[1]] and i != index[0]):
+            ++count
+            
+    # count for "box" as well
+    rownum = int(index[0] / 3)
+    colnum = int(index[1] / 3)
+    
+    for i in range(rownum * 3, rownum * 3 + 3):
+        for j in range(colnum * 3, colnum * 3 + 3):
+            if (val in solutions[i][j] and (i != index[0] and j != index[1])):
+                ++count
+                
+    return count
+
+# recursive solver that uses heuristics to decide what node to explore
+def solveh(working, domains, unassigned):
+    if (not unassigned):
+        return working
+    
+    # heap and superheap used to record indices that are the "best" according to the heuristics
+    heap = []
+    superheap = []
+    bestrating = 1.0
+    
+    # get the best indices according to domain size
+    for i in unassigned:
+        rating = domsize(domains, i) / 9
+        if (rating < bestrating):
+            bestrating = rating
+            heap = [i]
+        elif (rating == bestrating):
+            heap.append(i)
+    
+    # get the best indices according to degree(related cells)
+    bestrating = 1
+    for i in heap:
+        rating = related(working, i) / 27
+        if (rating < bestrating):
+            bestrating = rating
+            superheap = [i]
+        elif (rating == bestrating):
+            superheap.append(i)
+    
+    index = superheap[0]
+    bestrating = 27
+    val = 0
+    
+    # get best values according to LCV
+    for i in domains[index[0]][index[1]]:
+        rating = lcv(domains, index, i)
+        if (rating <= bestrating):
+            bestrating = rating
+            val = i
+
+    working[index[0]][index[1]] = val
+    unassigned.remove(index)
+    
+    newdomains = infer(copy.deepcopy(domains), copy.deepcopy(working), index[0], index[1], val)
+    result = solveh(copy.deepcopy(working), copy.deepcopy(newdomains), copy.deepcopy(unassigned))
+    if (result):
+        return result
+    elif (domains[index[0]][index[1]]):
+        working[index[0]][index[1]] = 0
+        domains[index[0]][index[1]].remove(val)
+        unassigned.append(index)
+        result = solveh(copy.deepcopy(working), copy.deepcopy(domains), copy.deepcopy(unassigned))
+        if (result):
+            return result
+    
+    return False
+
+
+# forward checking solver with heuristics
+def heuristic(start):
+    working = copy.deepcopy(start) # this is only "filled in values" and 0s
+    domains = gen2Domains(start)
+    # unassigned will be a list of positions we have to fill
+    unassigned = []
+    for i in range(0, 9):
+        for j in range(0, 9):
+            if (len(domains[i][j]) == 9):
+                unassigned.append((i, j))
+    
+    for i in range(0, 9):
+        for j in range(0, 9):
+            if (working[i][j] != 0):
+                domains = infer(domains, working, i, j, working[i][j])
+            
+    return solveh(working, domains, unassigned)
+
+
+def main():
+    print("###########")
+    print(*board, sep='\n')
+    print("##########")
+    result = heuristic(board)
+    print("###########")
+    print(*result, sep='\n')
+    print("##########")
+    
+main()