| |||||||||||
Modeling Sudokus creating columns and rows incrementally using xprs_newcol and xprs_addrow Description Incremental modelling of different Sudoku variants of increasing difficulty Further explanation of this example: Same Sudoku games as in sudoku.R
Source Files By clicking on a file name, a preview is opened at the bottom of this page.
sudoku_incremental.R ##################################### # This file is part of the # # Xpress-R interface examples # # # # (c) 2022-2024 Fair Isaac Corporation # ##################################### #' --- #' title: "Incremental Formulation of Sudoku Puzzles" #' author: Gregor Hendel, Y. Gu #' date: Jun. 2021 #' --- #' ## ----setup, include=FALSE----------------------------------------------------- knitr::opts_chunk$set(echo = TRUE) knitr::opts_chunk$set(results = "hold") knitr::opts_chunk$set(warning = FALSE,message = FALSE) #' #' This example shows an incremental formulation of the same sudoku games as in the example file #' 'sudoku.R', Here, we replace the single call to `xprs_loadproblemdata` to create all rows/columns at once by an incremental approach using `xprs_addrow` and `xprs_newcol`. #' #' #' # Load packages and a function for pretty names #' ## ----------------------------------------------------------------------------- # load the xpress library suppressMessages(library(xpress)) # load the Sudoku package library(sudokuAlt) # for data transformations library(dplyr) # a function to specify good names pretty_name <- function(prefix, y) { "%s_%s" %>% sprintf(prefix, paste(lapply(names(y), function(name) paste(name, y[name], sep = "_")), collapse = "_")) } #' #' #' # Plain Sudoku Game #' #' Firstly, we work with the plain sudoku game, where 4 basic types of restrictions are #' imposed. The mathematical formulation of the plain sudoku game is: #' #' $$ #' \begin{align} #' &\min & 0\\ #' & & \sum\limits_{l = 1}^9 x_{rcl} & = 1 & \forall r\in R, c \in C\\ #' & & \sum\limits_{r = 1}^9 x_{rcl} & = 1 & \forall c \in C, l \in L\\ #' & & \sum\limits_{c = 1}^9 x_{rcl} & = 1 & \forall r \in R, l \in L\\ #' & & \sum\limits_{(r,c) \in S_{ij}} x_{rcl} & = 1 & \forall i,j\in \{1,2,3\}, l \in L\\ #' & & x_{rcl} & = 1 & \forall (r,c,l) \in V_0\\ #' & & x_{rcl} & \in \{0,1\} & \forall r \in R, c \in C, l \in L\\ #' \end{align} #' $$ #' #' ## Game Creation #' ## ----------------------------------------------------------------------------- # create a sudoku game set.seed(123) game <- makeGame() plot(game) #' #' ## Incrementally Formulate The Sudoku Problem #' #' We write a function `incrementally_formulate_sudoku` to incrementally formulate the #' sudoku problem. Like `load_sudoku_problem`, `incrementally_formulate_sudoku` #' returns a 'prob' and 'index.df' for further processing. #' ## ----------------------------------------------------------------------------- incrementally_formulate_sudoku <- function(prob = NULL, game) { # create a new prob object if none was specified if (is.null(prob)) { prob <- createprob() } # create some sets to match the notation above R <- 1:9 C <- 1:9 L <- 1:9 # create a convenient data frame that contains all combinations of R,C,and L to simplify indexing index.df <- expand.grid(R, C, L) names(index.df) <- c("R", "C", "L") # infer the subcell for each row/column combination. index.df$Si <- (index.df$R - 1) %/% 3 + 1 index.df$Sj <- (index.df$C - 1) %/% 3 + 1 # add columns index.df$VarIdx <- index.df %>% apply(1, function(x) xprs_newcol( prob, lb = 0, ub = 1, coltype = "B", name = paste0("X_", paste(x[c("R", "C", "L")], collapse = "_")) )) # We use the designGame function to query a data frame representation of the Sudoku. game.df <- sudokuAlt::designGame(game) # We convert the game.df and merge the symbols game.df <- game.df[game.df$Symbol != "LNA", ] game.df$R <- as.integer(gsub("R", "", game.df$Row)) game.df$C <- as.integer(gsub("C", "", game.df$Col)) game.df$Si <- as.integer(gsub("S(.).", "\\1", game.df$Square)) game.df$Sj <- as.integer(gsub("S.(.)", "\\1", game.df$Square)) index.df <- merge(index.df, game.df, all.x = T) index.df <- index.df[order(index.df$VarIdx), ] # fix the initial assignment by setting variable lower bounds initial.assignment <- which(sprintf("L%d", index.df$L) == index.df$Symbol) initial.length <- length(initial.assignment) # change the lower bounds of the initial assignment columns chgbounds( prob, colind = index.df$VarIdx[initial.assignment], bndtype = rep("L", initial.length), bndval = rep(1, initial.length) ) # add rows according to the 4 sets of constraints # There are two possible approaches to add rows and function `group_map` is used # in both of them. Using `group_map` will be more effective than using 'for loop' and # filtering the data frame for indices we want in each iteration. # Approach 1 firstly creates a list to store the column indices and then add rows, # and approach 2 adds rows inside of the function 'group_map'. # For the first two sets of constraints, we use approach 1 and for the last two sets # of constraints, we use approach 2. # 1. only one symbol in each cell idxlist.cell <- index.df %>% group_by(R, C) %>% group_map( ~ .x$VarIdx) for (i in 1:length(idxlist.cell)) { xprs_addrow( prob, colind = idxlist.cell[[i]], rowcoef = rep(1, 9), rowtype = "E", rhs = 1, name = paste0("cell_constraint", i, collapse = "_") ) } # 2. each symbol only once per column idxlist.column <- index.df %>% group_by(C, L) %>% group_map( ~ .x$VarIdx) for (i in 1:length(idxlist.column)) { xprs_addrow( prob, colind = idxlist.column[[i]], rowcoef = rep(1, 9), rowtype = "E", rhs = 1, name = paste0("column_constraint", i, collapse = "_") ) } # 3. each symbol only once per row index.df %>% group_by(R, L) %>% group_map( ~ xprs_newrow( prob, colind = .x$VarIdx, rowcoef = rep(1, 9), rowtype = 'E', rhs = 1, name = pretty_name("Row", .y) ) ) # 4. each symbol only once in each square: index.df %>% group_by(Si, Sj, L) %>% group_map( ~ xprs_newrow( prob, colind = .x$VarIdx, rowcoef = rep(1, 9), rowtype = 'E', rhs = 1, name = pretty_name("Square", .y) ) ) # specify the name of the problem setprobname(prob, "Sudoku") # returns prob and index.df return(list(prob = prob, index.df = index.df)) } #' #' ## Solve The Game With Xpress #' ## ----------------------------------------------------------------------------- problist <- incrementally_formulate_sudoku(game = game) prob <- problist$prob print(prob) setoutput(prob) mipoptimize(prob) summary(prob) #' #' ## Plot The Solution #' ## ----------------------------------------------------------------------------- solution <- getmipsol(prob)$x # we write a function that enables Sudoku style plotting from ROI example: to_sudoku_solution <- function(solution, index.df) { # filter the rows with solution value very close to 1 instead of exactly 1, because # it will be safer to have a tolerance when comparing values. rowbyrowsol <- index.df %>% filter(near(solution, 1) == TRUE) %>% arrange(R, C) matrix_solution <- matrix(rowbyrowsol$L, 9, 9, byrow = T) sudoku_solution <- structure(matrix_solution, class = c("sudoku", "matrix")) sudoku_solution } sudoku_solution <- to_sudoku_solution(solution, problist$index.df) par(mfrow = c(1, 2)) plot(game) plot(sudoku_solution) #' #' #' # Sudoku With Diagonal Constraints #' #' A sudoku game with diagonal constraints requires that along the main diagonal each #' symbol appears only once, and the same holds for anti diagonal. For mathematical #' notations, please refer to the 'sudoku.R' file. #' #' ## Game Creation #' ## ----------------------------------------------------------------------------- # create a game with slightly less initial assignments than before. sudoku_game <- sudokuAlt::makeGame(gaps = 68) plot(sudoku_game) #' #' ## Add Diagonal And Antidiagonal Constraints #' ## ----------------------------------------------------------------------------- # add main diagonal constraints add_main_diagonal <- function(prob, index.df) { diagonal.df <- index.df %>% filter(R == C) %>% group_by(L) %>% group_map( ~ xprs_addrow( prob = prob, rowtype = "E", rhs = 1, colind = .x$VarIdx, rowcoef = rep(1, 9), name = pretty_name("diagonal_", .y) ) ) return(prob) } # add anti-diagonal constraints add_anti_diagonal <- function(prob, index.df) { antidiagonal.df <- index.df %>% filter(R + C == 10) %>% group_by(L) %>% group_map( ~ xprs_addrow( prob = prob, rowtype = "E", rhs = 1, colind = .x$VarIdx, rowcoef = rep(1, 9), name = pretty_name("antidiagonal_", .y) ) ) return(prob) } #' #' ## Solve The Game #' ## ----------------------------------------------------------------------------- # incrementally formulate the game and add diagonals problist <- incrementally_formulate_sudoku(game = sudoku_game) probdiag <- problist$prob index.df <- problist$index.df print(probdiag) setoutput(probdiag) probdiag <- add_main_diagonal(probdiag, index.df) probdiag <- add_anti_diagonal(probdiag, index.df) mipoptimize(probdiag) summary(probdiag) #' #' ## Plot The Solution #' ## ----------------------------------------------------------------------------- solution <- getmipsol(probdiag)$x sudoku_solution <- to_sudoku_solution(solution, index.df) par(mfrow = c(1, 2)) plot(sudoku_game) plot(sudoku_solution) #' #' #' # Solving a Sudoku with Only 4 Digits #' #' Simon Anthony solves a Sudoku with only 4 given digits in his Youtube channel #' [Cracking the Cryptic](https://www.youtube.com/watch?v=hAyZ9K2EBF0), and following #' rules apply to this game: #' #' * Classical rules (each digit once per row, column, and square) #' * Diagonals, see Section above #' * The central square must also form a *magic square*, meaning that #' the digits in each row and column of the central square must sum up #' to the same number. #' * Cells that are a knight's move apart (in chess) must not contain the same digit. #' #' For the mathematical formulation of this problem, please refer to the 'sudoku.R' file. #' #' ## Game Creation #' ## ----------------------------------------------------------------------------- # Make a fresh game and override the given symbols by the four digits from the video. game4digits <- makeGame() for (i in 1:9) for (j in 1:9) game4digits[i,j] <- NA game4digits[4,1] <- 3; game4digits[4,2] <- 8; game4digits[4,3] <- 4; game4digits[9,9] <- 2 plot(game4digits) #' #' ## Add Square Constraints, Get Knight's Move Neighbours And Add Knight's Move Constraints #' ## ----------------------------------------------------------------------------- # add magic square constraints add_magic_square <- function(prob, index.df) { # consider only the central square S_22 central.square.df <- index.df %>% filter(Si == 2) %>% filter(Sj == 2) # group by row and add the constraints for each row central.square.df.by.row <- central.square.df %>% group_by(R) %>% group_map( ~ xprs_addrow( prob, rowtype = "E", rhs = 15, colind = .x$VarIdx, rowcoef = .x$L, name = pretty_name("central.row_", .y) ) ) # group by column and add the constraints for each column central.square.df.by.col <- central.square.df %>% group_by(C) %>% group_map( ~ xprs_addrow( prob, rowtype = "E", rhs = 15, colind = .x$VarIdx, rowcoef = .x$L, name = pretty_name("central.col_", .y) ) ) # return prob with the added constraints return(prob) } # get knight's move neighbours get_knights_move_neighbors <- function(r, c) { # create all possible moves, possibly outside the feasible range # note that this traversal will encounter each pair of conflicting variables x,y # twice. The first time when all neighboring cells of x are traversed, # and a second time when those around y are determined. # The resulting duplicate rows are detected by Xpress in presolving neighbors <- matrix( c( r - 2, c - 1, r + 2, c - 1, r - 2, c + 1, r + 2, c + 1, r - 1, c - 2, r + 1, c - 2, r - 1, c + 2, r + 1, c + 2 ), byrow = T, ncol = 2 ) # filter elements outside the feasible range and return neighbors[(neighbors[, 1] >= 1) & (neighbors[, 1] <= 9) & (neighbors[, 2] <= 9) & (neighbors[, 2] >= 1), ] } # add the knight's move constraints add_knights_move_constraints <- function(prob, index.df) { # one additional constraint for each r,c,l # we establish the R,C,L order in index.df.sorted. # An element r,c,l has the index 81 * (r - 1) + 9 * (c - 1) + l index.df.sorted <- index.df %>% arrange(R, C, L) for (row in 1:9) { for (col in 1:9) { neighbors <- get_knights_move_neighbors(row, col) # add constraints that forbid the same symbol in the neighbors and this cell # one for each symbol for (symbol in 1:9) { cellvaridx <- index.df.sorted$VarIdx[81 * (row - 1) + 9 * (col - 1) + symbol] neighborvarindex <- index.df.sorted$VarIdx[81 * (neighbors[, 1] - 1) + 9 * (neighbors[, 2] - 1) + symbol] # add one row per neighbor for (n in neighborvarindex) { prob <- xprs_addrow( prob, rowtype = "L", rhs = 1, colind = c(cellvaridx, n), rowcoef = c(1, 1), name = sprintf("knights_move_cell_%d_%d", cellvaridx, n) ) } } } } return(prob) } #' #' ## Load All Constraints And Solve The Game #' ## ----------------------------------------------------------------------------- # incrementally formulate the problem problist <- incrementally_formulate_sudoku(game = game4digits) prob4digits <- problist$prob # add diagonals prob4digits <- add_main_diagonal(prob4digits, problist$index.df) %>% add_anti_diagonal(problist$index.df) %>% # add magic square add_magic_square(problist$index.df) %>% # add knight's move add_knights_move_constraints(problist$index.df) setoutput(prob4digits) summary(mipoptimize(prob4digits)) #' #' #' ## Plot The Solution #' ## ----------------------------------------------------------------------------- solution <- getmipsol(prob4digits)$x solution4digits <- to_sudoku_solution(solution, problist$index.df) par(mfrow = c(1, 2)) plot(game4digits) plot(solution4digits) #' #' #' | |||||||||||
© Copyright 2024 Fair Isaac Corporation. |