Skip to content

Commit 26a5232

Browse files
committed
Add two sudoku and one N-Queens python solution
1 parent da01e26 commit 26a5232

File tree

3 files changed

+279
-0
lines changed

3 files changed

+279
-0
lines changed

problems/N-Queens/brute_force.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/python
2+
#
3+
# http://en.wikipedia.org/wiki/Eight_queens_puzzle
4+
#
5+
6+
from itertools import permutations
7+
8+
n = 8
9+
cols = range(n)
10+
results = []
11+
12+
for vec in permutations(cols):
13+
# check diagonal attack, for col position, m and n:
14+
# abs(vec[m] - vec[n]) == abs(m - n) if there is diagonal attack
15+
# means:
16+
# a. vec[m] - vec[n] = m - n ==> vec[m] - m == vec[n] - n
17+
# b. vec[m] - vec[n] = n - m ==> vec[m] + m == vec[n] + n
18+
# if exists one pair (m, n), then there is diagonal attack.
19+
if (n == len(set(vec[i]+i for i in cols)) ==
20+
len(set(vec[i]-i for i in cols))):
21+
results.append(vec)
22+
23+
24+
print results
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env python
2+
#
3+
# From http://mehpic.blogspot.com/2011/10/solve-any-sudoku-with-10-lines-of.html
4+
#
5+
6+
input_board = [
7+
7,0,0,4,0,9,0,0,6,
8+
5,0,9,0,8,6,1,2,0,
9+
1,4,0,0,0,0,0,0,0,
10+
6,5,0,8,0,0,0,0,0,
11+
0,2,4,9,0,1,5,6,0,
12+
0,0,0,0,0,2,0,1,8,
13+
0,0,0,0,0,0,0,8,7,
14+
0,7,5,2,4,0,6,0,1,
15+
2,0,0,3,0,7,0,0,5
16+
]
17+
18+
board_size = 9
19+
subsquare_width = 3
20+
subsquare_height = 3
21+
22+
def prettyprint(board, board_size):
23+
print
24+
for i in range(board_size):
25+
print board[i*board_size:i*board_size+board_size]
26+
27+
28+
col_labels = [i%board_size for i in range(len(input_board))]
29+
row_labels = [i/board_size for i in range(len(input_board))]
30+
sqr_labels = [(board_size/subsquare_width)*(row_labels[i]/subsquare_height)+col_labels[i]/subsquare_width for i in range(len(input_board))]
31+
32+
33+
def solve(board):
34+
try:
35+
i = board.index(0)
36+
except:
37+
prettyprint(board, board_size)
38+
return
39+
# find out numbers already used in same row, column or square.
40+
bag = [board[j] for j in filter(lambda x: (col_labels[i]==col_labels[x]) or (row_labels[i]==row_labels[x]) or (sqr_labels[i]==sqr_labels[x]),range(len(board)))]
41+
# get numbers can be used to put in a position
42+
for j in filter(lambda x: x not in bag, range(1,board_size+1)):
43+
# get one number, put in this position, call itself again.
44+
board[i] = j; solve(board); board[i] = 0
45+
46+
solve(input_board)

problems/sudoku/sudoku_norvig.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#!/usr/bin/env python
2+
## Solve Every Sudoku Puzzle
3+
4+
## See http://norvig.com/sudoku.html
5+
6+
## Throughout this program we have:
7+
## r is a row, e.g. 'A'
8+
## c is a column, e.g. '3'
9+
## s is a square, e.g. 'A3'
10+
## d is a digit, e.g. '9'
11+
## u is a unit, e.g. ['A1','B1','C1','D1','E1','F1','G1','H1','I1']
12+
## grid is a grid,e.g. 81 non-blank chars, e.g. starting with '.18...7...
13+
## values is a dict of possible values, e.g. {'A1':'12349', 'A2':'8', ...}
14+
15+
def cross(A, B):
16+
"Cross product of elements in A and elements in B."
17+
return [a+b for a in A for b in B]
18+
19+
digits = '123456789'
20+
rows = 'ABCDEFGHI'
21+
cols = digits
22+
squares = cross(rows, cols)
23+
unitlist = ([cross(rows, c) for c in cols] +
24+
[cross(r, cols) for r in rows] +
25+
[cross(rs, cs) for rs in ('ABC','DEF','GHI') for cs in ('123','456','789')])
26+
units = dict((s, [u for u in unitlist if s in u])
27+
for s in squares)
28+
peers = dict((s, set(sum(units[s],[]))-set([s]))
29+
for s in squares)
30+
31+
################ Unit Tests ################
32+
33+
def test():
34+
"A set of tests that must pass."
35+
assert len(squares) == 81
36+
assert len(unitlist) == 27
37+
assert all(len(units[s]) == 3 for s in squares)
38+
assert all(len(peers[s]) == 20 for s in squares)
39+
assert units['C2'] == [['A2', 'B2', 'C2', 'D2', 'E2', 'F2', 'G2', 'H2', 'I2'],
40+
['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9'],
41+
['A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3']]
42+
assert peers['C2'] == set(['A2', 'B2', 'D2', 'E2', 'F2', 'G2', 'H2', 'I2',
43+
'C1', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9',
44+
'A1', 'A3', 'B1', 'B3'])
45+
print 'All tests pass.'
46+
47+
################ Parse a Grid ################
48+
49+
def parse_grid(grid):
50+
"""Convert grid to a dict of possible values, {square: digits}, or
51+
return False if a contradiction is detected."""
52+
## To start, every square can be any digit; then assign values from the grid.
53+
values = dict((s, digits) for s in squares)
54+
for square,data in grid_values(grid).items():
55+
if data in digits and not assign(values, square, data):
56+
return False ## (Fail if we can't assign data to square.)
57+
return values
58+
59+
def grid_values(grid):
60+
"""Convert grid into a dict of {square: char} with '0' or '.' for empties.
61+
i.e. {'A1': '0', 'A3': '4'}
62+
"""
63+
chars = [c for c in grid if c in digits or c in '0.']
64+
assert len(chars) == 81
65+
return dict(zip(squares, chars))
66+
67+
################ Constraint Propagation ################
68+
69+
def assign(values, s, d):
70+
"""Eliminate all the other values (except d) from values[s] and propagate.
71+
Return values, except return False if a contradiction is detected."""
72+
other_values = values[s].replace(d, '')
73+
if all(eliminate(values, s, d2) for d2 in other_values):
74+
return values
75+
else:
76+
return False
77+
78+
def eliminate(values, s, d):
79+
"""Eliminate d from values[s]; propagate when values or places <= 2.
80+
Return values, except return False if a contradiction is detected."""
81+
if d not in values[s]:
82+
return values ## Already eliminated
83+
values[s] = values[s].replace(d,'')
84+
## (1) If a square s is reduced to one value d2, then eliminate d2 from the peers.
85+
if len(values[s]) == 0:
86+
return False ## Contradiction: removed last value
87+
elif len(values[s]) == 1:
88+
d2 = values[s]
89+
if not all(eliminate(values, s2, d2) for s2 in peers[s]):
90+
return False
91+
## (2) If a unit u is reduced to only one place for a value d, then put it there.
92+
for u in units[s]:
93+
dplaces = [s for s in u if d in values[s]]
94+
if len(dplaces) == 0:
95+
return False ## Contradiction: no place for this value
96+
elif len(dplaces) == 1:
97+
# d can only be in one place in unit; assign it there
98+
if not assign(values, dplaces[0], d):
99+
return False
100+
return values
101+
102+
################ Display as 2-D grid ################
103+
104+
def display(values):
105+
"Display these values as a 2-D grid."
106+
width = 1+max(len(values[s]) for s in squares)
107+
line = '+'.join(['-'*(width*3)]*3)
108+
for r in rows:
109+
print ''.join(values[r+c].center(width)+('|' if c in '36' else '')
110+
for c in cols)
111+
if r in 'CF': print line
112+
print
113+
114+
################ Search ################
115+
116+
def solve(grid): return search(parse_grid(grid))
117+
118+
def search(values):
119+
"Using depth-first search and propagation, try all possible values."
120+
if values is False:
121+
return False ## Failed earlier
122+
if all(len(values[s]) == 1 for s in squares):
123+
return values ## Solved!
124+
## Chose the unfilled square s with the fewest possibilities
125+
n,s = min((len(values[s]), s) for s in squares if len(values[s]) > 1)
126+
return some(search(assign(values.copy(), s, d))
127+
for d in values[s])
128+
129+
################ Utilities ################
130+
131+
def some(seq):
132+
"Return some element of seq that is true."
133+
for e in seq:
134+
if e: return e
135+
return False
136+
137+
def from_file(filename, sep='\n'):
138+
"Parse a file into a list of strings, separated by sep."
139+
return file(filename).read().strip().split(sep)
140+
141+
def shuffled(seq):
142+
"Return a randomly shuffled copy of the input sequence."
143+
seq = list(seq)
144+
random.shuffle(seq)
145+
return seq
146+
147+
################ System test ################
148+
149+
import time, random
150+
151+
def solve_all(grids, name='', showif=0.0):
152+
"""Attempt to solve a sequence of grids. Report results.
153+
When showif is a number of seconds, display puzzles that take longer.
154+
When showif is None, don't display any puzzles."""
155+
def time_solve(grid):
156+
start = time.clock()
157+
values = solve(grid)
158+
t = time.clock()-start
159+
## Display puzzles that take long enough
160+
if showif is not None and t > showif:
161+
display(grid_values(grid))
162+
if values: display(values)
163+
print '(%.2f seconds)\n' % t
164+
return (t, solved(values))
165+
times, results = zip(*[time_solve(grid) for grid in grids])
166+
N = len(grids)
167+
if N > 1:
168+
print "Solved %d of %d %s puzzles (avg %.2f secs (%d Hz), max %.2f secs)." % (
169+
sum(results), N, name, sum(times)/N, N/sum(times), max(times))
170+
else:
171+
print "Solved %d of %d %s puzzles (avg %.2f secs, max %.2f secs)." % (
172+
sum(results), N, name, sum(times)/N, max(times))
173+
174+
def solved(values):
175+
"A puzzle is solved if each unit is a permutation of the digits 1 to 9."
176+
def unitsolved(unit): return set(values[s] for s in unit) == set(digits)
177+
return values is not False and all(unitsolved(unit) for unit in unitlist)
178+
179+
def random_puzzle(N=17):
180+
"""Make a random puzzle with N or more assignments. Restart on contradictions.
181+
Note the resulting puzzle is not guaranteed to be solvable, but empirically
182+
about 99.8% of them are solvable. Some have multiple solutions."""
183+
values = dict((s, digits) for s in squares)
184+
for s in shuffled(squares):
185+
if not assign(values, s, random.choice(values[s])):
186+
break
187+
ds = [values[s] for s in squares if len(values[s]) == 1]
188+
if len(ds) >= N and len(set(ds)) >= 8:
189+
return ''.join(values[s] if len(values[s])==1 else '.' for s in squares)
190+
return random_puzzle(N) ## Give up and make a new puzzle
191+
192+
grid1 = '003020600900305001001806400008102900700000008006708200002609500800203009005010300'
193+
grid2 = '4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......'
194+
hard1 = '.....6....59.....82....8....45........3........6..3.54...325..6..................'
195+
snyder = '.5..2..3.2....17.84.76..........5...52.....47...7..........35.43.65....1.9..7..6.'
196+
197+
if __name__ == '__main__':
198+
test()
199+
solve_all([snyder], "snyder", None)
200+
solve_all(from_file("easy50.txt", '========'), "easy", None)
201+
solve_all(from_file("top95.txt"), "hard", None)
202+
solve_all(from_file("hardest.txt"), "hardest", None)
203+
solve_all([random_puzzle() for _ in range(99)], "random", 100.0)
204+
205+
## References used:
206+
## http://www.scanraid.com/BasicStrategies.htm
207+
## http://www.sudokudragon.com/sudokustrategy.htm
208+
## http://www.krazydad.com/blog/2005/09/29/an-index-of-sudoku-strategies/
209+
## http://www2.warwick.ac.uk/fac/sci/moac/currentstudents/peter_cock/python/sudoku/

0 commit comments

Comments
 (0)