Project Euler Problem #96

Problem #96 concerns solving Sudoku grids. The question reads:

I must confess, this is one of the problems where I’m not really sure why my solution is as efficient as it is. It took me a lot of troubleshooting to find a solution which could solve all of the grids in a reasonable amount of time. Here is my solution:

Solution #1: Brute Force Approach

As always, we store the grids in a file and then read from the file to store the grids in arrays. As it turns out, the method I always used to solve Sudoku grids is not sufficient to solve every grid. My typical method is to look for cells that only certain digits can fill first. If none exist, then I search for rows, columns, and squares where only certain cells can have certain digits. These two methods are generally enough to solve reasonable Sudoku grids, and when I implemented them, they were able to solve roughly 40 of the grids in this problem in no time at all. For the other 10 grids, I resorted to brute force. Starting at the top of the grid and working down, I made guesses for each cell and looked for solutions with each of those guesses. If no solution existed, I would update the guess, and if no obvious solution existed, I would make another guess. This took a lot of trouble shooting to implement efficiently, and I am a bit confused why it is suddenly able to solve all of the grids quickly. Here is an implementation of this approach in Python 2.7:

 '''
 Author: Walker Kroubalkian
 Backtracking Brute Force Approach to Project Euler Problem #96
 '''
 
 import time
 
 def stringToArray(myString):
     a = []
     for x in myString:
         a.append(int(x))
     return a
 
 f = open("PE96Sudoku.txt","r")
 
 if f.mode=="r":
     contents = f.readlines()
     realContents = []
     i = 0
     for x in contents:
         if(i%10!=0):
             if x[len(x)-1]=="\n":
                 realContents.append(stringToArray(x[0:len(x)-1]))
             else:
                 realContents.append(stringToArray(x))
         i+=1
 else:
     raise ValueError("Cannot read from file")
 
 def countZeros(n):
     t = 0
     for x in n:
         for y in x:
             if y==0:
                 t+=1
     return t
 
 def copyArray(n):
     t = []
     for x in n:
         r = []
         for y in x:
             r.append(y)
         t.append(r)
     return t
 
 def optionCell(n,r,c):
     if(n[r][c]!=0):
         return []
     notPoss = []
     for x in range(9):
         notPoss.append(n[r][x])
         notPoss.append(n[x][c])
     lr = r-r%3
     tc = c-c%3
     for a in range(3):
         for b in range(3):
             notPoss.append(n[lr+a][tc+b])
     options = []
     for d in range(1,10):
         if d not in notPoss:
             options.append(d)
     return options
 
 def optionRow(n,r):
     rowOptions = []
     for c in range(9):
         rowOptions.append(optionCell(n,r,c))
     return rowOptions
 
 def potentialOptions(n):
     p = []
     for r in range(9):
         p.append(optionRow(n,r))
     return p
 
 def possibleGrid(n):
     p = potentialOptions(n)
     for r in range(9):
         for c in range(9):
             if n[r][c]==0 and len(p[r][c]) == 0:
                 return False
     return p
 
 def solveGrid(n):
     total = countZeros(n)
     v = possibleGrid(n)
     if v == False:
         return False
     while total>0:
         found = False
         for r in range(9):
             for c in range(9):
                 if len(v[r][c]) == 1 and n[r][c] == 0:
                     a = v[r][c][0]
                     n[r][c] = a
                     for x in range(9):
                         d = v[r][x]
                         if a in d:
                             d.remove(a)
                             v[r][x] = d
                     for x in range(9):
                         d = v[x][c]
                         if a in d:
                             d.remove(a)
                             v[x][c] = d
                     tr = r-r%3
                     lc = c-c%3
                     for x in range(3):
                         for y in range(3):
                             d = v[tr+x][lc+y]
                             if a in d:
                                 d.remove(a)
                                 v[tr+x][lc+y] = d
                     v[r][c] = []
                     found = True
                     break
             if found:
                 break
         if not found:
             return n
         total-=1
     return n
 
 def solveSudoku(n):
     possible = [n]
     maxZeros = countZeros(n)
     while(maxZeros>0):
         newPoss = []
         for x in possible:
             a = solveGrid(x)
             if a!=False:
                 zeroR = -1
                 zeroC = -1
                 for r in range(9):
                     for c in range(9):
                         if a[r][c]==0:
                             zeroR = r
                             zeroC = c
                 if zeroR>-1:
                     v = optionCell(a, zeroR, zeroC)
                     for z in v:
                         temp = copyArray(a)
                         temp[zeroR][zeroC] = z
                         newPoss.append(temp)
                 else:
                     return a
         possible = newPoss
         if len(possible)==0:
             return False
         for z in possible:
             v3 = countZeros(z)
             if v3==0:
                 return z
             if v3>maxZeros:
                 maxZeros = 3
     return possible[0]
 
 def isCorrectGrid(myGrid):
     for r in range(9):
         theRow = []
         for x in myGrid:
             if x==0 or x in theRow:
                 return False
             else:
                 theRow.append(x)
     for c in range(9):
         theCol = []
         for r in range(9):
             x = myGrid[r][c]
             if x==0 or x in theCol:
                 return False
             else:
                 theCol.append(x)
     for tr in range(0,9,3):
         for lc in range(0,9,3):
             theSquare = []
             for a in range(3):
                 for b in range(3):
                     x = myGrid[tr+a][lc+b]
                     if x==0 or x in theSquare:
                         return False
                     else:
                         theSquare.append(x)
     return True
 
 def projectEulerProblemNinetySix(n):
     l = len(n)
     total = 0
     for x in range(0,l,9):
         grid = n[x:x+9]
         a = solveSudoku(grid)
         if a==False or not isCorrectGrid(a):
             print False
         else:
             total+=100*a[0][0]+10*a[0][1]+a[0][2]
     return total
 
 start = time.time()
 print projectEulerProblemNinetySix(realContents)
 print ("--- %s seconds ---" % (time.time()-start))
 
 '''
 Prints
 
 24702
 --- 2.47313904762 seconds ---
 
 for input of given list of Sudoku Grids.
 ''' 

And with that, we’re done. That took nearly 2.47 seconds to run, so some of the grids really stretched the limits of my algorithm. I would like to see a cleaner algorithm for this problem at some point, and I may come back to this problem in the near future.

Thanks for reading! See you tomorrow.

Published by Walker Kroubalkian

My name is Walker Kroubalkian. I really enjoy math, computer science, and hiking.

Leave a comment

Design a site like this with WordPress.com
Get started