--- title: "N queens puzzle" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{N queens puzzle} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = FALSE, comment = "#>" ) ``` ```{r setup} library(satire) library(ggplot2) ``` ```{r echo=FALSE} library(ggplot2) board <- function(n = 4, cols = c('grey95', 'grey80')) { b <- expand.grid(x = seq(n), y = seq(n)) b$col <- with(b, ifelse(y %% 2 == 0, x %% 2 == 0, x %% 2 == 1)) ggplot(b) + geom_raster(aes(x, y, fill = col)) + scale_fill_manual(values = cols, guide = 'none') + coord_equal() + scale_y_reverse() + theme_void() } ``` ## The N-queens puzzle The [N queens](https://en.wikipedia.org/wiki/Eight_queens_puzzle) puzzle is a challenge to place `N` queens on an `N x N` chessboard so that no queens threaten each other. This requires that no two queens share the same row, column or diagonal. For an 8x8 chessboard there are 92 solutions - one possible solution is pictured below. ```{r echo=FALSE} queens8 <- data.frame( row = 1:8, col = c(6, 4, 7, 1, 8, 2, 5, 3), nm = 'Q' ) board(8) + geom_text(data = queens8, aes(col, row, label = nm)) + labs(title = "A solution to the 8-queens problem") + theme(plot.title = element_text(hjust = 0.5, vjust = 0)) ``` In this vignette, the N-queens problem (with N = 4) will be formulated and solved using SAT techniques. ## The 4-queens puzzle This vignette will formulate and then solve the 4-queens puzzle i.e. placing 4 queens on a 4x4 chessboard so that no queens threaten each other. First take an empty 4x4 chessboard ```{r echo=FALSE} board(4) ``` A Boolean variable (`q11` - `q44`) is assigned to each square. When a variable is `true` it means that there is a queen at this location, and `false` means there is no queen at this location. ```{r echo=FALSE} N <- 4 queens <- expand.grid(row = seq(N), col = seq(N)) queens$nm <- with(queens, paste0("q", row, col)) board() + geom_text(data = queens, aes(col, row, label = nm)) ``` If 4 queens are placed at the following locations, then this is definitely **not** a solution as the lower two queens can attach each other. ```{r echo=FALSE} queens_bad <- queens[queens$nm %in% c('q11', 'q24', 'q32', 'q43'),] board() + annotate( 'segment', y = 3, x = 2, yend = 4, xend = 3, color = 'red', linetype = 2 ) + geom_label(data = queens_bad, aes(col, row, label = nm)) ``` ## What constraints exist? To place 4 queens such that no queens can attack requires: * Each row can only contain 1 queen * Each column can only contain 1 queen * Each diagonal can only contain 1 queen (must be true for diagonals in both directions) ## Defining the problem ```{r} sat <- sat_new() #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Impose a constraint on the cardinality of each row # i.e. Exactly 1 member of each row must be true #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sat_card_exactly_one(sat, c("q11", "q12", "q13", "q14")) sat_card_exactly_one(sat, c("q21", "q22", "q23", "q24")) sat_card_exactly_one(sat, c("q31", "q32", "q33", "q34")) sat_card_exactly_one(sat, c("q41", "q42", "q43", "q44")) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Similarly, there can be only 1 queen in each column #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sat_card_exactly_one(sat, c("q11", "q21", "q31", "q41")) sat_card_exactly_one(sat, c("q12", "q22", "q32", "q42")) sat_card_exactly_one(sat, c("q13", "q23", "q33", "q43")) sat_card_exactly_one(sat, c("q14", "q24", "q34", "q44")) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Similarly, there can be only 1 queen in each diagonal #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sat_card_atmost_one(sat, c('q21', 'q12')) sat_card_atmost_one(sat, c('q31', 'q22', 'q13')) sat_card_atmost_one(sat, c('q41', 'q32', 'q23', 'q14')) sat_card_atmost_one(sat, c('q42', 'q33', 'q24')) sat_card_atmost_one(sat, c('q43', 'q34')) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Repeat for diagonals in the other direction #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sat_card_atmost_one(sat, c('q13', 'q24')) sat_card_atmost_one(sat, c('q12', 'q23', 'q34')) sat_card_atmost_one(sat, c('q11', 'q22', 'q33', 'q44')) sat_card_atmost_one(sat, c('q21', 'q32', 'q43')) sat_card_atmost_one(sat, c('q31', 'q42')) sat ``` ## Solve There are only 16 variables in this problem which means an exhaustive search only requires checking 2^16 combinations. This is within the realm of the naive solver which pre-allocates all combinations and evaluates them in parallel using standard R evaluation. ```{r} #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Solve using the built-in, naive, brute-force SAT solver # There are 2 solutions! #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ solns <- sat_solve_naive(sat) nrow(solns) ``` ```{r} #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Solution: # - columns = variable names # - row = a solution i.e. the boolean value of each variable in order to # satisfy the problem #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ solns ``` ```{r} #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Map these booleans to get the names of the variables which are TRUE # i.e. the location of the queens #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ solns <- apply(solns, 1, \(x) names(solns)[which(x)], simplify = FALSE) solns ``` ## Plot solution #1 ```{r} solns[[1]] ``` ```{r echo = FALSE} library(ggplot2) # a data.frame of the location on the board of the Queens for solution 1 sol_df <- queens[queens$nm %in% solns[[1]], ] board() + geom_text(data = sol_df, aes(col, row), label = 'Q') + labs( title = "N-Queens problem", subtitle = "N = 4. Solution #1" ) ``` ### Plot solution #2 ```{r} solns[[2]] ``` ```{r echo = FALSE} # a data.frame of the location on the board of the Queens for solution 1 sol_df <- queens[queens$nm %in% solns[[2]], ] board() + geom_text(data = sol_df, aes(col, row), label = 'Q') + labs( title = "N-Queens problem", subtitle = "N = 4. Solution #2" ) ```