diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c66c49..fe2b422 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - name: Install build tools run: | sudo apt-get update - sudo apt-get install -y build-essential g++-14 clang clang-format + sudo apt-get install -y build-essential g++-14 clang clang-format libtbb-dev # 3. Clang-format check - name: Clang-format Check diff --git a/README.md b/README.md index d6e0ec6..ae035fe 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Examples include (and will expand to): * Encoding * [rot47](./rot47/) * [prefix-sum](./prefix-sum/) + * [sudoku-solver](./sudoku-solver/) * [pi-monte-carlo](./pi-monte-carlo/) * [pi](./pi) * Data Structures diff --git a/parallel-transform/Makefile b/parallel-transform/Makefile index 172da2f..e51a197 100644 --- a/parallel-transform/Makefile +++ b/parallel-transform/Makefile @@ -3,6 +3,7 @@ include ../common.mk # per-example flags # CXXFLAGS += -pthread +LDLIBS += -ltbb ## get it from the folder name TARGET := $(notdir $(CURDIR)) @@ -12,7 +13,7 @@ OBJS := $(SRCS:.cpp=.o) all: $(TARGET) $(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $@ $^ + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/sudoku-solver/Makefile b/sudoku-solver/Makefile new file mode 100644 index 0000000..ab8b3c0 --- /dev/null +++ b/sudoku-solver/Makefile @@ -0,0 +1,31 @@ +# pull in shared compiler settings +include ../common.mk + +# per-example flags +# CXXFLAGS += -pthread + +## get it from the folder name +TARGET := $(notdir $(CURDIR)) +## all *.cpp files in this folder +SRCS := $(wildcard *.cpp) +OBJS := $(SRCS:.cpp=.o) + +all: $(TARGET) + +$(TARGET): $(OBJS) + $(CXX) $(CXXFLAGS) -o $@ $^ + +%.o: %.cpp + $(CXX) $(CXXFLAGS) -c $< -o $@ + +run: $(TARGET) + ./$(TARGET) $(ARGS) + +clean: + rm -f $(OBJS) $(TARGET) + +# Delegates to top-level Makefile +check-format: + $(MAKE) -f ../Makefile check-format DIR=$(CURDIR) + +.PHONY: all clean run check-format diff --git a/sudoku-solver/main.cpp b/sudoku-solver/main.cpp new file mode 100644 index 0000000..c0c9877 --- /dev/null +++ b/sudoku-solver/main.cpp @@ -0,0 +1,272 @@ +/** +DFS Sudoku solver CLI. + +Input: + argv[1]: 81-char board string. Use digits 1-9 for filled cells and '.' or '0' for empty. + argv[2] (optional): max number of solutions to print (positive integer). + +Output: + Prints each full solution as a single 81-char line. +*/ + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct SudokuState +{ + std::array cells{}; + std::array row_mask{}; + std::array col_mask{}; + std::array box_mask{}; +}; + +constexpr uint16_t kAllDigitsMask = 0x03FE; // bits 1..9 + +int +box_index(int row, int col) +{ + return (row / 3) * 3 + (col / 3); +} + +bool +place_digit(SudokuState& state, int index, int digit) +{ + const int row = index / 9; + const int col = index % 9; + const int box = box_index(row, col); + const uint16_t bit = static_cast(1u << digit); + + if ((state.row_mask[row] & bit) != 0 || (state.col_mask[col] & bit) != 0 || (state.box_mask[box] & bit) != 0) { + return false; + } + + state.cells[index] = digit; + state.row_mask[row] |= bit; + state.col_mask[col] |= bit; + state.box_mask[box] |= bit; + return true; +} + +void +remove_digit(SudokuState& state, int index, int digit) +{ + const int row = index / 9; + const int col = index % 9; + const int box = box_index(row, col); + const uint16_t bit = static_cast(1u << digit); + + state.cells[index] = 0; + state.row_mask[row] &= static_cast(~bit); + state.col_mask[col] &= static_cast(~bit); + state.box_mask[box] &= static_cast(~bit); +} + +uint16_t +candidate_mask(const SudokuState& state, int index) +{ + const int row = index / 9; + const int col = index % 9; + const int box = box_index(row, col); + const uint16_t used = static_cast(state.row_mask[row] | state.col_mask[col] | state.box_mask[box]); + return static_cast(kAllDigitsMask & static_cast(~used)); +} + +int +popcount16(uint16_t x) +{ + int count = 0; + while (x != 0) { + x = static_cast(x & static_cast(x - 1)); + ++count; + } + return count; +} + +std::optional +parse_board(std::string_view input) +{ + if (input.size() != 81) { + return std::nullopt; + } + + SudokuState state{}; + for (size_t i = 0; i < input.size(); ++i) { + const char c = input[i]; + if (c == '.' || c == '0') { + state.cells[i] = 0; + continue; + } + if (c < '1' || c > '9') { + return std::nullopt; + } + + const int digit = c - '0'; + if (!place_digit(state, static_cast(i), digit)) { + return std::nullopt; + } + } + + return state; +} + +std::string +board_to_string(const SudokuState& state) +{ + std::string out; + out.reserve(81); + for (int value : state.cells) { + out.push_back(static_cast('0' + value)); + } + return out; +} + +bool +choose_next_cell(const SudokuState& state, int& out_index) +{ + int best_index = -1; + int best_count = 10; + + for (int i = 0; i < 81; ++i) { + if (state.cells[i] != 0) { + continue; + } + + const uint16_t mask = candidate_mask(state, i); + const int count = popcount16(mask); + + if (count == 0) { + out_index = -1; + return true; + } + if (count < best_count) { + best_count = count; + best_index = i; + if (best_count == 1) { + break; + } + } + } + + out_index = best_index; + return false; +} + +void +solve_dfs(SudokuState& state, std::vector& solutions, size_t max_solutions) +{ + if (max_solutions != 0 && solutions.size() >= max_solutions) { + return; + } + + int index = -1; + const bool dead_end = choose_next_cell(state, index); + + if (dead_end) { + return; + } + + if (index == -1) { + solutions.push_back(board_to_string(state)); + return; + } + + uint16_t mask = candidate_mask(state, index); + while (mask != 0) { + const uint16_t bit = static_cast(mask & static_cast(-static_cast(mask))); + + int digit = 1; + while ((bit & static_cast(1u << digit)) == 0) { + ++digit; + } + + if (place_digit(state, index, digit)) { + solve_dfs(state, solutions, max_solutions); + remove_digit(state, index, digit); + + if (max_solutions != 0 && solutions.size() >= max_solutions) { + return; + } + } + + mask = static_cast(mask & static_cast(mask - 1)); + } +} + +std::optional +parse_positive_limit(const std::string& s) +{ + if (s.empty()) { + return std::nullopt; + } + + size_t value = 0; + for (char c : s) { + if (c < '0' || c > '9') { + return std::nullopt; + } + value = value * 10 + static_cast(c - '0'); + } + + if (value == 0) { + return std::nullopt; + } + + return value; +} + +void +print_usage(const char* program) +{ + std::cerr << "Usage: " << program << " <81-char-board> [max_solutions]\\n" + << "Board chars: 1-9 for fixed cells, '.' or '0' for empty cells.\\n"; +} + +} // namespace + +int +main(int argc, char* argv[]) +{ + if (argc < 2 || argc > 3) { + print_usage(argv[0]); + return 0; + } + + size_t max_solutions = 0; // 0 means unlimited + if (argc == 3) { + const auto parsed_limit = parse_positive_limit(argv[2]); + if (!parsed_limit.has_value()) { + std::cerr << "Error: max_solutions must be a positive integer.\\n"; + return 1; + } + max_solutions = *parsed_limit; + } + + auto parsed_state = parse_board(argv[1]); + if (!parsed_state.has_value()) { + std::cerr << "Error: invalid board input. Expected 81 chars and no row/column/box conflicts.\\n"; + return 1; + } + + SudokuState state = *parsed_state; + std::vector solutions; + solve_dfs(state, solutions, max_solutions); + + if (solutions.empty()) { + std::cout << "No solutions found.\\n"; + return 0; + } + + std::cout << "Found " << solutions.size() << " solution(s).\\n"; + for (const auto& solution : solutions) { + std::cout << solution << "\\n"; + } + + return 0; +} diff --git a/sudoku-solver/tests.sh b/sudoku-solver/tests.sh new file mode 100755 index 0000000..fefa6e9 --- /dev/null +++ b/sudoku-solver/tests.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +set -ex + +puzzle="53..7....6..195....98....6.8...6...34..8.3..17...2...6.6....28....419..5....8..79" +expected="534678912672195348198342567859761423426853791713924856961537284287419635345286179" + +output=$(./sudoku-solver "$puzzle") +if ! echo "$output" | grep -Fq "$expected"; then + echo "Test failed: expected solution was not found" + exit 1 +fi + +invalid="11..............................................................................." +if ./sudoku-solver "$invalid" >/dev/null 2>&1; then + echo "Test failed: contradictory board should be rejected as invalid input" + exit 1 +fi + +limited_output=$(./sudoku-solver "$puzzle" 1) +if ! echo "$limited_output" | grep -Fq "Found 1 solution(s)."; then + echo "Test failed: max_solutions limit did not work" + exit 1 +fi + +echo "All tests passed"