How Many Ways to Color This Grid: Teaching Combinatorics Using R

[This article was first published on R Works, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

The Problem

You have a 2×2 grid of squares, and you need to paint each square one of four colors: red, blue, green, or yellow. The restriction is that no two adjacent squares (sharing a side) can have the same color. How many valid ways you can color the grid?

This problem is a simplified illustration of the four color theorem, which says that four colors are sufficient to color any planar map such that no two adjacent regions share the same color. Additionally, it can be viewed as a specific case of a more generalized problem: coloring a x grid with colors while adhering to adjacency restrictions. From a graph theory perspective, we can represent this grid as a graph where each square corresponds to a vertex, and edges connect vertices that are adjacent. The challenge of coloring the grid without violating the adjacency rule translates to finding a proper vertex coloring of this graph.

The Solution

There are many ways to solve this. In each solution, we count the number of combinations that are “correct” or valid, i.e., adhere to the rules given above in the problem. For the solution in R, we will generate all possible combinations of coloring this grid with no restrictions, and using that, directly count cases that follow the adjacency rules specified in the problem.

I will use dplyr to illustrate how easy and straightforward it will be to get to the solution.

The simplest way to calculate ways to color this grid with no restrictions is to consider that for each of the squares (), there are possible options (, see square above), so that the total number of options we have is essentially —

We now use R to generate these cases or options.

library(gtools)
x <- c('r', 'b', 'g', 'y')

P <- permutations(
  n = 4,
  r = 4,
  v = x,
  repeats.allowed = T
)

colnames(P) <- c("A", "B", "C", "D")
P <- data.frame(P)
nrow(P)
[1] 256
head(P)
  A B C D
1 b b b b
2 b b b g
3 b b b r
4 b b b y
5 b b g b
6 b b g g

How do we now determine which cases do not follow the adjacency rules? To do this, we’ll need to use a pattern-searching logic to flag them. One way is to calculate, for all cases, different columns, corresponding to the adjacency restrictions (e.g. red-red, blue-blue, green-green and yellow-yellow). We would then use these columns to compute a single new column flagging cases where adjacency restrictions are followed, i.e. flag valid cases, and sum them up.

However, in R, using dplyr, we combine this in a single command using case when, which will flag each case whenever it meets the invalidity condition even once, e.g., for the coloring scheme “blue-blue-blue-red”, it will flag it “invalid” since just the existence of “blue-blue” is sufficient to deem this scheme invalid.

library(dplyr)
library(magrittr)

case_split <- P %>%
  mutate(concat = paste(P$A, P$B, P$C, P$D, P$A, sep = "")) %>%
  mutate(
    flag = case_when(
      grepl("rr", concat) ~ "invalid",
      grepl("bb", concat) ~ "invalid",
      grepl("gg", concat) ~ "invalid",
      grepl("yy", concat) ~ "invalid",
      .default = "valid"
    )
  ) %>%
  group_by(flag) %>% summarise(count = n())

knitr::kable(case_split, align = rep('c', 2))
flag count
invalid 172
valid 84

There are valid cases, which can, of course, be computed arithmetically using combinatorics formulas or simply going case by case and calculating how many valid cases emerge, which will also give us valid cases of the total .

Of these remaining invalid cases, I then thought about how many are so due to not heeding the adjacency rules just once, how many twice or more? For example, “bbbg” does not heed it twice, “bbrg” just once. To do this, for each of the cases, instead of just the existence of invalidity, we flag each occurrence of adjacency or invalidity and then sum them up.

flag_invalid_adj <- function(df) {
  df %>%
    rowwise() %>%
    mutate(
      # Check each adjacency pair for invalidity
      ab_invalid = A == B,
      bc_invalid = B == C,
      cd_invalid = C == D,
      ad_invalid = A == D,
      # Count the number of invalid pairs
      invalid = sum(ab_invalid, bc_invalid, cd_invalid, ad_invalid),
    ) %>%
    ungroup()
}

cols <- P[, 1:4]
invalid_data <- flag_invalid_adj(cols) %>% select(A, B, C, D, invalid)
finalCounts <- invalid_data %>% group_by(invalid) %>% summarise(count =
                                                                  n())
knitr::kable(finalCounts, align = rep('c', 2))
invalid count
0 84
1 96
2 72
4 4

The first row confirms the count of valid cases, i.e., have no adjacency problem. Finally, because we can, and we love ggplot2, let’s generate 1 sample grid from each category of invalidity, so that we can visually inspect what we’ve been saying. The “4 invalid cases” has 4 pairs of blue, or 4 adjacencies, while the “No invalid cases!” plot is an example of how we can indeed color a graph, map, or a grid with 4 colors with no adjacent squares or “regions”, having the same color. Some notes on the code used – we plot the 4 examples using gridExtra. We’ll also use textGrob to attribute an overall title for the family of plots, using an elegant font from the Google library (“rouge script”). Click “Show the code” to see the details.

Show the code
# Select first row of all groups
s <- invalid_data %>% group_by(invalid) %>% filter(row_number() == 1)

# Load necessary libraries for elegant visualizations
library(ggplot2)
library(grid)       # To ensure "textGrob" works
library(gridExtra)
library(showtext)   # For using custom fonts

# Load a font using showtext
font_add_google("Rouge Script", "rouge script")  # Example: Adding the "Lobster" font
showtext_auto()  # Automatically use showtext for all plots

# Create my color tibble (4x4)
sq <- s %>%
  mutate_all( ~ case_when(
    . == "b" ~ "blue",
    . == "g" ~ "green",
    . == "y" ~ "yellow",
    . == "r" ~ "red",
    TRUE ~ .  # Keep original value if it doesn't match
  ))

# Convert tibble to a matrix
color_matrix <- sq[, -5] %>% select(A, B, D, C) %>% as.matrix() # to match the coloring order of ggplot2

# Check the dimensions of the matrix
# Print(dim(color_matrix))  # Should be 4x4

# Function to create a plot based on a color vector and a title
create_plot <- function(colors, title) {
  df <- expand.grid(x = 0:1, y = 0:1)
  df$color <- colors
  
  ggplot(df, aes(x = x, y = y, fill = color)) +
    geom_tile(color = "white") +  # Change border color to white
    scale_fill_identity() +        # Use the colors as they are
    theme_minimal() +              # Use a minimal theme
    coord_fixed() +                # Keep aspect ratio
    labs(title = title) +          # Add the title
    theme(
      legend.position = "none",
      # Remove legend
      axis.title = element_blank(),
      # Suppress axis titles
      axis.text = element_blank(),
      # Suppress axis text
      axis.ticks = element_blank(),
      # Suppress axis ticks
      panel.grid = element_blank(),
      # Suppress grid lines
      plot.title = element_text(
        family = "rouge script",
        size = 16,
        hjust = 0.5
      )  # Use the custom font for the title
    )
}

# Create a vector of titles for each plot
titles <- c("4 invalid cases",
            "2 invalid cases",
            "1 invalid case",
            "No invalid cases!")

# Create a list of plots using the color matrix and titles
plots <- lapply(1:nrow(color_matrix), function(i) {
  create_plot(color_matrix[i, ], titles[i])
})

# Create the overall title using textGrob
overall_title <- textGrob(
  "Using R to elegantly illustrate the solution to a Combinatorics puzzle",
  gp = gpar(fontsize = 20, fontfamily = "rouge script")
)

# Create a spacer with appropriate height
spacer <- rectGrob(
  gp = gpar(fill = NA, col = NA),
  width = unit(1, "npc"),
  height = unit(0.5, "lines")
)

# Arrange the overall title and plots
grid.arrange(
  overall_title,
  spacer,
  arrangeGrob(grobs = plots, ncol = 2),
  ncol = 1,
  heights = c(1, 0.1, 4)
)

Four 2x2 grids with different colorings. The top left grid has four squares, all blue. The top right grid has four squares, the top two are yellow, and the bottom two are blue. The bottom left grid has four squares, with alternating colors going from left to right and top to bottom: red, blue, blue, red. The bottom right grid has alternating colors going from left to right and top to bottom: red, blue, yellow, green.

About the Author

Vidisha writes: “As a statistician and a data geek, I love solving puzzles of all kinds, with a particular fondness for combinatorial challenges. Recently, I tackled one in R using the dplyr package, which prompted me to think about the broader applications of R, not just as a powerful tool for solving puzzles for enthusiasts but also as an empowering way to teach high school mathematics. Moreover, R’s capabilities in data visualization can help learners grasp complex problems more intuitively, ultimately leading to a deeper understanding of their solutions.”

To leave a comment for the author, please follow the link and comment on their blog: R Works.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)