{"id":13825367,"url":"https://github.com/simonepri/sudoku-solver","last_synced_at":"2025-07-10T20:09:44.751Z","repository":{"id":66099013,"uuid":"160933193","full_name":"simonepri/sudoku-solver","owner":"simonepri","description":"🔢 Sudoku Solutions Enumerator (Sequential and Parallel)","archived":false,"fork":false,"pushed_at":"2019-01-26T09:05:03.000Z","size":10039,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-29T17:24:47.502Z","etag":null,"topics":["enumeration","enumerator","java","sudoku","sudoku-solver"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/simonepri.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"license","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-12-08T11:38:28.000Z","updated_at":"2020-03-20T19:33:07.000Z","dependencies_parsed_at":"2024-01-18T03:49:11.752Z","dependency_job_id":null,"html_url":"https://github.com/simonepri/sudoku-solver","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/simonepri/sudoku-solver","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simonepri%2Fsudoku-solver","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simonepri%2Fsudoku-solver/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simonepri%2Fsudoku-solver/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simonepri%2Fsudoku-solver/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/simonepri","download_url":"https://codeload.github.com/simonepri/sudoku-solver/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simonepri%2Fsudoku-solver/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264647947,"owners_count":23643654,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["enumeration","enumerator","java","sudoku","sudoku-solver"],"created_at":"2024-08-04T09:01:19.642Z","updated_at":"2025-07-10T20:09:44.697Z","avatar_url":"https://github.com/simonepri.png","language":"Java","funding_links":[],"categories":["Java"],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003e\n  \u003cb\u003esudoku-solver\u003c/b\u003e\n\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  🔢 Sudoku Solutions Enumerator (Sequential and Parallel)\n  \u003cbr/\u003e\n\n  \u003csub\u003e\n    Coded by \u003ca href=\"#authors\"\u003eSimone Primarosa\u003c/a\u003e and \u003ca href=\"#authors\"\u003eQ. Matteo Chen\u003c/a\u003e.\n  \u003c/sub\u003e\n\u003c/p\u003e\n\n## Introduction to Sudoku\n\u003ca href=\"#introduction-to-sudoku\"\u003e\n  \u003cimg src=\"data/media/sudoku.gif\" width=\"250\" align=\"right\" alt=\"Example Sudoku Board\"/\u003e\n\u003c/a\u003e\n\nSudoku is a popular puzzle game usually played on a 9x9 board of numbers between\n1 and 9.\n\nThe goal of the game is to fill the board. However, each row can\nonly contain one of each of the numbers between 1 and 9. Similarly, each column\nand 3x3 box can only contain one of each of the numbers between 1 and 9.\nThis makes for an engaging and challenging puzzle game.\n\nA well-formed Sudoku puzzle is one that has a unique solution. A Sudoku puzzle,\nmore in general, can have more than one solution and our goal is to enumerate\nthem all, but this task is not always feasible. Indeed, if we were given an\nempty Sudoku table, we would have to enumerate\n[6670903752021072936960 solutions][ref:sudoku-board-num] and it would take us\nthousands of years.\n\n### Definitions\nIn the following sections, we will use some symbols or words to refer to\nspecific aspects of the Sudoku problem.\n\nThe table below summarizes the most important.\n\nTerm              | Description\n------------------|-------------\nS                 | A perfect square indicating the number of columns, rows, and boxes of a Sudoku board.\nN                 | The total number of cells of a board given as N = S * S.\nB                 | The size of a single box of the matrix as B = √S\nBoard             | An instance of Sudoku represented with a S x S matrix.\nRow               | A row of a board's matrix that can only contain one of each of the numbers in [1, S].\nColumn            | A column of a board's matrix that can only contain one of each of the numbers in [1, S].\nBox               | A B x B sub-matrix of a board's matrix that can only contain one of each of the numbers in [1, S].\nCell              | A single entry of a board's matrix either empty or with a legal assignment.\nEmpty Cell        | A cell whose assignment has still to be found.\nCell's Candidates | A list of values in [1, S] which can be legally placed in a particular cell.\nSearch Space      | The product of the candidates of all the empty cells.\nSolution          | An assignment of values for all the empty cells of a board that satisfies the constraints.\n\n## Solving Algorithm\nA typical algorithm to solve Sudoku boards is called\n[backtracking][ref:backtracking]. This algorithm is essentially a\n[depth-first search][ref:dfs] in the tree of all possible guesses in the empty\ncells of the Sudoku board.\n\n### Sequential Backtracking\nThe sequential algorithm is implemented iteratively, and it simply explores the\ntree of all the legal candidates' assignment of each empty cells.\n\nThe pseudo-code that follows highlights its core parts.\n\n```python\ndef seq_sol_counter(board):\n  stack = []\n\n  if board.is_full(): return 1\n  (row, col) = board.get_empty_cell()\n  stack.push((row, col, EMPTY_CELL_VALUE))\n  for val in board.get_candidates(row, col): stack.push((row, col, val))\n\n  count = 0\n  while len(stack) \u003e 0:\n    (row, col, val) = stack.pop()\n    board.set_cell(row, col, val)\n    if val == EMPTY_CELL_VALUE: continue\n\n    if board.is_full(): count += 1; continue\n    (row, col) = board.get_empty_cell()\n    stack.push((row, col, 0))\n    for val in board.get_candidates(row, col): stack.push((row, col, val))\n\n  return count\n```\n\u003e The actual implementation can be found in\n[`src/main/java/sudoku/SequentialSolver.java`][source:sequential].\n\nIt's important to notice that the strategy adopted by the `get_empty_cell`\nfunction to pick the empty cell [can affect the search space][ref:look-ahead]\nand thus the time needed to enumerate all the solutions.\n\nAnother notable thing to consider is that the time complexity and space\ncomplexity of all the operations on the board inside the while loop (`is_full`,\n`set_cell`, `get_empty_cell`, `get_candidates`) can significantly impact the\noverall performance of the backtracking and thus has to be kept as efficient as\npossible.\n\nMore details about the computational complexity of the operations and the idea\nbehind their implementation can be found in the\n\"[implementation details](#implementation-details)\" section.\n\n### Parallel Backtracking\nThe parallel algorithm is implemented by parallelizing the recursive guesses of\neach empty cell using the [fork/join][ref:fork-join] model.\n\nThe pseudo-code that follows highlights its core parts.\n\n```python\ndef par_sol_counter(board, move):\n  if move is not null:\n    (row, col, val) = move\n    board = board.clone()\n    board.set_cell(row, col, val)\n\n  if board.is_full(): return 1\n\n  space = board.get_search_space_size()\n  if space == 0: return 0\n  if space \u003c= SEQUENTIAL_CUTOFF:\n    return seq_sol_counter(board)\n\n  count = 0\n  tasks = []\n  (row, col) = board.get_empty_cell()\n  for val in board.get_candidates(row, col):\n    task = new recursive_task(par_sol_counter, board, (row, col, val))\n    tasks.push(task)\n  for i in range(1, len(tasks)):\n    tasks[i].fork()\n  count += tasks[0].compute()\n  for i in range(1, len(tasks)):\n    count += tasks[i].join()\n\n  return count\n```\n\u003e The actual implementation can be found in\n[`src/main/java/sudoku/ParallelSolver.java`][source:parallel].\n\nThe considerations that have been given about the\n[sequential backtracking](#sequential-backtracking) also hold for the parallel\nversion. In addition to those, it's worth mentioning that two new methods\n(`get_search_space_size` and `clone`) and a new class (`recursive_task`) have\nbeen introduced.\n\nMore details about the computational complexity of the operations and the idea\nbehind their implementation can be found in the\n\"[implementation details](#implementation-details)\" section.\n\n## Implementation details\nIn this section, we discuss the purpose of the methods mentioned in the previous\nsections providing when appropriate some insights on how we made them efficient.\n\n### Check if the board is completed\nThe operation `is_full` consists in knowing whether the board contains at least\nan empty cell.\n\nClearly, a simple approach is to loop through the whole board and check whether\nor not there is an empty cell, but this would cost us `O(N)` each time. Instead\nof doing so, we keep the count the number of filled cells of the board (also\ncalled clues) and then compare it against the total number of cells of the\nboard.\n\nAt the cost of a constant additional work inside the `set_cell` to keep the\ncount updated allows us to lower the time complexity of the operation to `O(1)`.\n\n### Check if a value is legal for a cell\nThe `get_candidates` operation has the job of returning the, possibly empty,\nlist of valid values which can be legally placed in a particular empty cell.\n\nTo accomplish this, one could simply iterate on the row, column, and box of the\ncell given searching for unused values. Doing this would cost us\n`O(3*S) + O(S) = O(S)`, and it's almost the best we can aim for this particular\noperation. \"Almost\", because we can remove the `O(3*S)` addend by keeping track\nof the used values on each row, column, and box of the board.\n\nTo do so, we keep a bit-set of `S` bits for each row, column, and box and each\ntime a specific cell's value `v` is set we also set the bit at position `v - 1`\nof the 3 bit-sets for the particular row, column, and box of the given cell. To\ncheck whether a value `v` is valid or not we just check if the bit at position\n`v - 1` is not set in any of the 3 bit-sets for the particular row, column, and\nbox of the given cell. Since each check is constant and we have `S` values to\ncheck, the overall time complexity is `O(S)`.\n\nThis optimization costs us a constant additional work inside the `set_cell`\nmethod to keep the bit-sets updated and an additional per-instance memory usage\nof `O(3*S) = O(S)`.\n\n### Count the number of candidates of a cell\nThe `get_search_space_size` computes the search space as defined in the\n\"[definitions](#definitions)\" section.\n\nIntuitively, we can do something like the `get_candidates` to count the number\nof candidates instead of creating a list, and this would cost us\n`O(N) * O(S) = O(N * S)` but there's a tricky and memory hungry approach allows\nus to reduce the cost of the operation to just `O(N) * O(1) = O(N)`.\n\nLet's say that for a particular cell we want to count the candidates, then the\nstate of the 3 bit-sets would be the following one.\n\n| value    | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |\n|----------|---|---|---|---|---|---|---|---|---|\n| row      | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |\n| column   | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |\n| box      | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |\n\nIf we compute the bitwise or operation of the 3 bit-sets we obtain a new bit-set\nthat has a 1 on every invalid candidate as showed below.\n\n| value    | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |\n|----------|---|---|---|---|---|---|---|---|---|\n| invalid  | 1 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 0 |\n\nThus, counting the number of candidates has been reduced to the problem of\ncounting the zeros of a bit-set.\n\nIf we use fixed sized integers (e.g., `32-bit int`) to represent our bit-sets,\nthen we could use the integer given by the binary representation of the bit-set\nto accesses a pre-computed table that provides us with the answer of how many\nzeros or ones that particular number has in constant time. The pre-computed\ntable has to be built only once and can be shared by all the boards instantiated\nand implies an additional memory usage of `2^O(S)`.\n\n### Find an empty cell\nOne possible strategy we can use for the `get_empty_cell` is simply to pick the\nfirst empty cell we find on the board.\n\nWe could do this in `O(N)` by just iterating on the board and returning the\nempty cell if present. Instead, we managed to do it in `O(1)` by keeping track\nof the following two information using `O(S)` additional per-instance memory:\n- The row index of the first row having an empty cell.\n- The column index of the first empty cell on of each row.\n\nTo obtain the info, on each `set_cell` operation we update the column index\nfor the particular row in `O(S)` and then, if the row has no more empty cells,\nwe update the row index accordingly.\n\n### Find the empty cell with the lowest number of candidates\nAs we mentioned in the sections above, the strategy used to pick the empty cell\ncan impact significantly on the size of the search space. Indeed, the more is\nthe number of legal candidates for a cell the lower is the probability that our\nguess for that cell will result in a sudoku solution.\n\nThus, is intuitively better to always try to guess values for cells that have\nthe lowest number of candidates.\n\nTo do this without affecting the current complexity of the `get_empty_cell`,\nsimilarly as we did for the strategy above, we keep track of the following two\ninformation using `O(S)` additional per-instance memory:\n- The row index of the row that has the cell with the lowest number of candidates.\n- The column index of the cell with the lowest number of candidates of each row.\n\nTo obtain the info, on each set operation we do three things:\n- For the current row, we search the column with the lowest number of candidates\nin `O(S) * O(1) = O(S)`.\n- For each row of the current box, we compare the number of candidates at the\nsaved column index with the new number of candidates of all the columns of that\nbox, and we update the saved column index for that row if needed in\n`O(B) * O(B) * O(1) = O(S) * O(1) = O(S)`.\n- For each row, we compare the number of candidates at the saved column index\nwith the new number of candidates of the current column, and we update the saved\ncolumn index for that row and the saved row index if needed in `O(S) * O(1) =\nO(S)`.\n\n### Optimized addition and multiplication with BigInteger\nIn Java, BigInteger objects are immutable and thus every time an operation is\nexecuted on them a new object is instantiated.\n\nWe implemented two modified versions of the BigInteger class, namely `BigIntSum`\nand `BigIntProd`, that are mutable BitInteger and allow us to do sums and\nproducts in constant amortized time.\n\n\u003e In general, the libs we wrote doesn't guarantee constant amortized time\noperations, but they do in our particular use case in which we mostly sum and\nmultiply together small numbers.\n\n### Parallelize branches using the fork/join framework\nThe Java's [fork/join][ref:fork-join] framework it's easy to reason with, and\nits [work-stealing][ref:work-stealing] scheduler provides convenient theoretical\nguarantees.\n\nA so-called `RecursiveTask` class (`recursive_task` in the pseudo-code above)\ncan model fairly well a backtracking solver. Each backtracking choice can be\ntested concurrently forking on each possible candidate.\n\nTo minimize the overhead of task creation we employ two common strategies in the\nfork/join realm:\n- Reusing the same task rather than creating a new fork to evaluate the first choice.\n- Choosing a sequential cut-off in a way that the workload is distributed evenly among the tasks.\n\n### Parallelize board copy\nSince the `board` object has to be modified to fill a cell, each parallel task\nhas to have its local instance of the board.\n\nDuplicating a board is an expensive operation, so instead of doing it eagerly in\nthe callee task, we supply the `RecursiveTask` constructor with the change that\nwe want to try, and we make the copy of the board in its `compute` method that\nwill be executed in a separate thread.\n\nIn this way we offload an expensive computation in the forked task, decreasing\nthe span.\n\n### Choose of the appropriate sequential cut-off\nDue to the overhead involved with the creation of parallel tasks, it's faster to\nswitch to the sequential algorithm when the size of the problem becomes small\nenough. We will call that threshold problem size `sequential cut-off` or just\n`cut-off`.\n\nThe way we can chose this value mostly depends on what we define as the\n`size of the problem`.\n\nWe experimented with the two following approaches:\n- The size of the problem is the number of empty cells to fill.\n- The size of the problem is the search space.\n\nWe measured limited differences when choosing one of these parameters, provided\nthat we optimize the value of the cutoff accordingly, but we used the second\ndefinition.\n\nThe correlation graph in the \"[benchmark results](#benchmark-results)\" section\nreadily explains this result, that is, the search space and empty cell have an\nalmost perfect linear correlation. Thus one can be used to estimate the other.\n\nConcretely, the optimal sequential cutoff can be found looking at processor\nutilization patterns: it should be full during most of the computation, and all\nthe CPU should complete their task at the same time. The minimum sequential\ncutoff also has to consider the task creation overhead.\n\n## Performance comparison\nWe have built a benchmarking tool with which we have evaluated different KPI of\nthe two implementation we implemented.\n\n\u003e The data collected during the benchmarks can be found in\n[`data/benchmark`][source:data-benchmark].\nNote that all the times are in μs.\n\n### Benchmarking environment\nTo benchmark our implementation we used a handful of test cases differing mainly\nfor the number of legal solutions.\n\nThe table that follows summarizes the main characteristics of the tests.\n\n| test name | total cells | empty cells | filling factor | solutions         | search space                         |\n|-----------|-------------|-------------|----------------|-------------------:|--------------------------------------|\n| 1a        | 81          | 53          | 34.57%         | 1         | 10^25      |\n| 1b        | 81          | 59          | 27.16%         | 4,715      | 10^36          |\n| 1c        | 81          | 61          | 24.69%         | 132,271    | 10^40           |\n| 1d        | 81          | 62          | 23.46%         |  587,264    | 10^41              |\n| 1e        | 81          | 63          | 22.22%         | 3,151,964   | 10^43        |\n| 1f        | 81          | 64          | 20.99%         | 16,269,895  | 10^45     |\n| 2a        | 81          | 58          | 28.40%         | 1         | 10^31     |\n| 2b        | 81          | 60          | 25.93%         | 276       | 10^35   |\n| 2b        | 81          | 62          | 23.46%         | 32,128     | 10^39          |\n| 2d        | 81          | 64          | 20.99%         | 1,014,785   | 10^43  |\n| 2e        | 81          | 65          | 19.75%         | 738,836    | 10^45 |\n| 2f        | 81          | 66          | 18.52%         | 48,794,239  | 10^47 |\n\n\u003e The test files can be found in\n[`src/benchmark/boards`][source:bench-boards].\n\nTo get a scalable and homogeneous environment, we leveraged the\n[Google Cloud Infrastructure][ref:goole-cloud].\n\nThe table that follows shows the specs of the machine we used.\n\n| os    | core                                    | cpu                            |\n|-------|-----------------------------------------|--------------------------------|\n| Linux | 2, 4, 8, 12, 16, 24, 32, 40, 48, 56, 64 | Intel(R) Xeon(R) CPU @ 2.30GHz |\n\n### Benchmark results\nThe benchmark reveled a speed-up that grows almost linearly in relation to the\nnumber of cores.\n\nThe table that follows shows the speed-up values grouped by test case and core\ncount.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"data/benchmark/speedup/1*.svg\" width=\"49%\" align=\"center\" alt=\"Speedup for test series 1\"/\u003e\n  \u003cimg src=\"data/benchmark/speedup/2*.svg\" width=\"49%\" align=\"center\" alt=\"Speedup for test series 2\"/\u003e\n\u003c/p\u003e\n\nUnfortunately due to the overheads introduced in the parallel algorithm, the\nspeed-up obtained is sometimes slightly smaller than 1, but this happens only on\nsmaller test cases or with a very low number of cores.\n\nTo understand the root cause of the values obtained, we computed a correlation\nmatrix over all the data we gathered.\n\nWhile the sequential time depends linearly on the number of solution and it's\nuncorrelated with the number of cores, the factors that make up the parallel time and\nspeed-up are more varied.\n\nThe correlation matrix is muddled by the core count, thus we plotted several\ncorrelation matrices grouping the data by core count. On the right there is an\nexample of the correlation matrix for the test data obtained on the 64 core\nmachine.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"data/benchmark/correlation/all.svg\" width=\"49%\" align=\"center\" alt=\"Correlation matrix for all data\"/\u003e\n  \u003cimg src=\"data/benchmark/correlation/c64.svg\" width=\"49%\" align=\"center\" alt=\"Correlation matrix for 64 cores\"/\u003e\n\u003c/p\u003e\n\nWe can clearly see that the speed-up is correlated greatly with the search space\nand the empty cells, rather than only with the number of solutions. This is\nexpected because search space and empty cells are almost linearly dependent and\nsearch space determines how much branching we can have on our parallel solver.\nThe same trend can also be found looking at the speed-up plot.\n\nThe correlation matrices also show us that even though both the parallel and\nsequential times are linearly dependent on the number of solutions, the speed-up\nis not very strongly correlated with it as well.\n\nWe can conclude that the speed-up comes from being able to explore the whole\nsearch space faster and that it could be improved a little by tweaking the\nfork/join parameters.\n\n## Usage\n\u003cimg src=\"data/media/run-cli.png\" width=\"350\" align=\"right\" alt=\"Sudoku solution enumerator CLI\"/\u003e\n\nThe project is provided with a [CLI][bin:run-cli] that allows you\nto run the solver on your machine with ease.\n\nIf you want to run it locally, you need to run the following commands.\n```bash\ngit clone https://github.com/simonepri/sudoku-solver.git\ncd sudoku-solver\n\n./sudoku\n```\n\n\u003e NB: This will also trigger the build process so be sure to have the\n[Java JDK][download:jjdk] installed on your machine prior to launch it.\n\n## Benchmarking suite\n\u003cimg src=\"data/media/bench-cli.png\" width=\"350\" align=\"right\" alt=\"Sudoku solution enumerator benchmarking CLI\"/\u003e\n\nThe project is provided with a [CLI][bin:bench-cli] that allows you\nto reproduce the tests results on your machine.\n\nIf you want to run it locally, you need to run the following commands.\n```bash\ngit clone https://github.com/simonepri/sudoku-solver.git\ncd sudoku-solver\n\n./bench\n```\n\n\u003e NB: This will also trigger the build process so be sure to have the\n[`Java JDK`][download:jjdk] installed on your machine prior to launch it.\n\n\u003e TIP: You can stop a test by hitting `CTRL+C` or `Command+C`.\n\n## Development\nClone the repository to your local machine then cd into the directory created by\nthe cloning operation.\n\n```bash\ngit clone https://github.com/simonepri/sudoku-solver.git\ncd sudoku-solver\n```\n\nThe source code for the sudoku solver can be found in\n[`src/main/java/sudoku`][source:main], while the source code for the unit tests\nand the benchmarking suite can be found in [`src/test/java/sudoku`][source:test]\nand [`src/benchmark`][source:benchmark] respectively.\n\nBuild the project, run the unit tests and run the CLI.\n```bash\n# On Linux and Darwin\n./gradlew build\n./gradlew test\n./gradlew run\n\n# On Windows\n./gradlew.bat build\n./gradlew.bat test\n./gradlew.bat run\n```\n\n\u003e NB: You will need the [`Java JDK`][download:jjdk] installed on your machine to\nbuild the project.\n\n## Authors\n- **Simone Primarosa** - *Github* ([@simonepri][github:simonepri]) • *Twitter* ([@simoneprimarosa][twitter:simoneprimarosa])\n- **Q. Matteo Chen** - *Github* ([@chq-matteo][github:chq-matteo]) • *Twitter* ([@chqmatteo][twitter:chqmatteo])\n\n## License\nThis project is licensed under the MIT License - see the [license][license] file for details.\n\n\u003c!-- Links --\u003e\n[license]: https://github.com/simonepri/sudoku-solver/tree/master/license\n[source]: https://github.com/simonepri/sudoku-solver/tree/master/src/main/java/sudoku\n[bin:bench-cli]: https://github.com/simonepri/sudoku-solver/tree/master/bench\n[bin:run-cli]: https://github.com/simonepri/sudoku-solver/tree/master/sudoku\n\n[source:main]: https://github.com/simonepri/sudoku-solver/tree/master/src/main/java/sudoku\n[source:test]: https://github.com/simonepri/sudoku-solver/tree/master/src/test/java/sudoku\n[source:benchmark]: https://github.com/simonepri/sudoku-solver/tree/master/src/benchmark\n[source:sequential]: https://github.com/simonepri/sudoku-solver/tree/master/src/main/java/sudoku/SequentialSolver.java\n[source:parallel]: https://github.com/simonepri/sudoku-solver/tree/master/src/main/java/sudoku/ParallelSolver.java\n[source:bench-boards]: https://github.com/simonepri/sudoku-solver/tree/master/src/benchmark/boards\n[source:data-benchmark]: https://github.com/simonepri/sudoku-solver/tree/master/data/benchmark\n\n[github:simonepri]: https://github.com/simonepri\n[twitter:simoneprimarosa]: http://twitter.com/intent/user?screen_name=simoneprimarosa\n[github:chq-matteo]: https://github.com/chq-matteo\n[twitter:chqmatteo]: http://twitter.com/intent/user?screen_name=chqmatteo\n\n[download:git]: https://git-scm.com/downloads\n[download:jjdk]: https://www.oracle.com/technetwork/pt/java/javase/downloads/index.html\n\n[ref:sudoku-board-num]: http://www.afjarvis.staff.shef.ac.uk/sudoku\n[ref:backtracking]: https://en.wikipedia.org/wiki/backtracking\n[ref:dfs]: https://en.wikipedia.org/wiki/depth-first_search\n[ref:look-ahead]: https://en.wikipedia.org/wiki/look-ahead_(backtracking)\n[ref:fork-join]: https://en.wikipedia.org/wiki/fork-join_model\n[ref:work-stealing]: https://en.wikipedia.org/wiki/work_stealing\n[ref:goole-cloud]: https://cloud.google.com/compute/docs/machine-types#highcpu\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimonepri%2Fsudoku-solver","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsimonepri%2Fsudoku-solver","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimonepri%2Fsudoku-solver/lists"}