Skip to content

Commit 0b00eb0

Browse files
committed
Remove complex callback logic.
This commit restores the performance optimization from 7ad5848, which was accidentally broken in e5485f6 -- the next commit! It goes back to a simpler way to factor out the `search` logic between the three use cases: 1. find the solution to a problem when there's exactly one 2. find all solutions to a problem 3. determine if there's more than one solution to a problem and to abort the search as soon as a solution is found in cases 2 and 3. Previously, the callback was in charge of two aspects of the logic (recording solutions and determining whether to abort the search). Now these responsibilities are split between two arguments, and that's easier to understand.
1 parent 8b1bc4a commit 0b00eb0

15 files changed

Lines changed: 107 additions & 95 deletions

File tree

go/sudoku/cmd/sudoku/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ func solve(estimate bool, format string, input string, output string, multiple b
163163
if err != nil {
164164
return err
165165
}
166-
solutions, difficulty := sudoku.Solve(&grid)
166+
solutions, difficulty := sudoku.Solve(&grid, multiple)
167167
if len(solutions) == 1 {
168168
grid = solutions[0]
169169
err = writeGrid(grid, format, output)
@@ -181,7 +181,7 @@ func solve(estimate bool, format string, input string, output string, multiple b
181181
}
182182
}
183183
} else {
184-
return fmt.Errorf("multiple solutions found (%d)", len(solutions))
184+
return errors.New("multiple solutions found")
185185
}
186186
}
187187
if estimate {

go/sudoku/cmd/sudoku/main_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func TestSolve(t *testing.T) {
120120
checkFileContents(t, []string{"sudoku", "solve", "-i", inputFile, "-o", outputFile}, outputFile, solutionConsole)
121121

122122
// Runtime errors
123-
testDispatch(t, []string{"sudoku", "solve", problem2}, "", 1, "", "^multiple solutions found (6)\n$")
123+
testDispatch(t, []string{"sudoku", "solve", problem2}, "", 1, "", "^multiple solutions found\n$")
124124
testDispatch(t, []string{"sudoku", "solve", problem3}, "", 1, "", "^no solution found\n$")
125125
testDispatch(t, []string{"sudoku", "solve", "-m", problem3}, "", 1, "", "^no solution found\n$")
126126
testDispatch(t, []string{"sudoku", "solve", "ABC"}, "", 1, "", "cannot read problem: cell contains invalid value")

go/sudoku/generator.go

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,26 +52,21 @@ func (g *Grid) minimize() float64 {
5252
for _, cell := range rand.Perm(81) {
5353
g[cell], value = uint8(0), g[cell]
5454
var s solver
55-
var solved bool
55+
var grids []Grid
5656
s.init()
57-
if s.load(g) && s.search(func(_ *Grid) bool {
58-
// Another solution was already found, abort.
59-
if solved {
60-
return false
61-
}
62-
// First solution is found, continue.
63-
solved = true
64-
return true
65-
}) {
57+
if !s.load(g) {
58+
panic("minimize expects a valid grid")
59+
}
60+
grids = s.search(grids, false)
61+
if len(grids) == 0 {
62+
panic("minimize expects a valid grid")
63+
} else if len(grids) == 1 {
6664
// Only one solution was found.
6765
difficulty = s.difficulty()
6866
} else {
6967
// More than one solution was found, restore cell.
7068
g[cell] = value
7169
}
72-
if !solved {
73-
panic("minimize expects a valid grid")
74-
}
7570
}
7671

7772
return difficulty

go/sudoku/generator_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestGenerate(t *testing.T) {
2626
t.Errorf("seed = %d: expected between 50 and 65 zeroes, got %d", seed, zeroCount)
2727
}
2828

29-
solutions, solveDifficulty := Solve(&grid)
29+
solutions, solveDifficulty := Solve(&grid, false)
3030
if len(solutions) != 1 {
3131
t.Errorf("seed = %d: more than one solution", seed)
3232
}

go/sudoku/server.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ func handleProblem(w http.ResponseWriter, r *http.Request) {
150150
http.Error(w, err.Error(), 400)
151151
return
152152
}
153-
solutions, difficulty := Solve(&grid)
153+
solutions, difficulty := Solve(&grid, false)
154154
if len(solutions) == 0 {
155155
http.Error(w, "no solution found", 400)
156156
return
@@ -169,7 +169,7 @@ func handleSolution(w http.ResponseWriter, r *http.Request) {
169169
http.Error(w, err.Error(), 400)
170170
return
171171
}
172-
solutions, difficulty := Solve(&grid)
172+
solutions, difficulty := Solve(&grid, false)
173173
if len(solutions) == 0 {
174174
http.Error(w, "no solution found", 400)
175175
return

go/sudoku/solver.go

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -124,16 +124,16 @@ func (s *solver) mark(cell int, value uint8) bool {
124124

125125
// search finds all solutions.
126126
//
127-
// Each solution is reported by calling callback.
127+
// It appends solutions to the grids slice and returns that slice.
128128
//
129-
// If callback returns true, search continues and eventually returns true when
130-
// the search completes.
131-
//
132-
// If callback returns false, search aborts and returns false immediately.
133-
func (s *solver) search(callback func(*Grid) bool) bool {
129+
// When multiple is false, search aborts as soon as two solutions are found.
130+
// This is useful to know whether there's zero, one, or several solutions.
131+
// When multiple is true, it looks for all solutions.
132+
func (s *solver) search(grids []Grid, multiple bool) []Grid {
134133
// If the grid is complete, there is a solution in this branch.
135134
if s.progress == 81 {
136-
return callback(&s.grid)
135+
grids = append(grids, s.grid)
136+
return grids
137137
}
138138

139139
// Since s.next is empty, sharing the underlying array with a copy is OK.
@@ -151,15 +151,17 @@ func (s *solver) search(callback func(*Grid) bool) bool {
151151
if s.conflicts[cell]&(1<<value) == 0 {
152152
copy = *s
153153
if copy.mark(cell, value) {
154-
if !copy.search(callback) {
154+
grids = copy.search(grids, multiple)
155+
// Abort search when two solutions are found and we don't look for more.
156+
if !multiple && len(grids) > 1 {
155157
s.steps = copy.steps
156-
return false
158+
break
157159
}
158160
}
159161
s.steps = copy.steps
160162
}
161163
}
162-
return true
164+
return grids
163165
}
164166

165167
// candidate find the cell with the most conflicts.
@@ -193,17 +195,19 @@ func (s *solver) difficulty() float64 {
193195
return math.Log(math.Max(float64(s.steps)/81, 1)) + 1
194196
}
195197

196-
// Solve a grid. Return a slice of 0, 1, or several solutions, and an estimate
197-
// of how difficult the grid is.
198-
func Solve(g *Grid) ([]Grid, float64) {
198+
// Solve a grid.
199+
//
200+
// Return a slice of 0, 1, or several solutions, and an estimate of how
201+
// difficult the grid is.
202+
//
203+
// When multiple is false and there are multiple solutions, stop searching as
204+
// soon as two solutions are found.
205+
func Solve(g *Grid, multiple bool) ([]Grid, float64) {
199206
var s solver
200207
var grids []Grid
201208
s.init()
202209
if s.load(g) {
203-
s.search(func(g *Grid) bool {
204-
grids = append(grids, *g)
205-
return true
206-
})
210+
grids = s.search(grids, multiple)
207211
}
208212
return grids, s.difficulty()
209213
}

go/sudoku/solver_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func TestSolve(t *testing.T) {
5959
}
6060

6161
for _, test := range tests {
62-
gotSolutions, gotDifficulty := Solve(&test.input)
62+
gotSolutions, gotDifficulty := Solve(&test.input, true)
6363
if len(gotSolutions) != len(test.wantSolutions) {
6464
t.Errorf("%v: expected %d solution(s), got %d", test.input, len(test.wantSolutions), len(gotSolutions))
6565
continue
@@ -110,6 +110,6 @@ func BenchmarkSolve(b *testing.B) {
110110
}
111111
for n := 0; n < b.N; n++ {
112112
grid := grids[n%len(grids)]
113-
Solve(&grid)
113+
Solve(&grid, false)
114114
}
115115
}

python/_sudoku/generator.c

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -102,25 +102,16 @@ void random_grid(uint8_t grid[]) {
102102
memcpy(grid, s.grid, sizeof(s.grid));
103103
}
104104

105-
static bool minimize_callback(uint8_t grid[], void *solved) {
106-
// Another solution was already found, abort.
107-
if (*(bool *)solved) {
108-
return false;
109-
}
110-
// First solution is found, continue.
111-
*(bool *)solved = true;
112-
return true;
113-
}
114-
115105
// minimize turns a solution into a problem by removing values from cells.
116106
double minimize(uint8_t grid[]) {
117107
size_t order[81];
118108
size_t i;
119109
size_t cell;
120110
uint8_t value;
121-
bool solved;
122111
solver s;
123112
size_t next[81];
113+
bool loaded;
114+
int solutions;
124115
double difficulty = 0;
125116

126117
// Clear cells until this creates multiple solutions.
@@ -129,17 +120,18 @@ double minimize(uint8_t grid[]) {
129120
cell = order[i];
130121
value = grid[cell];
131122
grid[cell] = (uint8_t)0;
132-
solved = false;
133123
solver_init(&s, next);
134-
if (solver_load(&s, grid) &&
135-
solver_search(&s, &minimize_callback, (void *)&solved)) {
124+
loaded = solver_load(&s, grid);
125+
assert(loaded);
126+
solutions = solver_search(&s, NULL, false);
127+
assert(solutions > 0);
128+
if (solutions == 1) {
136129
// Only one solution was found.
137130
difficulty = solver_difficulty(&s);
138131
} else {
139132
// More than one solution was found, restore cell.
140133
grid[cell] = value;
141134
}
142-
assert(solved);
143135
}
144136

145137
return difficulty;

python/_sudoku/module.c

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,11 @@
77
#include "solver.h"
88
#include "utils.h"
99

10-
static bool solve_callback(uint8_t values[], void *grids) {
11-
PyGILState_STATE gstate;
12-
gstate = PyGILState_Ensure();
13-
14-
PyObject *grid = Grid_AsPyObject(values);
15-
if (grid == NULL) {
16-
PyGILState_Release(gstate);
17-
return false;
18-
}
19-
if (PyList_Append((PyObject *)grids, grid) < 0) {
20-
PyGILState_Release(gstate);
21-
return false;
22-
}
23-
24-
PyGILState_Release(gstate);
25-
return true;
26-
}
27-
2810
static PyObject *
2911
_sudoku_solve(PyObject *self, PyObject *args, PyObject *kwds) {
30-
static char *kwlist[] = {"grid", NULL};
12+
static char *kwlist[] = {"grid", "multiple", NULL};
3113
PyObject *grid;
14+
bool multiple = false;
3215
uint8_t values[81];
3316
PyObject *grids;
3417
solver s;
@@ -37,7 +20,7 @@ _sudoku_solve(PyObject *self, PyObject *args, PyObject *kwds) {
3720
PyObject *result;
3821

3922
// borrows a reference to grid
40-
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &grid)) {
23+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|p", kwlist, &grid, &multiple)) {
4124
return NULL;
4225
}
4326

@@ -53,14 +36,13 @@ _sudoku_solve(PyObject *self, PyObject *args, PyObject *kwds) {
5336
Py_BEGIN_ALLOW_THREADS
5437
solver_init(&s, next);
5538
if (solver_load(&s, values)) {
56-
solver_search(&s, solve_callback, grids);
39+
if (solver_search(&s, grids, multiple) < 0) {
40+
Py_DECREF(grids);
41+
return NULL;
42+
}
5743
}
5844
Py_END_ALLOW_THREADS
59-
// solve_callback may cause a Python error.
60-
if (PyErr_Occurred()) {
61-
Py_DECREF(grids);
62-
return NULL;
63-
}
45+
6446
difficulty = solver_difficulty(&s);
6547

6648
result = Py_BuildValue("Od", grids, difficulty);
@@ -96,7 +78,7 @@ _sudoku_generate(PyObject *self, PyObject *args) {
9678

9779
static PyMethodDef _sudoku_methods[] = {
9880
{"solve", (PyCFunction)_sudoku_solve, METH_VARARGS | METH_KEYWORDS,
99-
PyDoc_STR("Solve a grid.\n\nReturn a list of 0, 1, or several solutions.")},
81+
PyDoc_STR("Solve a grid.")},
10082
{"generate", (PyCFunction)_sudoku_generate, METH_VARARGS,
10183
PyDoc_STR("Create a random problem.")},
10284
{NULL, NULL, 0, NULL}

python/_sudoku/solver.c

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,35 @@ static size_t solver_candidate(solver *s) {
186186
return candidate;
187187
}
188188

189-
bool solver_search(solver *s, bool callback(uint8_t[], void *), void *arg) {
189+
static bool record_solution(PyObject *grids, uint8_t values[]) {
190+
// Re-acquire the GIL, which is released when solver_search runs.
191+
PyGILState_STATE gstate;
192+
gstate = PyGILState_Ensure();
193+
194+
PyObject *grid = Grid_AsPyObject(values);
195+
if (grid == NULL) {
196+
PyGILState_Release(gstate);
197+
return false;
198+
}
199+
if (PyList_Append(grids, grid) < 0) {
200+
PyGILState_Release(gstate);
201+
return false;
202+
}
203+
204+
PyGILState_Release(gstate);
205+
return true;
206+
}
207+
208+
// Return the number of solutions found, or -1 if an error occurred.
209+
int solver_search(solver *s, PyObject *grids, bool multiple) {
190210
// If the grid is complete, there is a solution in this branch.
191211
if (s->progress == 81) {
192-
return callback(s->grid, arg);
212+
if (grids != NULL) {
213+
if (!record_solution(grids, s->grid)) {
214+
return -1;
215+
};
216+
}
217+
return 1;
193218
}
194219

195220
// Since next is empty, sharing the underlying array with a copy is OK.
@@ -202,19 +227,28 @@ bool solver_search(solver *s, bool callback(uint8_t[], void *), void *arg) {
202227
solver copy;
203228
uint8_t value;
204229
size_t cell = solver_candidate(s);
230+
int solutions = 0;
231+
int new_solutions;
205232
for (value = 1; value < 10; value++) {
206233
if ((s->conflicts[cell] & ((uint16_t)1 << value)) == 0) {
207234
memcpy(&copy, s, sizeof(solver));
208235
if (solver_mark(&copy, cell, value)) {
209-
if (!solver_search(&copy, callback, arg)) {
236+
new_solutions = solver_search(&copy, grids, multiple);
237+
if (new_solutions < 0) {
210238
s->steps = copy.steps;
211-
return false;
239+
return new_solutions;
240+
}
241+
solutions += new_solutions;
242+
// Abort search when two solutions are found and we don't look for more.
243+
if (!multiple && solutions > 1) {
244+
s->steps = copy.steps;
245+
return solutions;
212246
}
213247
}
214248
s->steps = copy.steps;
215249
}
216250
}
217-
return true;
251+
return solutions;
218252
}
219253

220254
double solver_difficulty(solver *s) {

0 commit comments

Comments
 (0)