Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion parallel-transform/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ include ../common.mk

# per-example flags
# CXXFLAGS += -pthread
LDLIBS += -ltbb

## get it from the folder name
TARGET := $(notdir $(CURDIR))
Expand All @@ -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 $@
Expand Down
31 changes: 31 additions & 0 deletions sudoku-solver/Makefile
Original file line number Diff line number Diff line change
@@ -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
272 changes: 272 additions & 0 deletions sudoku-solver/main.cpp
Original file line number Diff line number Diff line change
@@ -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 <array>
#include <cstdint>
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

namespace {

struct SudokuState
{
std::array<int, 81> cells{};
std::array<uint16_t, 9> row_mask{};
std::array<uint16_t, 9> col_mask{};
std::array<uint16_t, 9> 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<uint16_t>(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<uint16_t>(1u << digit);

state.cells[index] = 0;
state.row_mask[row] &= static_cast<uint16_t>(~bit);
state.col_mask[col] &= static_cast<uint16_t>(~bit);
state.box_mask[box] &= static_cast<uint16_t>(~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<uint16_t>(state.row_mask[row] | state.col_mask[col] | state.box_mask[box]);
return static_cast<uint16_t>(kAllDigitsMask & static_cast<uint16_t>(~used));
}

int
popcount16(uint16_t x)
{
int count = 0;
while (x != 0) {
x = static_cast<uint16_t>(x & static_cast<uint16_t>(x - 1));
++count;
}
return count;
}

std::optional<SudokuState>
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<int>(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<char>('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<std::string>& 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<uint16_t>(mask & static_cast<uint16_t>(-static_cast<int16_t>(mask)));

int digit = 1;
while ((bit & static_cast<uint16_t>(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<uint16_t>(mask & static_cast<uint16_t>(mask - 1));
}
}

std::optional<size_t>
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<size_t>(c - '0');
}
Comment on lines +209 to +215
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse_positive_limit accumulates into a size_t without overflow detection, so very large max_solutions inputs can wrap and be treated as a much smaller (but still positive) limit. Consider rejecting values that would overflow size_t (e.g., pre-check value > (max - digit)/10) and returning nullopt on overflow so the CLI reports invalid input.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback


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<std::string> 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;
}
26 changes: 26 additions & 0 deletions sudoku-solver/tests.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading