From c8506d6d136c23db1f7bbe7279bfdca12c352695 Mon Sep 17 00:00:00 2001 From: Zhihua Lai Date: Mon, 23 Mar 2026 13:41:31 +0000 Subject: [PATCH 01/12] Add sudoku - using agent from vscode (GPT-5.3-codex) --- README.md | 1 + sudoku-solver/Makefile | 31 +++++ sudoku-solver/main.cpp | 271 +++++++++++++++++++++++++++++++++++++++++ sudoku-solver/tests.sh | 26 ++++ 4 files changed, 329 insertions(+) create mode 100644 sudoku-solver/Makefile create mode 100644 sudoku-solver/main.cpp create mode 100755 sudoku-solver/tests.sh 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/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..39faf2c --- /dev/null +++ b/sudoku-solver/main.cpp @@ -0,0 +1,271 @@ +/** +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 1; + } + + 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..6d7537c --- /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 -q "$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 -q "Found 1 solution(s)."; then + echo "Test failed: max_solutions limit did not work" + exit 1 +fi + +echo "All tests passed" From 56a35ede4c2fbab9783a091e29344c3c58309e03 Mon Sep 17 00:00:00 2001 From: Zhihua Lai Date: Mon, 23 Mar 2026 13:41:47 +0000 Subject: [PATCH 02/12] Format fix --- sudoku-solver/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sudoku-solver/main.cpp b/sudoku-solver/main.cpp index 39faf2c..1254dea 100644 --- a/sudoku-solver/main.cpp +++ b/sudoku-solver/main.cpp @@ -19,7 +19,8 @@ DFS Sudoku solver CLI. namespace { -struct SudokuState { +struct SudokuState +{ std::array cells{}; std::array row_mask{}; std::array col_mask{}; From 0f646423692bd9b8d2c8033e21dd4ce49c069ab9 Mon Sep 17 00:00:00 2001 From: Zhihua Lai Date: Mon, 23 Mar 2026 13:50:45 +0000 Subject: [PATCH 03/12] Fix --- parallel-transform/Makefile | 3 ++- sudoku-solver/main.cpp | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) 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/main.cpp b/sudoku-solver/main.cpp index 1254dea..c0c9877 100644 --- a/sudoku-solver/main.cpp +++ b/sudoku-solver/main.cpp @@ -235,7 +235,7 @@ main(int argc, char* argv[]) { if (argc < 2 || argc > 3) { print_usage(argv[0]); - return 1; + return 0; } size_t max_solutions = 0; // 0 means unlimited From d6dbad1808ac84b73d020191a413071828d91e0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:51:11 +0000 Subject: [PATCH 04/12] Initial plan From 141d647e844e25015a7ab15a0e7601673937acd7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:51:16 +0000 Subject: [PATCH 05/12] Initial plan From 906e174cf7072a4e33f184a2b98b39c4e226055b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:51:20 +0000 Subject: [PATCH 06/12] Initial plan From 2b6a9344943936fe44069c957735c3712388c7a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:51:22 +0000 Subject: [PATCH 07/12] Initial plan From 79d0f1095b96e21175aceb0374a9067224df4797 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:51:59 +0000 Subject: [PATCH 08/12] fix: use grep -Fq for exact string match in tests.sh Co-authored-by: doctorlai-msrc <92519564+doctorlai-msrc@users.noreply.github.com> Agent-Logs-Url: https://github.com/DoctorLai/cpp-coding-exercise/sessions/0e440f61-3a33-46cf-b097-29fac49845e0 --- sudoku-solver/tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sudoku-solver/tests.sh b/sudoku-solver/tests.sh index 6d7537c..dc2ced4 100755 --- a/sudoku-solver/tests.sh +++ b/sudoku-solver/tests.sh @@ -18,7 +18,7 @@ if ./sudoku-solver "$invalid" >/dev/null 2>&1; then fi limited_output=$(./sudoku-solver "$puzzle" 1) -if ! echo "$limited_output" | grep -q "Found 1 solution(s)."; then +if ! echo "$limited_output" | grep -Fq "Found 1 solution(s)."; then echo "Test failed: max_solutions limit did not work" exit 1 fi From 01d9df125791231c4444cf82ed37ca783491d62f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:52:23 +0000 Subject: [PATCH 09/12] Use grep -Fq for fixed-string matching in tests.sh Co-authored-by: doctorlai-msrc <92519564+doctorlai-msrc@users.noreply.github.com> Agent-Logs-Url: https://github.com/DoctorLai/cpp-coding-exercise/sessions/046cfbae-a5b3-412f-ade9-bee8689aa4db --- sudoku-solver/tests.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sudoku-solver/tests.sh b/sudoku-solver/tests.sh index 6d7537c..fefa6e9 100755 --- a/sudoku-solver/tests.sh +++ b/sudoku-solver/tests.sh @@ -6,7 +6,7 @@ puzzle="53..7....6..195....98....6.8...6...34..8.3..17...2...6.6....28....419..5 expected="534678912672195348198342567859761423426853791713924856961537284287419635345286179" output=$(./sudoku-solver "$puzzle") -if ! echo "$output" | grep -q "$expected"; then +if ! echo "$output" | grep -Fq "$expected"; then echo "Test failed: expected solution was not found" exit 1 fi @@ -18,7 +18,7 @@ if ./sudoku-solver "$invalid" >/dev/null 2>&1; then fi limited_output=$(./sudoku-solver "$puzzle" 1) -if ! echo "$limited_output" | grep -q "Found 1 solution(s)."; then +if ! echo "$limited_output" | grep -Fq "Found 1 solution(s)."; then echo "Test failed: max_solutions limit did not work" exit 1 fi From f5cdcfe8e39b24792789f683e5ac16406ffa26ec Mon Sep 17 00:00:00 2001 From: Zhihua Lai Date: Mon, 23 Mar 2026 14:12:17 +0000 Subject: [PATCH 10/12] Add dep --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 04b66fb5098e920331699ca856eddf43ce2c5a89 Mon Sep 17 00:00:00 2001 From: Zhihua Lai Date: Mon, 23 Mar 2026 14:13:55 +0000 Subject: [PATCH 11/12] Remove print --- sudoku-solver/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sudoku-solver/main.cpp b/sudoku-solver/main.cpp index c0c9877..7e5a2f3 100644 --- a/sudoku-solver/main.cpp +++ b/sudoku-solver/main.cpp @@ -259,11 +259,11 @@ main(int argc, char* argv[]) solve_dfs(state, solutions, max_solutions); if (solutions.empty()) { - std::cout << "No solutions found.\\n"; + // std::cout << "No solutions found.\\n"; return 0; } - std::cout << "Found " << solutions.size() << " solution(s).\\n"; + // std::cout << "Found " << solutions.size() << " solution(s).\\n"; for (const auto& solution : solutions) { std::cout << solution << "\\n"; } From bf732453cd1c229494d46c11e8148d260fcb79b4 Mon Sep 17 00:00:00 2001 From: Zhihua Lai Date: Mon, 23 Mar 2026 14:14:35 +0000 Subject: [PATCH 12/12] Revert "Remove print" This reverts commit 04b66fb5098e920331699ca856eddf43ce2c5a89. --- sudoku-solver/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sudoku-solver/main.cpp b/sudoku-solver/main.cpp index 7e5a2f3..c0c9877 100644 --- a/sudoku-solver/main.cpp +++ b/sudoku-solver/main.cpp @@ -259,11 +259,11 @@ main(int argc, char* argv[]) solve_dfs(state, solutions, max_solutions); if (solutions.empty()) { - // std::cout << "No solutions found.\\n"; + std::cout << "No solutions found.\\n"; return 0; } - // std::cout << "Found " << solutions.size() << " solution(s).\\n"; + std::cout << "Found " << solutions.size() << " solution(s).\\n"; for (const auto& solution : solutions) { std::cout << solution << "\\n"; }