Intro
This time, I have programmed a parallel sudoku solver in Java.
Code
io.github.coderodde.sudoku.ParallelSudokuSolver.java:
package io.github.coderodde.sudoku;
import io.github.coderodde.sudoku.misc.IntSet;
import io.github.coderodde.sudoku.misc.RandomSudokuBoardSeedProvider;
import io.github.coderodde.sudoku.misc.SudokuBoardVerifier;
import io.github.coderodde.sudoku.misc.Utils;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicReference;
/**
 * This class implements a parallel sudoku solver.
 * 
 * @version 1.0.0 (Dec 4, 2024)
 * @since 1.0.0 (Dec 4, 2024)
 */
public final class ParallelSudokuSolver {
    
    /**
     * The width/height of the minisquare.
     */
    private int sqrtn;
    
    /**
     * The row-wise filters.
     */
    private IntSet[] rowIntSets;
    
    /**
     * The column-wise filters.
     */
    private IntSet[] colIntSets;
    
    /**
     * The minisquare filters.
     */
    private IntSet[][] minisquareIntSets;
    
    /**
     * Solves the input sudoku, which becomes modified. 
     * 
     * @param sudokuBoard the sudoku board to solve.
     * @return a solved board.
     */
    public SudokuBoard solve(final SudokuBoard sudokuBoard) {
        return solve(sudokuBoard, 
                     Runtime.getRuntime().availableProcessors());
    }
    /**
     * Solves the input sudoku, 
     * 
     * @param sudokuBoard the sudoku board to solve.
     * @param numberOfProcessors the number of processors to use.
     * @return a solved board.
     */
    public SudokuBoard solve(final SudokuBoard sudokuBoard,
                             int numberOfProcessors) {
        
        if (!SudokuBoardVerifier.isValid(sudokuBoard)) {
            // Don't process invalid sudoku boards:
            return null;
        }
        
        // Once here, sudokuBoard is valid.
        if (Utils.isCompleteSudokuBoard(sudokuBoard)) {
            // Once here, the sudokuBoard is both valid and complete. Just 
            // return it!
            return sudokuBoard;
        }
        
        // Preload all the filters basing on present values in the sudokuBoard:
        loadIntSets(sudokuBoard);
        
        // The solver thread list:
        final List<SudokuSolverThread> threads =
                new ArrayList<>(numberOfProcessors);
        
        // The seeds of the search. Each seed will be passes as initial board to
        // each solver thread:
        final List<SudokuBoard> seeds = 
                RandomSudokuBoardSeedProvider
                        .computeSeeds(sudokuBoard,
                                      numberOfProcessors);
        
        // Used for halting all the threads when a solution is found:
        final SharedThreadState sharedThreadState = 
                new SharedThreadState();
        
        // Spawn the threads:
        for (int i = 0; i < Math.min(numberOfProcessors, seeds.size()); ++i) {
            threads.add(
                    new SudokuSolverThread(new SudokuBoard(seeds.get(i)),
                                           sudokuBoard, 
                                           sharedThreadState));
            threads.get(i).start();
        }
        
        // Wait for all the solver threads to exit:
        for (final SudokuSolverThread thread : threads) {
            try {
                thread.join();
            } catch (final InterruptedException ex) {
                ex.printStackTrace();
                System.exit(1);
            }
        }
        
        // Fetch and return the solution:
        return sharedThreadState.getSolution(); 
    }
    
    /**
     * Preloads all the filters from the input board.
     * 
     * @param board the board from which to fetch all the cell values for the
     *              filters.
     */
    private void loadIntSets(final SudokuBoard board) {
        final int n = board.getWidthHeight();
        final int capacity = n + 1;
        sqrtn = (int) Math.sqrt(n);
        
        // BEGIN: Create filters.
        rowIntSets = new IntSet[n];
        colIntSets = new IntSet[n];
        minisquareIntSets = new IntSet[sqrtn][sqrtn];
        
        for (int i = 0; i < n; ++i) {
            rowIntSets[i] = new IntSet(capacity);
            colIntSets[i] = new IntSet(capacity);
        }
        
        for (int y = 0; y < sqrtn; ++y) {
            minisquareIntSets[y] = new IntSet[sqrtn];
            
            for (int x = 0; x < sqrtn; ++x) {
                minisquareIntSets[y][x] = new IntSet(capacity);
            }
        }
        // END: Create filters.
        
        // BEGIN: Preload filter values.
        for (int y = 0; y < board.getWidthHeight(); ++y) {
            for (int x = 0; x < board.getWidthHeight(); ++x) {
                final int cellValue = board.get(x, y);
                
                if (cellValue == Utils.UNUSED_CELL) {
                    continue;
                }
                
                rowIntSets[y].add(cellValue);
                colIntSets[x].add(cellValue);
                minisquareIntSets[y / sqrtn]
                                 [x / sqrtn].add(cellValue);
            }
        }
        // END: Preload filter values.
    }
    
    /**
     * This inner class implements sudoku solver threads.
     */
    private final class SudokuSolverThread extends Thread {
        
        /**
         * The seeding sudoku board.
         */
        private final SudokuBoard seed;
        
        /**
         * The original task sudoku board.
         */
        private final SudokuBoard original;
        
        /**
         * The shared thread state. Used for communicating that a solution is 
         * found and all the threads must exit.
         */
        private final SharedThreadState sharedThreadState;
        
        /**
         * The random number generator.
         */
        private final Random random = new Random();
        
        /**
         * Constructs this thread.
         * 
         * @param seed              the seed sudoku board.
         * @param original          the original sudoku board.
         * @param sharedThreadState the shared thread state.
         */
        SudokuSolverThread(final SudokuBoard seed,
                           final SudokuBoard original,
                           final SharedThreadState sharedThreadState) {
            this.seed = seed;
            this.original = original;
            this.sharedThreadState = sharedThreadState;
        }
        
        @Override
        public void run() {
            solveImpl(0, 0);
        }
        
        /**
         * Returns an {@code int} array of cell values in random order.
         * 
         * @return cell values.
         */
        private int[] getCellValues() {
            final int[] cellValues = new int[seed.getWidthHeight()];
            
            for (int i = 0; i < seed.getWidthHeight(); ++i) {
                cellValues[i] = i + 1;
            }
            
            Utils.shuffle(cellValues, random);
            return cellValues;
        }
        
        /**
         * The actual solution method.
         * 
         * @param x the {@code x}-coordinate of the cell to process next.
         * @param y the {@code y}-coordinate of the cell to process next.
         * 
         * @return {@code true} if a solution was found deeper in the search.
         */
        private boolean solveImpl(int x,
                                  int y) {
            
            if (sharedThreadState.getSolution() != null) {
                // Once here, we have a solution and we simple exit with true:
                return true;
            }
            
            if (x == seed.getWidthHeight()) {
                // Once here, we have reached the right border. Go to the
                // beginning of the next row:
                x = 0;
                ++y;
            }
            
            if (y == seed.getWidthHeight()) {
                // Once here, we have a solution. Record it and exit with true:
                sharedThreadState.setSolution(seed);
                return true;
            }
            
            if (original.get(x, y) != Utils.UNUSED_CELL) {
                // Once here, we need just to copy a cell value from the 
                // original sudoku board to the seed:
                seed.set(x,
                         y,
                         original.get(x, y));
                
                // Process further:
                return solveImpl(x + 1, y);
            }
            
            // Get an array of cell values in random order:
            final int[] cellValues = getCellValues();
            
            for (final int cellValue : cellValues) {
                
                if (rowIntSets[y].contains(cellValue)) {
                    // Once here, the row already contains cellValue:
                    continue;
                }
                
                if (colIntSets[x].contains(cellValue)) {
                    // Once here, the column already contains cellValue:
                    continue;
                }
                
                if (minisquareIntSets[y / sqrtn]
                                     [x / sqrtn].contains(cellValue)) {
                    // Once here, the minisquare already contains cellValue:
                    continue;
                }
                
                // Write the cell value to seed:
                seed.set(x, y, cellValue);
                
                // BEGIN: Mark in filters.
                rowIntSets[y].add(cellValue);
                colIntSets[x].add(cellValue);
                minisquareIntSets[y / sqrtn]
                                 [x / sqrtn].add(cellValue);
                // END: Mark in filters.
                
                if (solveImpl(x + 1, y)) {
                    // Recur further:
                    return true;
                }
                
                // BEGIN: Unmark in filters.
                rowIntSets[y].remove(cellValue);
                colIntSets[x].remove(cellValue);
                minisquareIntSets[y / sqrtn]
                                 [x / sqrtn].remove(cellValue);
                // END: Unmark in filters.
            }
            // Once here, we could not set to (x, y). Backtrack a little:
            return false;
        }
    }
    
    /**
     * This class implements a simple shared thread state.
     */
    private static final class SharedThreadState {
        
        /**
         * Solution so far. Starts with {@code null}.
         */
        private final AtomicReference<SudokuBoard> solution = 
                  new AtomicReference(null);
        
        public SudokuBoard getSolution() {
            return solution.get();
        }
        
        public void setSolution(final SudokuBoard board) {
            solution.set(board);
        }
    }
}
io.github.coderodde.sudoku.SudokuBoard.java:
package io.github.coderodde.sudoku;
import static io.github.coderodde.sudoku.misc.Utils.checkWidthHeight;
/**
 * This class implements a sudoku board.
 * 
 * @version 1.0.0 (Jun 22, 2025)
 * @since 1.0.0 (Jun 22, 2025)
 */
public final class SudokuBoard {
    
    /**
     * The actual data matrix for holding integers.
     */
    private final int[][] data;
    
    public SudokuBoard(final int widthHeight) {
        checkWidthHeight(widthHeight);
        this.data = new int[widthHeight]
                           [widthHeight];
    }
    
    /**
     * Copy-constructs this sudoku board from {@code board}.
     * 
     * @param board the source sudoku board.
     */
    public SudokuBoard(final SudokuBoard board) {
        this.data = new int[board.getWidthHeight()]
                           [board.getWidthHeight()];
        
        for (int y = 0; y < board.getWidthHeight(); ++y) {
            this.data[y] = new int[board.getWidthHeight()];
            
            System.arraycopy(board.data[y], 
                             0,
                             this.data[y], 
                             0,
                             data.length);
        }
    }
    
    /**
     * Return {@code true} if and only if the cell value at {@code (x, y)} 
     * contains a valid cell value (between 1, and {@code data.length}, 
     * inclusive).
     * 
     * @param x the {@code x}-coordinate of the cell.
     * @param y the {@code y}-coordinate of the cell.
     * @return {@code true} if and only if the cell contains a valid value.
     */
    public boolean isValidCellValue(final int x,
                                    final int y) {
        final int cellValue = get(x, 
                                  y);
        
        return 1 <= cellValue && cellValue <= data.length;
    }
    
    public int getWidthHeight() {
        return data.length;
    }
    
    public void set(final int x,
                    final int y,
                    final int value) {
        data[y][x] = value;
    }
    
    public int get(final int x, 
                   final int y) {
        return data[y][x];
    }
    
    /**
     * Returns the ASCII art of this sudoku board.
     * 
     * @return the ASCII art of this sudoku board.
     */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        final String horizontalBar = getHorizontalBar(data.length);
        
        sb.append(horizontalBar);
        
        for (int y = 0; y < data.length; ++y) {
            final String row = getDataRow(data[y]);
            
            sb.append('\n')
              .append(row)
              .append('\n')
              .append(horizontalBar);
        }
        
        return sb.toString();
    }
    
    /**
     * Computes a string representing a row in a sudoku board.
     * 
     * @param dataRow the data row of a sudoku.
     * 
     * @return a string representation of the data row.
     */
    private static String getDataRow(final int[] dataRow) {
        final String maximumCellWidth = Integer.toString(dataRow.length);
        final int cellWidth = maximumCellWidth.length();
        final String fmt = String.format("|%%%dd", cellWidth);
        final StringBuilder sb = 
                new StringBuilder(
                        1 + (1 + cellWidth) * dataRow.length);
        
        for (int x = 0; x < dataRow.length; ++x) {
            sb.append(String.format(fmt, dataRow[x]));
        }
        
        return sb.append('|').toString();
    }
    
    /**
     * Returns the horizontal separating bar.
     * 
     * @param widthHeight the width/height of the target sudoku board.
     * 
     * @return the string representation of the separating bar.
     */
    private static String getHorizontalBar(final int widthHeight) {
        final String nstr = Integer.toString(widthHeight);
        final String innerCellWidth = getInnerCellWidth(nstr.length());
        final StringBuilder sb = new StringBuilder("+");
        
        for (int x = 0; x < widthHeight; ++x) {
            sb.append(innerCellWidth)
              .append('+');
        }
        
        return sb.toString();
    }
    
    /**
     * Computes the inner cell upper horizontal bar.
     * 
     * @param cellWidth the width of a cell.
     * 
     * @return inner cell upper horizontal bar.
     */
    private static String getInnerCellWidth(final int cellWidth) {
        final StringBuilder sb = new StringBuilder(cellWidth);
        
        for (int x = 0; x < cellWidth; ++x) {
            sb.append('-');
        }
        
        return sb.toString();
    }
}
io.github.coderodde.sudoku.demo.Demo.java:
package io.github.coderodde.sudoku.demo;
import io.github.coderodde.sudoku.ParallelSudokuSolver;
import io.github.coderodde.sudoku.SudokuBoard;
import io.github.coderodde.sudoku.misc.RandomSudokuBoardGenerator;
import io.github.coderodde.sudoku.misc.RandomSudokuBoardPruner;
import io.github.coderodde.sudoku.misc.SudokuBoardVerifier;
import io.github.coderodde.sudoku.misc.Utils;
import java.util.Random;
/**
 * This class implements a demonstration program comparing the parallel sudoku
 * search with varying number of threads.
 * 
 * @version 1.1.0 (Jun 23, 2024)
 * @since 1.0.0 (Dec 4, 2024)
 */
public class Demo {
    private static final int WIDTH_HEIGHT = 16;
    private static final int NUMBER_OF_CELLS_TO_PRUNE = 150;
    
    public static void main(String[] args) {
        final Random random = new Random();
        int threads = Runtime.getRuntime().availableProcessors() * 4;
        
        System.out.println("Generating a random full sudoku board...");
        
        final SudokuBoard sourceSudokuBoard = 
                new RandomSudokuBoardGenerator(WIDTH_HEIGHT, random)
                        .generateRandomSudokuBoard();
        
        System.out.println("Board generated. Pruning...");
        
        RandomSudokuBoardPruner.prune(sourceSudokuBoard, 
                                      NUMBER_OF_CELLS_TO_PRUNE,
                                      random);
        
        while (threads > 0) {
            benchmark(threads, new SudokuBoard(sourceSudokuBoard));
            threads /= 2;
        }
    }
    
    private static void benchmark(final int threads,
                                  final SudokuBoard sourceSudokuBoard) {
        
        final long ta = System.currentTimeMillis();
        
        final SudokuBoard solution = 
                new ParallelSudokuSolver()
                        .solve(sourceSudokuBoard,
                               threads);
        
        final long tb = System.currentTimeMillis();
        final long duration = tb - ta;
        
        final boolean isValid = SudokuBoardVerifier.isValid(solution) &&
                                Utils.isCompleteSudokuBoard(solution);
        
        System.out.println(solution);
        System.out.printf("Threads: %2d, duration: %d ms, complete = %b.\n\n", 
                          threads, 
                          duration,
                          isValid);
    }
}
io.github.coderodde.sudoku.misc.IntSet.java:
package io.github.coderodde.sudoku.misc;
/**
 * This class implements a simple set data structures for small integers.
 * 
 * @version 1.0.0 (Dec 4, 2024)
 * @since 1.0.0 (Dec 4, 2024)
 */
public final class IntSet {
    
    /**
     * The actual data array.
     */
    private final boolean[] data;
    
    /**
     * Constructs a new integer set that can accommodate {@code dataCapacity}
     * elements.
     * 
     * @param capacity data array capacity.
     */
    public IntSet(final int capacity) {
        this.data = new boolean[capacity];
    }
    
    /**
     * Adds the integer {@code integer} to this integer set.
     * 
     * @param integer the integer to set.
     */
    public void add(final int integer) {
        data[integer] = true;
    }
    
    /**
     * Queries whether the integer {@code integer} is in this integer set.
     * 
     * @param integer the integer to query for inclusion.
     * 
     * @return {@code true} if and only if the integer {@code integer} is in 
     *         this integer set.
     */
    public boolean contains(final int integer) {
        return data[integer];
    }
    
    /**
     * Removes the integer {@code integer} from this integer set.
     * 
     * @param integer the integer to remove from this integer set.
     */
    public void remove(final int integer) {
        data[integer] = false;
    }
}
io.github.coderodde.sudoku.misc..java:
package io.github.coderodde.sudoku.misc;
import io.github.coderodde.sudoku.SudokuBoard;
import static io.github.coderodde.sudoku.misc.Utils.shuffle;
import java.util.Random;
/**
 * This class is responsible for randomly generating full sudoku boards.
 * 
 * @version 1.0.0 (Jun 22, 2025)
 * @since 1.0.0 (Jun 22, 2025)
 */
public final class RandomSudokuBoardGenerator {
    
    /**
     * The generated sudoku board.
     */
    private final SudokuBoard board;
    
    /**
     * The matrix of random cell value providers.
     */
    private final RandomCellValueProvider[][] providers;
    
    /**
     * The row-wise filters.
     */
    private final IntSet[] rowIntSets;
    
    /**
     * The column-wise filters.
     */
    private final IntSet[] colIntSets;
    
    /**
     * The minisquare filters.
     */
    private final IntSet[][] minisquareIntSets;
    
    /**
     * The minisquare width/height.
     */
    private final int nsqrt;
    
    /**
     * Construct this sudoku board generator.
     * 
     * @param widthHeight the width/height of the resulting sudoku board.
     * @param random      the random number generator.
     */
    public RandomSudokuBoardGenerator(final int widthHeight,
                                      final Random random) {
        this.board = new SudokuBoard(widthHeight);
        this.providers = new RandomCellValueProvider[widthHeight]
                                                    [widthHeight];
        
        for (int y = 0; y < widthHeight; ++y) {
            for (int x = 0; x < widthHeight; ++x) {
                this.providers[y][x] = new RandomCellValueProvider(widthHeight,
                                                                   random);
            }
        }
        
        this.rowIntSets = new IntSet[widthHeight];
        this.colIntSets = new IntSet[widthHeight];
        this.minisquareIntSets = new IntSet[widthHeight]
                                           [widthHeight];
        
        for (int y = 0; y < widthHeight; ++y) {
            this.rowIntSets[y] = new IntSet(widthHeight + 1);
        }
        
        for (int x = 0; x < widthHeight; ++x) {
            this.colIntSets[x] = new IntSet(widthHeight + 1);
        }
        
        final int sqrtn = (int) Math.sqrt(widthHeight);;
        
        for (int y = 0; y < widthHeight; ++y) {
            for (int x = 0; x < widthHeight; ++x) {
                this.minisquareIntSets[y / sqrtn]
                                      [x / sqrtn] = new IntSet(widthHeight + 1);
            }
        }
        
        this.nsqrt = (int) Math.sqrt(widthHeight);
    }
    
    /**
     * The actual generation method.
     * 
     * @return a randomly  built sudoku board.
     */
    public SudokuBoard generateRandomSudokuBoard() {
        generateRandomSudokuBoardImpl(board, 0, 0);
        return board;
    }
    
    /**
     * Implements the random sudoku board generation.
     * 
     * @param board the board to work on.
     * @param x the {@code x}-coordinate of the next cell to process.
     * @param y the {@code y}-coordinate of the next cell to process.
     * @return {@code true} if and only if a valid sudoku board is constructed.
     *         (Will end up in {code board}.)
     */
    private boolean generateRandomSudokuBoardImpl(final SudokuBoard board, 
                                                  int x,
                                                  int y) {
        if (x == board.getWidthHeight()) {
            x = 0;
            ++y;
        }
        
        if (y == board.getWidthHeight()) {
            return true;
        }
        
        for (final int cellValue : providers[y][x].getCellValues()) {
            
            if (rowIntSets[y].contains(cellValue)) {
                continue;
            }
            
            if (colIntSets[x].contains(cellValue)) {
                continue;
            }
            
            if (minisquareIntSets[y / nsqrt]
                                 [x / nsqrt].contains(cellValue)) {
                continue;
            }
            
            rowIntSets[y].add(cellValue);
            colIntSets[x].add(cellValue);
            minisquareIntSets[y / nsqrt]
                             [x / nsqrt].add(cellValue);
            
            board.set(x, y, cellValue);
            
            if (generateRandomSudokuBoardImpl(board,
                                              x + 1,
                                              y)) {
                return true;
            }
            
            rowIntSets[y].remove(cellValue);
            colIntSets[x].remove(cellValue);
            minisquareIntSets[y / nsqrt]
                             [x / nsqrt].remove(cellValue);
        }
        
        return false;
    }
    
    /**
     * This inner static class implements a random cell value provider.
     */
    private static final class RandomCellValueProvider {
        
        private final int[] randomCellValues;
        /**
         * Constructs this random cell value provider.
         * 
         * @param n the width/height of the target sudoku board.
         */
        RandomCellValueProvider(final int n, final Random random) {
            this.randomCellValues = new int[n];
            
            for (int i = 0; i < n; ++i) {
                randomCellValues[i] = i + 1;
            }
            
            shuffle(randomCellValues, random);
        }
        
        int[] getCellValues() {
            return this.randomCellValues;
        }
    }
}
io.github.coderodde.sudoku.misc.RandomSudokuBoardSeedProvider.java:
package io.github.coderodde.sudoku.misc;
import io.github.coderodde.sudoku.SudokuBoard;
import java.awt.Point;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
 * This class provides facilities for generating random sudoku board seeds.
 * 
 * @version 1.0.0 (Jun 23, 2025)
 * @since 1.0.0 (Jun 23, 2025)
 */
public final class RandomSudokuBoardSeedProvider {
    
    /**
     * Computes a list of seed sudoku boards.
     * 
     * @param sourceBoard    the source sudoku board.
     * @param requestedSeeds the requested number of seeds.
     * 
     * @return a list of seeds.
     */
    public static List<SudokuBoard> computeSeeds(final SudokuBoard sourceBoard,
                                                 final int requestedSeeds) {
        
        // Get the list of all empty cells:
        final List<Point> emptyCellPoints = getEmptyCellPoints(sourceBoard);
        
        // Fix the seed list capacity:
        final int seedsListCapacity = 
                Math.min(emptyCellPoints.size(), 
                         requestedSeeds);
        
        final Random random = new Random();
        
        final List<SudokuBoard> seeds = new ArrayList<>(seedsListCapacity);
        
        // Actual seed generatoin:
        for (int i = 0; i < seedsListCapacity; ++i) {
            seeds.add(computeRandomSeed(sourceBoard, 
                                        emptyCellPoints, 
                                        random));
        }
        
        return seeds;
    }
    
    /**
     * Computes another random seed.
     * 
     * @param board                the current board.
     * @param emptyCellCoordinates the list of empty cell coordinates.
     * @param random               the random number generator.
     * @return a random seed.
     */
    private static SudokuBoard computeRandomSeed(
            final SudokuBoard board,
            final List<Point> emptyCellCoordinates,
            final Random random) {
        
        // Get a copy:
        final SudokuBoard seed = new SudokuBoard(board);
        
        // Get the index of the target point containing no valid cell value:
        final int targetPointIndex = 
                random.nextInt(emptyCellCoordinates.size());
        
        // Get the target point:
        final Point targetPoint = emptyCellCoordinates.get(targetPointIndex);
        
        // Get an array of randomly arranged cell values:
        final int[] cellValues = getRandomCellValues(board.getWidthHeight());
        
        for (final int cellValue : cellValues) {
            // Set the cell value:
            seed.set(targetPoint.x, 
                     targetPoint.y, 
                     cellValue);
            
            // Check that after new cell value the seed remains valid:
            if (SudokuBoardVerifier.isValid(seed)) {
                // Once here, we have found a seed. Return it:
                emptyCellCoordinates.remove(targetPointIndex);
                return seed;
            }
        }
        
        throw new IllegalStateException("Should not get here");
    }
    
    /**
     * Computes the list of point coordinates pointing to empty cells.
     * 
     * @param board the board to process.
     * @return the list of point coordinates pointing to empty cells.
     */
    private static List<Point> getEmptyCellPoints(final SudokuBoard board) {
        final List<Point> emptyCellPoints = new ArrayList<>();
        
        for (int y = 0; y < board.getWidthHeight(); ++y) {
            for (int x = 0; x < board.getWidthHeight(); ++x) {
                if (board.get(x, y) == Utils.UNUSED_CELL) {
                    emptyCellPoints.add(new Point(x, y));
                }
            }
        }
        
        return emptyCellPoints;
    }
    
    /**
     * Computes a randomly ordered array of cell values.
     * 
     * @param widthHeight the width/height of the target board.
     * @return a randomly ordered array of cell values.
     */
    private static int[] getRandomCellValues(final int widthHeight) {
        final int[] cellValues = new int[widthHeight];
        
        for (int i = 0; i < widthHeight; ++i) {
            cellValues[i] = i + 1;
        }
        
        Utils.shuffle(cellValues);
        return cellValues;
    }
}
io.github.coderodde.sudoku.misc.SudokuBoardVerifier.java:
package io.github.coderodde.sudoku.misc;
import io.github.coderodde.sudoku.SudokuBoard;
/**
 * This class provides a static method for checking that an input sudoku board 
 * is valid, i.e., does not break rules of sudoku. For example, the method in
 * question must return {@code false} if a column contains duplicate cell 
 * values.
 * 
 * @version 1.0.0 (Jun 22, 2025)
 * @since 1.0.0 (Jun 22, 2025)
 */
public final class SudokuBoardVerifier {
   
    private SudokuBoardVerifier() {
        
    }
    
    /**
     * Verifies the input sudoku board.
     * 
     * @param board the sudoku board to verify.
     * 
     * @return {@code true} if and only if the input sudoku board is valid.
     */
    public static boolean isValid(final SudokuBoard board) {
        final int n = board.getWidthHeight();
        
        final IntSet[] rowIntSets = new IntSet[n];
        final IntSet[] colIntSets = new IntSet[n];
        
        for (int i = 0; i < n; ++i) {
            rowIntSets[i] = new IntSet(n + 1);
            colIntSets[i] = new IntSet(n + 1);
        }
        
        final int sqrtn = (int) Math.sqrt(n);
        final IntSet[][] minisquareIntSets = new IntSet[sqrtn]
                                                       [sqrtn];
        
        for (int y = 0; y < sqrtn; ++y) {
            for (int x = 0; x < sqrtn; ++x) {
                minisquareIntSets[y][x] = new IntSet(n + 1);
            }
        }
        
        for (int y = 0; y < n; ++y) {
            for (int x = 0; x < n; ++x) {
                final int cellValue = board.get(x, y);
                
                if (cellValue == Utils.UNUSED_CELL) {
                    continue;
                }
                
                if (!board.isValidCellValue(x, y)) {
                    return false;
                }
                
                final int minisquareCellX = x / sqrtn;
                final int minisquareCellY = y / sqrtn;
                
                if (minisquareIntSets[minisquareCellY]
                                     [minisquareCellX].contains(cellValue)) {
                    return false;
                }
                
                minisquareIntSets[minisquareCellY]
                                 [minisquareCellX].add(cellValue);
                
                if (rowIntSets[x].contains(cellValue)) {
                    return false;
                }
                
                rowIntSets[x].add(cellValue);
                
                if (colIntSets[y].contains(cellValue)) {
                    return false;
                }
                
                colIntSets[y].add(cellValue);
            }
        }
        
        return true;
    }
}
io.github.coderodde.sudoku.misc.Utils.java:
package io.github.coderodde.sudoku.misc;
import io.github.coderodde.sudoku.SudokuBoard;
import java.util.Random;
/**
 * This class provides common utilities.
 * 
 * @version 1.0.0 (Jun 22, 2025)
 * @since 1.0.0 (Jun 22, 2025)
 */
public final class Utils {
    
    public static final int UNUSED_CELL = 0;
    private static final int MINIMUM_WIDTH_HEIGHT = 4;
    
    private Utils() {
        
    }
    
    public static void checkWidthHeight(final int widthHeight) {
        if (widthHeight < 4) {
            final String exceptionMessage =
                    String.format(
                            "The widthHeight(%d) < MINIMUM_WIDTH_HEIGHT(%d)",
                            widthHeight,
                            MINIMUM_WIDTH_HEIGHT);
            
            throw new IllegalArgumentException(exceptionMessage);
        }
        
        int n;
        
        for (n = MINIMUM_WIDTH_HEIGHT / 2; ; ++n) {
            if (n * n >= widthHeight) {
                break;
            }
        }
        
        if (n * n != widthHeight) {
            final String exceptionMessage = 
                    String.format(
                            "Abnormal widthHeight(%d)",
                            widthHeight);
            
            throw new IllegalArgumentException(exceptionMessage);
        }
    }
    
    /**
     * This static method implements the 
     * <a href="https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle">
     * Fisher-Yates shuffle</a>.
     * 
     * @param array the array to shuffle.
     */
    public static void shuffle(final int[] array, final Random random) {
        for (int i = array.length - 1; i > 0; --i) {
            final int j = random.nextInt(i + 1);
            final int tmp = array[j];
            array[j] = array[i];
            array[i] = tmp;
        }
    }   
    
    /**
     * Passes to {@link #shuffle(int[], java.util.Random)}.
     * 
     * @param array the array to shuffle.
     */
    public static void shuffle(final int[] array) {
        shuffle(array, new Random());
    }
    
    public static boolean isCompleteSudokuBoard(final SudokuBoard board) {
        for (int y = 0; y < board.getWidthHeight(); ++y) {
            for (int x = 0; x < board.getWidthHeight(); ++x) {
                if (board.get(x, y) == Utils.UNUSED_CELL) {
                    return false;
                }
            }
        }
        
        return true;
    }
}
Typical output
I managed to produce that optimistic results:
Generating a random full sudoku board...
Board generated. Pruning...
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 5|16|13| 6| 7|12| 4|10| 8| 3| 9| 2| 1|15|14|11|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1|14|12| 4| 6| 8|15|11|16| 7| 5|10| 9| 3|13| 2|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 2| 3|15| 9| 1|16|13| 5|11| 6| 4|14| 7|12|10| 8|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|10| 8| 7|11| 2| 9|14| 3| 1|13|15|12| 6| 5|16| 4|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|15|12| 9| 8|14|10| 7|13| 4| 2|16| 5|11| 1| 3| 6|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|11| 2|16| 1| 3| 6| 5| 9|12|15|10| 8| 4|13| 7|14|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 3| 7| 4|14|15| 1| 2|12|13| 9| 6|11| 8|16| 5|10|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|13| 6|10| 5|11| 4|16| 8| 7|14| 3| 1| 2| 9|15|12|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|14| 9|11| 3|16| 2|12| 1|15| 4| 7| 6| 5|10| 8|13|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 4|13| 2|16| 5|11| 6|14| 9|10| 8| 3|15| 7|12| 1|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 7|15| 8|12|13| 3|10| 4| 5|11| 1|16|14| 6| 2| 9|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 6| 5| 1|10| 9|15| 8| 7|14|12| 2|13|16|11| 4| 3|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 9|10|14| 7|12| 5| 1| 6| 2| 8|13|15| 3| 4|11|16|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|16|11| 3|13| 8|14| 9|15| 6| 5|12| 4|10| 2| 1| 7|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|12| 4| 5| 2|10| 7|11|16| 3| 1|14| 9|13| 8| 6|15|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 8| 1| 6|15| 4|13| 3| 2|10|16|11| 7|12|14| 9| 5|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Threads: 64, duration: 2419 ms, complete = true.
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 5|16|14| 6| 4|12| 7|10| 8| 3| 9| 2|15|11|13| 1|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1| 4|12|13| 6| 8|15|11|16|10| 5| 7| 9| 3|14| 2|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 2| 3|15| 9| 1|16|13| 5|11| 4| 6|14| 7|12|10| 8|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|10| 8| 7|11| 2| 9|14| 3| 1|13|15|12| 6| 5|16| 4|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 7|12| 9| 8|14|10|16|13| 4| 2| 3| 5|11| 1|15| 6|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|11| 2|16| 1| 3| 6| 5| 9|12|15|10| 8| 4|13| 7|14|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 3|15| 4|14| 7| 1|12| 2| 9| 6|13|11| 8|16| 5|10|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|13| 6|10| 5|15| 4|11| 8| 7|14|16| 1| 2| 9| 3|12|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|14| 9|11| 3|16| 2| 4| 1|15| 7| 8| 6| 5|10|12|13|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 4|13| 2|16| 5|15| 6|12|10| 9| 1| 3|14| 7| 8|11|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|15| 7| 8|12|13| 3|10|14| 5|11| 4|16| 1| 6| 2| 9|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 6| 5| 1|10| 9|11| 8| 7|14|12| 2|13| 3|15| 4|16|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 9|10|13| 7|12| 5| 1| 6| 2| 8|14|15|16| 4|11| 3|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|16|11| 3| 2| 8|13| 9|15| 6| 5|12| 4|10|14| 1| 7|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|12|14| 5| 4|10| 7| 2|16| 3| 1|11| 9|13| 8| 6|15|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 8| 1| 6|15|11|14| 3| 4|13|16| 7|10|12| 2| 9| 5|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Threads: 32, duration: 27295 ms, complete = true.
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 5|16|13| 6| 7|12| 4|10| 8| 3| 9| 2|14|11|15| 1|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1|14|12| 4| 6| 8|15|11|16| 7| 5|10| 9| 3|13| 2|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 2| 3|15| 9| 1|16|13| 5|11| 6| 4|14| 7|12|10| 8|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|10| 8| 7|11| 2| 9|14| 3| 1|13|15|12| 6| 5|16| 4|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|15|12| 9| 8|14|10| 7|13| 4| 2|16| 5|11| 1| 3| 6|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|11| 2|16| 1| 3| 6| 5| 9|12|15|10| 8| 4|13|14| 7|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 3| 7| 4|14|15| 1| 2|12|13| 9| 6|11| 8|16| 5|10|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|13| 6|10| 5|11| 4|16| 8| 7|14| 3| 1|15| 9| 2|12|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|14| 9|11| 3|16| 2|12| 1|15| 4| 7| 6| 5|10| 8|13|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 4|13| 2|16| 5|15| 6|14| 9|10| 8| 3| 1| 7|12|11|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 7|15| 8|12| 9| 3|10| 4| 5|11| 1|13| 2|14| 6|16|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 6| 5| 1|10|13|11| 8| 7|14|12| 2|16| 3|15| 4| 9|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 9|10| 5| 7|12|14| 1| 6| 2| 8|13|15|16| 4|11| 3|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|16|11| 3|13| 8| 7| 9|15| 6| 5|12| 4|10| 2| 1|14|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|12| 4| 6| 2|10| 5|11|16| 3| 1|14| 9|13| 8| 7|15|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 8| 1|14|15| 4|13| 3| 2|10|16|11| 7|12| 6| 9| 5|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Threads: 16, duration: 21201 ms, complete = true.
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 5|16|15| 6| 7|12| 4|10| 8| 3| 9| 2|13|11|14| 1|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1| 4|12|13| 6| 8|15|11|16| 7| 5|14| 9| 3|10| 2|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 2| 3|14| 9| 1|16|13| 5|11| 6| 4|10| 7|12|15| 8|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|10| 8| 7|11| 2| 9|14| 3| 1|13|15|12| 6| 5|16| 4|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|15|12| 9| 8|14|10| 7|13| 4| 2|16| 5|11| 1| 3| 6|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|11| 2|16| 1| 3| 6| 5| 9|13|15|12| 8| 4|14| 7|10|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 3| 7| 4|14|15| 1| 2|12| 6|10|11| 9| 8|16|13| 5|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|13| 6|10| 5|11| 4|16| 8| 7|14| 3| 1|15| 9| 2|12|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|14| 9|11| 3|16| 2|12| 1|15| 4| 7| 6| 5|10| 8|13|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 4|13| 2|16| 5|15| 6|14| 9| 8|10| 3| 1| 7|12|11|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 7|15| 8|12|13| 3|10| 4| 5|11| 1|16|14| 2| 6| 9|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 6| 5| 1|10| 9|11| 8| 7|14|12| 2|13|16|15| 4| 3|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 8|10|13| 7|12| 5| 1| 6| 2| 9|14|15| 3| 4|11|16|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|16|11| 3| 2| 8| 7| 9|15|12| 5|13| 4|10| 6| 1|14|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|12|14| 5| 4|10|13|11|16| 3| 1| 6| 7| 2| 8| 9|15|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 9| 1| 6|15| 4|14| 3| 2|10|16| 8|11|12|13| 5| 7|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Threads:  8, duration: 6445 ms, complete = true.
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 5|16|13| 6| 7|12| 4|10| 8| 3| 9| 2| 1|11|15|14|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1|14|12| 4| 6| 8|15|11|16|10| 5| 7| 9| 3|13| 2|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 2| 3|15| 9| 1|16|13| 5|11| 4| 6|14| 7|12|10| 8|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|10| 8| 7|11| 2| 9|14| 3| 1|13|15|12| 6| 5|16| 4|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|15|12| 9| 8|14|10| 7|13| 4| 2|16| 5|11| 1| 3| 6|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|11| 2|16| 1| 3| 6| 5|12| 9|15|10| 8| 4|13|14| 7|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 3| 7| 4|14|15| 1| 9| 2|12| 6|11|13| 8|16| 5|10|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|13| 6|10| 5|16| 4|11| 8| 7|14| 3| 1|15| 9| 2|12|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|14| 9|11| 3|12| 2|16| 1|15| 7| 4| 6| 5|10| 8|13|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 4|13| 2|16| 5|15| 6| 9|10| 8| 1| 3|14| 7|12|11|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 7|15| 8|12| 4| 3|10|14| 5|11|13|16| 2| 6| 9| 1|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 6| 5| 1|10|11|13| 8| 7|14|12| 2| 9|16|15| 4| 3|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 8|10|14| 7|13| 5| 1| 6| 2| 9|12|15| 3| 4|11|16|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|16|11| 3| 2|10| 7|12|15| 6| 5| 8| 4|13|14| 1| 9|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|12| 4| 5|13| 9|14| 2|16| 3| 1| 7|11|10| 8| 6|15|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 9| 1| 6|15| 8|11| 3| 4|13|16|14|10|12| 2| 7| 5|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Threads:  4, duration: 4555 ms, complete = true.
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 5|16|13| 6| 7|12| 4|10| 8| 3| 9| 2| 1|15|14|11|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1|14|12| 4| 6| 8|15|11|16|10| 5| 7| 9| 3|13| 2|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 2| 3|15| 9| 1|16|13| 5|11| 4| 6|14| 7|12|10| 8|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|10| 8| 7|11| 2| 9|14| 3| 1|13|15|12| 6| 5|16| 4|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|15|12| 9| 8|14|10| 7|13| 4| 2|16| 5|11| 1| 3| 6|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|11| 2|16| 1| 3| 6| 5| 9|12|15|10| 8| 4|13| 7|14|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 3| 7| 4|14|15| 1| 2|12| 9| 6|13|11| 8|16| 5|10|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|13| 6|10| 5|11| 4|16| 8| 7|14| 3| 1| 2| 9|15|12|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|14| 9|11| 3|16| 2|12| 1|15| 7| 4| 6| 5|10| 8|13|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 4|13| 2|16| 5|11| 6|14|10| 9| 8| 3|15| 7|12| 1|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 7|15| 8|12| 9| 3|10| 4| 5|11| 1|13|14| 6| 2|16|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 6| 5| 1|10|13|15| 8| 7|14|12| 2|16| 3|11| 4| 9|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 9|10| 5| 7|12|13| 1| 6| 2| 8|14|15|16| 4|11| 3|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|16|11| 3|13| 8|14| 9|15| 6| 5|12| 4|10| 2| 1| 7|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|12| 4|14| 2|10| 5|11|16| 3| 1| 7| 9|13| 8| 6|15|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 8| 1| 6|15| 4| 7| 3| 2|13|16|11|10|12|14| 9| 5|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Threads:  2, duration: 17218 ms, complete = true.
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 5|16|13| 6| 7|12| 4|10| 8| 3| 9| 2| 1|14|15|11|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1|14|12| 4| 6| 8|15|11|16| 7| 5|10| 9| 3|13| 2|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 2| 3|15| 9| 1|16|13| 5|11| 6| 4|14| 7|12|10| 8|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|10| 8| 7|11| 2| 9|14| 3| 1|13|15|12| 6| 5|16| 4|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 7|12| 9| 8|15|10|16|13| 4| 2| 3| 5|11| 1|14| 6|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|11| 2|16| 1| 3| 6| 7|14| 9|15|12| 8| 4|13| 5|10|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 3|15| 4|14| 9| 1| 5|12| 6|10|11|13| 8|16| 2| 7|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|13| 6|10| 5| 8| 4|11| 2| 7|14|16| 1|15| 9| 3|12|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|14| 9|11| 3|16| 2|12| 1|15| 4| 7| 6| 5|10| 8|13|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|15|13| 2|16| 5|11| 6| 4|10| 8| 1| 3|14| 7|12| 9|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 4| 7| 8|12|14| 3|10| 9| 5|11|13|16| 2|15| 6| 1|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 6| 5| 1|10|13|15| 8| 7|14|12| 2| 9| 3|11| 4|16|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 8|10| 5| 7|12|13| 1| 6| 2| 9|14|15|16| 4|11| 3|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|16|11| 3| 2|10| 7| 9|15|12| 5| 8| 4|13| 6| 1|14|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|12| 4|14|13|11| 5| 2|16| 3| 1| 6| 7|10| 8| 9|15|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 9| 1| 6|15| 4|14| 3| 8|13|16|10|11|12| 2| 7| 5|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Threads:  1, duration: 24279 ms, complete = true.
Critique request
As always, I am eager to hear any constructive commentary on my attempt.