Skip to content

Commit a0ebd9c

Browse files
Merge pull request #22 from DoctorLai/sudoku
Sudoku
2 parents 54398dd + bf73245 commit a0ebd9c

6 files changed

Lines changed: 333 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- name: Install build tools
2121
run: |
2222
sudo apt-get update
23-
sudo apt-get install -y build-essential g++-14 clang clang-format
23+
sudo apt-get install -y build-essential g++-14 clang clang-format libtbb-dev
2424
2525
# 3. Clang-format check
2626
- name: Clang-format Check

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Examples include (and will expand to):
3535
* Encoding
3636
* [rot47](./rot47/)
3737
* [prefix-sum](./prefix-sum/)
38+
* [sudoku-solver](./sudoku-solver/)
3839
* [pi-monte-carlo](./pi-monte-carlo/)
3940
* [pi](./pi)
4041
* Data Structures

parallel-transform/Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ include ../common.mk
33

44
# per-example flags
55
# CXXFLAGS += -pthread
6+
LDLIBS += -ltbb
67

78
## get it from the folder name
89
TARGET := $(notdir $(CURDIR))
@@ -12,7 +13,7 @@ OBJS := $(SRCS:.cpp=.o)
1213
all: $(TARGET)
1314

1415
$(TARGET): $(OBJS)
15-
$(CXX) $(CXXFLAGS) -o $@ $^
16+
$(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS)
1617

1718
%.o: %.cpp
1819
$(CXX) $(CXXFLAGS) -c $< -o $@

sudoku-solver/Makefile

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# pull in shared compiler settings
2+
include ../common.mk
3+
4+
# per-example flags
5+
# CXXFLAGS += -pthread
6+
7+
## get it from the folder name
8+
TARGET := $(notdir $(CURDIR))
9+
## all *.cpp files in this folder
10+
SRCS := $(wildcard *.cpp)
11+
OBJS := $(SRCS:.cpp=.o)
12+
13+
all: $(TARGET)
14+
15+
$(TARGET): $(OBJS)
16+
$(CXX) $(CXXFLAGS) -o $@ $^
17+
18+
%.o: %.cpp
19+
$(CXX) $(CXXFLAGS) -c $< -o $@
20+
21+
run: $(TARGET)
22+
./$(TARGET) $(ARGS)
23+
24+
clean:
25+
rm -f $(OBJS) $(TARGET)
26+
27+
# Delegates to top-level Makefile
28+
check-format:
29+
$(MAKE) -f ../Makefile check-format DIR=$(CURDIR)
30+
31+
.PHONY: all clean run check-format

sudoku-solver/main.cpp

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/**
2+
DFS Sudoku solver CLI.
3+
4+
Input:
5+
argv[1]: 81-char board string. Use digits 1-9 for filled cells and '.' or '0' for empty.
6+
argv[2] (optional): max number of solutions to print (positive integer).
7+
8+
Output:
9+
Prints each full solution as a single 81-char line.
10+
*/
11+
12+
#include <array>
13+
#include <cstdint>
14+
#include <iostream>
15+
#include <optional>
16+
#include <string>
17+
#include <string_view>
18+
#include <vector>
19+
20+
namespace {
21+
22+
struct SudokuState
23+
{
24+
std::array<int, 81> cells{};
25+
std::array<uint16_t, 9> row_mask{};
26+
std::array<uint16_t, 9> col_mask{};
27+
std::array<uint16_t, 9> box_mask{};
28+
};
29+
30+
constexpr uint16_t kAllDigitsMask = 0x03FE; // bits 1..9
31+
32+
int
33+
box_index(int row, int col)
34+
{
35+
return (row / 3) * 3 + (col / 3);
36+
}
37+
38+
bool
39+
place_digit(SudokuState& state, int index, int digit)
40+
{
41+
const int row = index / 9;
42+
const int col = index % 9;
43+
const int box = box_index(row, col);
44+
const uint16_t bit = static_cast<uint16_t>(1u << digit);
45+
46+
if ((state.row_mask[row] & bit) != 0 || (state.col_mask[col] & bit) != 0 || (state.box_mask[box] & bit) != 0) {
47+
return false;
48+
}
49+
50+
state.cells[index] = digit;
51+
state.row_mask[row] |= bit;
52+
state.col_mask[col] |= bit;
53+
state.box_mask[box] |= bit;
54+
return true;
55+
}
56+
57+
void
58+
remove_digit(SudokuState& state, int index, int digit)
59+
{
60+
const int row = index / 9;
61+
const int col = index % 9;
62+
const int box = box_index(row, col);
63+
const uint16_t bit = static_cast<uint16_t>(1u << digit);
64+
65+
state.cells[index] = 0;
66+
state.row_mask[row] &= static_cast<uint16_t>(~bit);
67+
state.col_mask[col] &= static_cast<uint16_t>(~bit);
68+
state.box_mask[box] &= static_cast<uint16_t>(~bit);
69+
}
70+
71+
uint16_t
72+
candidate_mask(const SudokuState& state, int index)
73+
{
74+
const int row = index / 9;
75+
const int col = index % 9;
76+
const int box = box_index(row, col);
77+
const uint16_t used = static_cast<uint16_t>(state.row_mask[row] | state.col_mask[col] | state.box_mask[box]);
78+
return static_cast<uint16_t>(kAllDigitsMask & static_cast<uint16_t>(~used));
79+
}
80+
81+
int
82+
popcount16(uint16_t x)
83+
{
84+
int count = 0;
85+
while (x != 0) {
86+
x = static_cast<uint16_t>(x & static_cast<uint16_t>(x - 1));
87+
++count;
88+
}
89+
return count;
90+
}
91+
92+
std::optional<SudokuState>
93+
parse_board(std::string_view input)
94+
{
95+
if (input.size() != 81) {
96+
return std::nullopt;
97+
}
98+
99+
SudokuState state{};
100+
for (size_t i = 0; i < input.size(); ++i) {
101+
const char c = input[i];
102+
if (c == '.' || c == '0') {
103+
state.cells[i] = 0;
104+
continue;
105+
}
106+
if (c < '1' || c > '9') {
107+
return std::nullopt;
108+
}
109+
110+
const int digit = c - '0';
111+
if (!place_digit(state, static_cast<int>(i), digit)) {
112+
return std::nullopt;
113+
}
114+
}
115+
116+
return state;
117+
}
118+
119+
std::string
120+
board_to_string(const SudokuState& state)
121+
{
122+
std::string out;
123+
out.reserve(81);
124+
for (int value : state.cells) {
125+
out.push_back(static_cast<char>('0' + value));
126+
}
127+
return out;
128+
}
129+
130+
bool
131+
choose_next_cell(const SudokuState& state, int& out_index)
132+
{
133+
int best_index = -1;
134+
int best_count = 10;
135+
136+
for (int i = 0; i < 81; ++i) {
137+
if (state.cells[i] != 0) {
138+
continue;
139+
}
140+
141+
const uint16_t mask = candidate_mask(state, i);
142+
const int count = popcount16(mask);
143+
144+
if (count == 0) {
145+
out_index = -1;
146+
return true;
147+
}
148+
if (count < best_count) {
149+
best_count = count;
150+
best_index = i;
151+
if (best_count == 1) {
152+
break;
153+
}
154+
}
155+
}
156+
157+
out_index = best_index;
158+
return false;
159+
}
160+
161+
void
162+
solve_dfs(SudokuState& state, std::vector<std::string>& solutions, size_t max_solutions)
163+
{
164+
if (max_solutions != 0 && solutions.size() >= max_solutions) {
165+
return;
166+
}
167+
168+
int index = -1;
169+
const bool dead_end = choose_next_cell(state, index);
170+
171+
if (dead_end) {
172+
return;
173+
}
174+
175+
if (index == -1) {
176+
solutions.push_back(board_to_string(state));
177+
return;
178+
}
179+
180+
uint16_t mask = candidate_mask(state, index);
181+
while (mask != 0) {
182+
const uint16_t bit = static_cast<uint16_t>(mask & static_cast<uint16_t>(-static_cast<int16_t>(mask)));
183+
184+
int digit = 1;
185+
while ((bit & static_cast<uint16_t>(1u << digit)) == 0) {
186+
++digit;
187+
}
188+
189+
if (place_digit(state, index, digit)) {
190+
solve_dfs(state, solutions, max_solutions);
191+
remove_digit(state, index, digit);
192+
193+
if (max_solutions != 0 && solutions.size() >= max_solutions) {
194+
return;
195+
}
196+
}
197+
198+
mask = static_cast<uint16_t>(mask & static_cast<uint16_t>(mask - 1));
199+
}
200+
}
201+
202+
std::optional<size_t>
203+
parse_positive_limit(const std::string& s)
204+
{
205+
if (s.empty()) {
206+
return std::nullopt;
207+
}
208+
209+
size_t value = 0;
210+
for (char c : s) {
211+
if (c < '0' || c > '9') {
212+
return std::nullopt;
213+
}
214+
value = value * 10 + static_cast<size_t>(c - '0');
215+
}
216+
217+
if (value == 0) {
218+
return std::nullopt;
219+
}
220+
221+
return value;
222+
}
223+
224+
void
225+
print_usage(const char* program)
226+
{
227+
std::cerr << "Usage: " << program << " <81-char-board> [max_solutions]\\n"
228+
<< "Board chars: 1-9 for fixed cells, '.' or '0' for empty cells.\\n";
229+
}
230+
231+
} // namespace
232+
233+
int
234+
main(int argc, char* argv[])
235+
{
236+
if (argc < 2 || argc > 3) {
237+
print_usage(argv[0]);
238+
return 0;
239+
}
240+
241+
size_t max_solutions = 0; // 0 means unlimited
242+
if (argc == 3) {
243+
const auto parsed_limit = parse_positive_limit(argv[2]);
244+
if (!parsed_limit.has_value()) {
245+
std::cerr << "Error: max_solutions must be a positive integer.\\n";
246+
return 1;
247+
}
248+
max_solutions = *parsed_limit;
249+
}
250+
251+
auto parsed_state = parse_board(argv[1]);
252+
if (!parsed_state.has_value()) {
253+
std::cerr << "Error: invalid board input. Expected 81 chars and no row/column/box conflicts.\\n";
254+
return 1;
255+
}
256+
257+
SudokuState state = *parsed_state;
258+
std::vector<std::string> solutions;
259+
solve_dfs(state, solutions, max_solutions);
260+
261+
if (solutions.empty()) {
262+
std::cout << "No solutions found.\\n";
263+
return 0;
264+
}
265+
266+
std::cout << "Found " << solutions.size() << " solution(s).\\n";
267+
for (const auto& solution : solutions) {
268+
std::cout << solution << "\\n";
269+
}
270+
271+
return 0;
272+
}

sudoku-solver/tests.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/bin/bash
2+
3+
set -ex
4+
5+
puzzle="53..7....6..195....98....6.8...6...34..8.3..17...2...6.6....28....419..5....8..79"
6+
expected="534678912672195348198342567859761423426853791713924856961537284287419635345286179"
7+
8+
output=$(./sudoku-solver "$puzzle")
9+
if ! echo "$output" | grep -Fq "$expected"; then
10+
echo "Test failed: expected solution was not found"
11+
exit 1
12+
fi
13+
14+
invalid="11..............................................................................."
15+
if ./sudoku-solver "$invalid" >/dev/null 2>&1; then
16+
echo "Test failed: contradictory board should be rejected as invalid input"
17+
exit 1
18+
fi
19+
20+
limited_output=$(./sudoku-solver "$puzzle" 1)
21+
if ! echo "$limited_output" | grep -Fq "Found 1 solution(s)."; then
22+
echo "Test failed: max_solutions limit did not work"
23+
exit 1
24+
fi
25+
26+
echo "All tests passed"

0 commit comments

Comments
 (0)