sudoku.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import copy
  2. from contextlib import suppress
  3. from heapq import heappush, heappop
  4. import matplotlib.pyplot as plt
  5. # return a board that is like the board b, but has domains for each element of b (always 1-9)
  6. def genDomains(b):
  7. for row in range(0, 9):
  8. for cell in range(0, 9):
  9. if (b[row][cell] == 0):
  10. b[row][cell] = list(range(1, 10))
  11. return b
  12. # returns True if value is valid
  13. def valid(brd, row, col, val):
  14. # check row
  15. if (val in brd[row]):
  16. return False
  17. # check column
  18. for i in range(0, 9):
  19. if (brd[i][col] == val):
  20. return False
  21. # check "box"
  22. rownum = int(row / 3)
  23. colnum = int(col / 3)
  24. for i in range(rownum * 3, rownum * 3 + 3):
  25. for j in range(colnum * 3, colnum * 3 + 3):
  26. if (brd[i][j] == val):
  27. return False
  28. return True
  29. # naive backtracking solver
  30. def naive(start):
  31. working = copy.deepcopy(start) # this is only "filled in values" and 0s
  32. solution = genDomains(start)
  33. # unassigned will be a list of positions we have to fill
  34. unassigned = []
  35. for i in range(0, 9):
  36. for j in range(0, 9):
  37. if (isinstance(solution[i][j], list)):
  38. unassigned.append((i, j))
  39. assumptions = []
  40. if(len(unassigned) == 0):
  41. return (working, 0)
  42. # count assignments
  43. count = 0
  44. # while there are unassigned vars, keep going
  45. while(len(unassigned)):
  46. index = unassigned[-1]
  47. success = False
  48. # iterate over all values in the domain list
  49. while solution[index[0]][index[1]]:
  50. i = solution[index[0]][index[1]].pop()
  51. count += 1
  52. # took too long
  53. if (count >= 10000):
  54. print("took too long")
  55. return 10000
  56. # check if this part of the domain(solution) is valid
  57. if (valid(working, index[0], index[1], i)):
  58. #count += 1
  59. #if (count >= 10000):
  60. # print("took too long")
  61. # return False
  62. solution[index[0]][index[1]].append(i) # keep in the domain
  63. working[index[0]][index[1]] = i
  64. assumptions.append(index)
  65. unassigned.pop()
  66. success = True
  67. break
  68. if (success):
  69. continue
  70. else:
  71. # restore domain to full since we failed
  72. solution[index[0]][index[1]] = list(range(1, 10))
  73. working[index[0]][index[1]] = 0
  74. lastdex = assumptions.pop()
  75. solution[lastdex[0]][lastdex[1]].remove(working[lastdex[0]][lastdex[1]])
  76. working[lastdex[0]][lastdex[1]] = 0
  77. unassigned.append(lastdex)
  78. # if we exit without assigning everything, we should have failed
  79. if (unassigned): return 10000
  80. return count
  81. # returns a board (domains) where inferences are made for the cell at row, col
  82. def infer(solutions, brd, row, col, val):
  83. domains = copy.deepcopy(solutions)
  84. # remove from same row & col
  85. for i in range(0, 9):
  86. if (val in domains[row][i] and i != col):
  87. domains[row][i].remove(val)
  88. if (val in domains[i][col] and i != row):
  89. domains[i][col].remove(val)
  90. # remove for "box"
  91. rownum = int(row / 3)
  92. colnum = int(col / 3)
  93. for i in range(rownum * 3, rownum * 3 + 3):
  94. for j in range(colnum * 3, colnum * 3 + 3):
  95. if (val in domains[i][j] and (i != row and j != col)):
  96. domains[i][j].remove(val)
  97. return domains
  98. # generates domains in a format supporting forward checking
  99. def gen2Domains(b):
  100. for row in range(0, 9):
  101. for cell in range(0, 9):
  102. if (b[row][cell] == 0):
  103. b[row][cell] = list(range(1, 10))
  104. else:
  105. b[row][cell] = [b[row][cell]]
  106. return b
  107. # recursive solver for forward-checking
  108. def solve(working, domains, unassigned, count):
  109. if (not unassigned):
  110. return (True, count)
  111. index = unassigned.pop()
  112. # for every value in the domain, check if using it works. if all fail, backtrack.
  113. for i in domains[index[0]][index[1]]:
  114. working[index[0]][index[1]] = i
  115. newdomains = infer(domains, working, index[0], index[1], i)
  116. domains[index[0]][index[1]].remove(i)
  117. count += 1
  118. # took too long
  119. if (count >= 10000):
  120. print("took too long")
  121. return (True, 10000)
  122. # check for invalidated nodes (empty domains)
  123. flag = True
  124. result = False
  125. for i in range(0, 9):
  126. for j in range(0, 9):
  127. if (len(newdomains[i][j]) <= 0):
  128. flag = False
  129. if (flag): result = solve(working, newdomains, copy.deepcopy(unassigned), count)
  130. if (not result):
  131. return (False, count)
  132. if (result[0]):
  133. return result
  134. else:
  135. #domains[index[0]][index[1]].remove(i)
  136. count = result[1]
  137. return (False, count)
  138. # forward checking solver
  139. def forward(start):
  140. working = copy.deepcopy(start) # this is only "filled in values" and 0s
  141. domains = gen2Domains(working)
  142. # unassigned will be a list of positions we have to fill
  143. unassigned = []
  144. for i in range(0, 9):
  145. for j in range(0, 9):
  146. if (len(domains[i][j]) == 9):
  147. unassigned.append((i, j))
  148. # forward-checking on pre-assigned values
  149. for i in range(0, 9):
  150. for j in range(0, 9):
  151. if (working[i][j] != 0):
  152. domains = infer(domains, working, i, j, working[i][j])
  153. result = solve(working, domains, unassigned, 0)
  154. return result[1]
  155. #if (result[0]): return result[1]
  156. #else: return 10000
  157. # returns size of domain for a given index
  158. def domsize(domains, index):
  159. return (len(domains[index[0]][index[1]]))
  160. # returns the # of 0s that are in the same row, col, or box as index
  161. def related(brd, index):
  162. count = 0
  163. # count 0s in row + col
  164. for i in range(0, 9):
  165. if (brd[index[0]][i] == 0 and i != index[1]):
  166. ++count
  167. if (brd[i][index[1]] == 0 and i != index[0]):
  168. ++count
  169. # count for "box" as well
  170. rownum = int(index[0] / 3)
  171. colnum = int(index[1] / 3)
  172. for i in range(rownum * 3, rownum * 3 + 3):
  173. for j in range(colnum * 3, colnum * 3 + 3):
  174. if (brd[i][j] == 0 and (i != index[0] and j != index[1])):
  175. ++count
  176. return count
  177. # returns the # of constraints that will follow from assigning index with val
  178. def lcv(solutions, index, val):
  179. count = 0
  180. # count 0s in row + col
  181. for i in range(0, 9):
  182. if (val in solutions[index[0]][i] and i != index[1]):
  183. ++count
  184. if (val in solutions[i][index[1]] and i != index[0]):
  185. ++count
  186. # count for "box" as well
  187. rownum = int(index[0] / 3)
  188. colnum = int(index[1] / 3)
  189. for i in range(rownum * 3, rownum * 3 + 3):
  190. for j in range(colnum * 3, colnum * 3 + 3):
  191. if (val in solutions[i][j] and (i != index[0] and j != index[1])):
  192. ++count
  193. return count
  194. # return the correct node + val to try
  195. def genVal(domains, working, unassigned):
  196. # used to track intermediary values
  197. heap = []
  198. superheap = []
  199. bestrating = 1.0
  200. # get the best indices according to domain size
  201. for i in unassigned:
  202. rating = domsize(domains, i) / 9.0
  203. if (rating < bestrating):
  204. bestrating = rating
  205. heap = [i]
  206. elif (rating == bestrating):
  207. heap.append(i)
  208. # get the best indices according to degree(related cells)
  209. bestrating = 1
  210. for i in heap:
  211. rating = related(working, i) / 27.0
  212. if (rating < bestrating):
  213. bestrating = rating
  214. superheap = [i]
  215. elif (rating == bestrating):
  216. superheap.append(i)
  217. index = superheap[0]
  218. bestrating = 27
  219. val = working[index[0]][index[1]]
  220. # get best values according to LCV
  221. for i in domains[index[0]][index[1]]:
  222. rating = lcv(domains, index, i)
  223. if (rating <= bestrating):
  224. bestrating = rating
  225. val = i
  226. return (index, val)
  227. # recursive solver that uses heuristics to decide what node to explore
  228. def solveh(working, domains, unassigned, count):
  229. if (not unassigned):
  230. return (True, count)
  231. # while there are unassigned values keep trying
  232. while(unassigned):
  233. # get next value using heuristics, remove this node from assigned
  234. nextThing = genVal(domains, working, unassigned)
  235. index = nextThing[0]
  236. val = nextThing[1]
  237. working[index[0]][index[1]] = val
  238. unassigned.remove(index)
  239. # check for invalidated nodes (empty domain)
  240. flag = True
  241. result = False
  242. newdomains = infer(domains, working, index[0], index[1], val)
  243. for i in range(0, 9):
  244. for j in range(0, 9):
  245. if (not newdomains[i][j]):
  246. flag = False
  247. count += 1
  248. # took too long
  249. if (count >= 10000):
  250. print("took too long")
  251. return (False, count)
  252. # success! recurse
  253. if (flag): result = solveh(working, newdomains, copy.deepcopy(unassigned), count)
  254. if (not result): pass
  255. elif (result[0]):
  256. return result
  257. elif (len(domains[index[0]][index[1]]) > 1): # remove from domain, keep going
  258. working[index[0]][index[1]] = 0
  259. domains[index[0]][index[1]].remove(val)
  260. unassigned.append(index)
  261. if (flag): count = result[1]
  262. else: # no values worked :( return false
  263. if (flag): return (False, result[1])
  264. return (False, count)
  265. # forward checking solver with heuristics
  266. def heuristic(start):
  267. working = copy.deepcopy(start) # this is only "filled in values" and 0s
  268. domains = gen2Domains(start)
  269. # unassigned will be a list of positions we have to fill
  270. unassigned = []
  271. for i in range(0, 9):
  272. for j in range(0, 9):
  273. if (len(domains[i][j]) == 9):
  274. unassigned.append((i, j))
  275. # initial inferences
  276. for i in range(0, 9):
  277. for j in range(0, 9):
  278. if (working[i][j] != 0):
  279. domains = infer(domains, working, i, j, working[i][j])
  280. result = solveh(working, domains, unassigned, 0)
  281. if (result[0]): return result[1]
  282. else: return 10000
  283. def main():
  284. plt.ioff()
  285. plt.switch_backend('agg')
  286. averages = []
  287. bverages = []
  288. cverages = []
  289. for i in range(1, 72):
  290. avgA = 0
  291. avgB = 0
  292. avgC = 0
  293. for j in range(1, 11):
  294. filepath = "sudoku_problems/" + str(i) + "/" + str(j) + ".sd"
  295. # import board
  296. with open(filepath) as file:
  297. board = file.read().splitlines()
  298. board = board[:-1]
  299. # convert to list of list of ints
  300. for l in board:
  301. board[board.index(l)] = list(map(lambda x: int(x), l.split()))
  302. avgA += naive(copy.deepcopy(board));print(i, j)
  303. avgB += forward(copy.deepcopy(board));print(i, j)
  304. avgC += heuristic(copy.deepcopy(board));print(i, j)
  305. averages.append(avgA / 10.0)
  306. bverages.append(avgB / 10.0)
  307. cverages.append(avgC / 10.0)
  308. figure, axes = plt.subplots(1, 1, True)
  309. axes.plot(range(1, 72), averages, label='Naive Algorithm')
  310. axes.plot(range(1, 72), bverages, label='Forward-Checking Algorithm')
  311. axes.plot(range(1, 72), cverages, label='Heuristics')
  312. axes.legend()
  313. plt.xlabel("Number of Initial Valued Filled In")
  314. plt.ylabel("Average Number of Variable Assignments in 10 Runs")
  315. plt.savefig("graph.pdf")
  316. main()