Modeling Sudokus creating columns and rows incrementally using xprs_newcol and xprs_addrow
|
|
Type: | Programming |
Rating: | 1 (simple) |
Description: | Incremental modelling of different Sudoku variants of increasing difficulty |
File(s): | sudoku_incremental.R |
|
sudoku_incremental.R |
##################################### # This file is part of the # # Xpress-R interface examples # # # # (c) 2022-2025 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) #' #' #' |
© 2001-2025 Fair Isaac Corporation. All rights reserved. This documentation is the property of Fair Isaac Corporation (“FICO”). Receipt or possession of this documentation does not convey rights to disclose, reproduce, make derivative works, use, or allow others to use it except solely for internal evaluation purposes to determine whether to purchase a license to the software described in this documentation, or as otherwise set forth in a written software license agreement between you and FICO (or a FICO affiliate). Use of this documentation and the software described in it must conform strictly to the foregoing permitted uses, and no other use is permitted.