This file is part of the ForSudoku Fortran project. Copyright (C) 2006-2024 Vincent Magnin & Norwid Behrnd
This is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version.
This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; see the files LICENSE and LICENSE_EXCEPTION respectively. If not, see http://www.gnu.org/licenses/.
Contributed by Vincent Magnin, 2006-11-27; Norwid Behrnd, 2023 Last modifications: 2023-09-12, vmagnin 2024-03-23
!! This file is part of the ForSudoku Fortran project. !! Copyright (C) 2006-2024 Vincent Magnin & Norwid Behrnd !! !! This is free software; you can redistribute it and/or modify !! it under the terms of the GNU General Public License as published by !! the Free Software Foundation; either version 3, or (at your option) !! any later version. !! !! This software is distributed in the hope that it will be useful, !! but WITHOUT ANY WARRANTY; without even the implied warranty of !! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the !! GNU General Public License for more details. !! !! You should have received a copy of the GNU General Public License along with !! this program; see the files LICENSE and LICENSE_EXCEPTION respectively. !! If not, see <http://www.gnu.org/licenses/>. !!------------------------------------------------------------------------------ !! Contributed by Vincent Magnin, 2006-11-27; Norwid Behrnd, 2023 !! Last modifications: 2023-09-12, vmagnin 2024-03-23 !!------------------------------------------------------------------------------ module sudoku !! Sudoku module !! !! @author: Vincent Magnin and Norwid Behrnd !! !! This module contains a set of procedures to generate sudokus grids and !! solve them. implicit none contains !***************************************************************************** ! Input/Output routines !***************************************************************************** subroutine save_grid(grid, filename) !! Input/Output routines integer, dimension(9, 9), intent(in) :: grid character(*), intent(in) :: filename integer :: row, col integer :: fileunit ! file creation: open (newunit=fileunit, file=filename, status="REPLACE") do row = 1, 9 write (fileunit, '(3I2, " |", 3I2, " |", 3I2)') (grid(row, col), col=1, 9) if ((row == 3) .or. (row == 6)) then write (fileunit, *) "------+-------+------" end if end do close (fileunit) end subroutine save_grid subroutine read_grid(grid, filename) !! Read a grid from a provided the path to a file integer, dimension(9, 9), intent(out) :: grid !! Output grid character(*), intent(in) :: filename !! File path character(len=2) :: pipe1, pipe2 ! to read the pipe/the vertical bar integer :: row integer :: fileunit logical :: file_exists ! check for the presence of the file requested inquire (file=filename, exist=file_exists) if (file_exists .eqv. .false.) stop "The requested file is absent." ! open and read the file, line by line open (newunit=fileunit, file=filename) do row = 1, 9 read (fileunit, '(3I2, A2, 3I2, A2, 3I2)') & grid(row, 1:3), pipe1, grid(row, 4:6), pipe2, grid(row, 7:9) ! skip the lines of dashes if ((row == 3) .or. (row == 6)) then read (fileunit, *) end if end do close (fileunit) end subroutine read_grid subroutine display_grid(grid) !! Display a grid on the terminal. integer, dimension(9, 9), intent(in) :: grid integer :: row, col do row = 1, 9 print '(3I2, " |", 3I2, " |", 3I2)', (grid(row, col), col=1, 9) if ((row == 3) .or. (row == 6)) then print *, "------+-------+------" end if end do end subroutine display_grid subroutine request_grid(grid) integer, dimension(9, 9), intent(inout) :: grid integer :: row, col do row = 1, 9 write (*, "(A, I1, A)") "Enter line ", row, ":" read *, (grid(row, col), col=1, 9) end do end subroutine request_grid !***************************************************************************** ! Validation routines !***************************************************************************** pure logical function valid_colum_or_row(vector) !! Validation of either a row or a column. !! !! Returns true if each digit in the 1D array appears only once integer, dimension(1:9), intent(in) :: vector !! A row or a column ! The number of occurrences of each digit: integer, dimension(1:9) :: counters integer :: i counters = 0 do i = 1, 9 associate(d => vector(i)) if (d /= 0) then counters(d) = counters(d) + 1 if (counters(d) > 1) then valid_colum_or_row = .false. return ! We leave immediately the function and return false end if end if end associate end do valid_colum_or_row = .true. end function valid_colum_or_row pure logical function valid_zone(region) !! Validation of a zone/region. !! !! Returns true if each digit in the 3x3 region appears only once. integer, dimension(1:3, 1:3), intent(in) :: region !! Sudoku's subregion ! The number of occurrences of each digit: integer, dimension(1:9) :: counters integer :: row, col counters = 0 do row = 1, 3 do col = 1, 3 associate(d => region(row, col)) if (d /= 0) then counters(d) = counters(d) + 1 if (counters(d) > 1) then valid_zone = .false. return ! We leave immediately the function and return false end if end if end associate end do end do valid_zone = .true. end function valid_zone pure logical function valid_grid(grid) !! Check if the whole grid is valid. !! !! Returns true if a full grid is valid. integer, dimension(9, 9), intent(in) :: grid !! Sudoku grid. integer :: row, col ! Verification of the 9 lines: do row = 1, 9 if (.not. valid_colum_or_row(grid(row, 1:9))) then valid_grid = .false. return end if end do ! Verification of the 9 columns: do col = 1, 9 if (.not. valid_colum_or_row(grid(1:9, col))) then valid_grid = .false. return end if end do ! Verification of the 9 regions: do row = 1, 7, +3 do col = 1, 7, +3 if (.not. valid_zone(grid(row:row+2, col:col+2))) then valid_grid = .false. return end if end do end do valid_grid = .true. end function valid_grid pure logical function valid_digit(grid, row, col) !! Returns true if the row, column and region of a digit are all valid: integer, dimension(9, 9), intent(in) :: grid !! Sudoky grid. integer, intent(in) :: row !! Row number of the region. integer, intent(in) :: col !! Column number of the region. integer :: i, j i = (row - 1) / 3 j = (col - 1) / 3 valid_digit = valid_colum_or_row(grid(row, 1:9)) .and. & valid_colum_or_row(grid(1:9, col)) .and. & valid_zone(grid(i*3+1:i*3+3, j*3+1:j*3+3)) end function valid_digit pure logical function is_full(grid) !! Returns true if the grid is full. integer, dimension(9, 9), intent(in) :: grid !! Sudoky grid. if (any(grid(:,:) == 0)) then is_full = .false. else is_full = .true. end if end function pure subroutine list_possible_digits(grid, row, col, & nb_possible, possible_digit) !! Procedure to create a list of allowed digits in the present empty cell. integer, dimension(9, 9), intent(in) :: grid !! Sudoku grid integer, intent(in) :: row !! Row number integer, intent(in) :: col !! Column number ! These arguments are returned: integer, intent(out) :: nb_possible integer, dimension(1:9), optional, intent(out) :: possible_digit integer :: cr, lr, i, j logical, dimension(0:9) :: possible ! Each digit is either possible or not possible = .true. ! Given digits in those row and column are excluded: do j = 1, 9 possible(grid(j, col)) = .false. possible(grid(row, j)) = .false. end do ! Given digits in that region are excluded: lr = 1 + 3 * ((row - 1) / 3) cr = 1 + 3 * ((col - 1) / 3) do i = lr, lr + 2 do j = cr, cr + 2 possible(grid(i, j)) = .false. end do end do nb_possible = 0 if (present(possible_digit)) possible_digit = 0 ! Count and store the remaining possible digits: do j = 1, 9 if (possible(j)) then nb_possible = nb_possible + 1 if (present(possible_digit)) possible_digit(nb_possible) = j end if end do end subroutine list_possible_digits !***************************************************************************** ! Solver routines !***************************************************************************** pure subroutine sort(empty_cells, p, n) !! Starting from position p, sort the list of empty cells by !! ascending number of allowed digits. We use a bubble sort algorithm integer, dimension(1:81, 1:3), intent(inout) :: empty_cells !! integer, intent(in) :: p !! The sort starts at position p (included) integer, intent(in) :: n !! Number of empty cells in the list integer :: i integer, dimension(1:3) :: col logical :: none_swap do ! Compare each cell with the next one and swap them if necessary: none_swap = .true. do i = p, n - 1 if (empty_cells(i, 3) > empty_cells(i+1, 3)) then ! Swap them: col = empty_cells(i, :) empty_cells(i , :) = empty_cells(i+1, :) empty_cells(i+1, :) = col none_swap = .false. end if end do if (none_swap) exit ! The bubble sort is finished end do end subroutine sort subroutine solve_puzzle(grid) !! Receives a puzzle grid and solves it. integer, dimension(9, 9), intent(inout) :: grid !! Input problem grid and returns solved grid. integer, dimension(9, 9) :: grid0 real :: r ! Random number integer :: row, col, i, j ! Counter of empty cells: integer :: nb_empty ! List of empty cells: integer, dimension(1:81, 1:3) :: empty_cells ! List and number of possible digits: integer, dimension(1:9) :: possible_digit integer :: nb_possible ! Save the initial grid: grid0 = grid ! Identify and store the coordinates of empty cells in the grid ! in the table "empty_cells": empty_cells = 0 nb_empty = 0 do row = 1, 9 do col = 1, 9 if (grid(row, col) == 0) then nb_empty = nb_empty + 1 empty_cells(nb_empty, 1) = row empty_cells(nb_empty, 2) = col end if end do end do ! Iterate over all empty cells: possible_digit = 0 i = 1 do while (i <= nb_empty) ! To accelerate the algorithm, count for each empty cell the digits ! which could be inserted in that cell: do j = i, nb_empty row = empty_cells(j, 1) col = empty_cells(j, 2) ! The last two arguments have intent(out): call list_possible_digits(grid, row, col, empty_cells(j, 3)) ! empty_cells(j, 3) will contain the nb of possible digits for the ! empty cell number j. end do ! Sort the empty cells: call sort(empty_cells, i, nb_empty) ! For the empty cell i, regenerate a list of possible digits: row = empty_cells(i, 1) col = empty_cells(i, 2) call list_possible_digits(grid, row, col, & & nb_possible, possible_digit) ! If there are possibilities, choose randomly one and ! continue with the next empty cell: if (nb_possible > 0) then call random_number(r) ! 0 <= r < 1 grid(row, col) = possible_digit(1 + int(r * nb_possible)) i = i + 1 else ! Start all over again i = 1 grid = grid0 end if end do end subroutine solve_puzzle subroutine cli_solver(grid, file) !! Provides a solution for a puzzle passed by CLI: !! !! ```shell !! $ ./executable test_in_02.txt !! ``` integer, dimension(9, 9), intent(inout) :: grid !! Sudoku Grid character(*), intent(in) :: file !! Filepath logical :: presence presence = .false. inquire (file=file, exist=presence) if (presence .eqv. .false.) then print *, "ERROR: the requested file '", file, "' is inaccessible." else call read_grid(grid, file) if (valid_grid(grid) .eqv. .true.) then call solve_puzzle(grid) call display_grid(grid) else print *, "The input file'", file, "' is an invalid grid." end if end if end subroutine cli_solver !***************************************************************************** ! Puzzle generators !***************************************************************************** subroutine create_filled_grid(grid) !! Grid generation by brute force: in each cycle a digit is added and !! checked for validity. If the grid becomes invalid, the grid generation !! is started all over again. integer, dimension(9, 9), intent(out) :: grid !! Sudoku grid real :: r integer :: row, col integer :: tests restart:do ! We start with an empty grid: grid = 0 try:do row = 1, 9 do col = 1, 9 tests = 0 digit: do tests = tests + 1 ! We add a random digit in the grid: call random_number(r) grid(row, col) = 1 + int(r * 9) ! and check if the Sudoku constraints are OK: if (valid_digit(grid, row, col)) then ! Let's continue with other cells: exit digit else if (tests > 30) then ! The probability of finding a valid digit is low, ! and we therefore restart a new grid: cycle restart end if end if end do digit end do end do try ! We have left naturally the "try" loop ! and have therefore found a valid grid: exit end do restart end subroutine create_filled_grid subroutine create_puzzle_with_unique_solution(grid, nb_empty) !! Creates a minimal puzzle. !! Digits are randomly removed one by one. The process ends when it is not !! possible anymore to remove a digit while keeping a unique solution. !! The number of remaining digits is therefore a priori unknown. integer, dimension(9, 9), intent(inout) :: grid integer, intent(out) :: nb_empty ! List of the cells, numbered from 1 to 81, line by line: integer, dimension(81) :: list real :: r(2) ! To draw two random numbers integer :: row, col, n, n1, n2, i, temp, d integer :: nb_possible ! List of the cells, numbered from 1 to 81, line by line: list = [(i, i=1,81)] ! The list is randomly shuffled to avoid removing too many neighbours. ! The probability that a position is never drawn is (80/81)^81 ~ 0.107 ! Increasing the upper limit would impede performance. do i = 1, 81 ! We draw two positions: call random_number(r) ! 0 <= r < 1 n1 = 1 + int(r(1) * 81) n2 = 1 + int(r(2) * 81) ! and swap them in the list: temp = list(n1) list(n1) = list(n2) list(n2) = temp end do nb_empty = 0 ! Remove digits one by one: do i = 1, 81 ! Number of the cell in the shuffled list: n = list(i) ! Coordinates of the cell in the grid: row = 1 + (n-1) / 9 col = 1 + mod(n-1, 9) ! We save then delete the digit in that cell: d = grid(row, col) grid(row, col) = 0 ! How many digits are possible at that position? ! Note: 79% of CPU time is spent in list_possible_digits() call list_possible_digits(grid, row, col, nb_possible) if (nb_possible > 1) then ! We put back the digit in the cell: grid(row, col) = d ! and we continue with the next cell in the list... else nb_empty = nb_empty + 1 end if end do end subroutine create_puzzle_with_unique_solution subroutine create_puzzle(grid, givens) !! Creates a puzzle by brute force. !! But we are not 100% sure that the solution is unique !! (just a "high" probability). integer, dimension(9, 9), intent(inout) :: grid !! Sudoku grid. integer, intent(in) :: givens !! Number of given digits in the puzzle. integer, dimension(9, 9) :: grid0 ! Maximum number of times we try to solve a grid: integer, parameter :: n = 1000 ! To store and compare the n Sudoku solutions: integer, dimension(:, :, :), allocatable :: solutions real :: r(2) integer :: row, col, i logical :: unique allocate(solutions(1:n, 1:9, 1:9)) ! Save the initial grid: grid0 = grid print *, "Search of a puzzle (without guaranty for a unique solution)..." do grid = grid0 ! Show the advancement of the algorithm: write(*, '(".")', advance='no') ! Remove digits: do i = 1, 81 - givens ! Choose randomly a cell with a digit: do call random_number(r) row = 1 + int(r(1) * 9) col = 1 + int(r(2) * 9) if (grid(row, col) /= 0) exit end do ! Erase the digit in this cell: grid(row, col) = 0 end do ! The grid is solved up to n times to increase the probability that ! the solution is unique: unique = .true. solve: do i = 1, n solutions(i, :, :) = grid call solve_puzzle(solutions(i, :, :)) ! Is that solution identical to all previous ones? if (i >= 2) then if (any(solutions(i, :, :) /= solutions(i-1, :, :))) then unique = .false. exit solve end if end if end do solve if (unique) exit end do write(*,*) end subroutine create_puzzle !************************************************************** ! System independent initialization of pseudo-random generator !************************************************************** subroutine initialize_random_number_generator(user_seed) !! Initialize random number generator with a seed. integer, optional, intent(in) :: user_seed integer, allocatable, dimension(:) :: seed integer, dimension(1:8) :: time_values integer :: i, n call random_seed(size=n) allocate (seed(1:n)) if (present(user_seed)) then seed = user_seed else ! Real-time clock: call date_and_time(values=time_values) ! We use the milliseconds to compute the seeds: do i = 1, n seed(i) = (huge(seed(i)) / 1000) * time_values(8) - i end do end if call random_seed(put=seed(1:n)) end subroutine initialize_random_number_generator end module sudoku