7

I am interested in determining all of the possible "continuous" paths of an NxN matrix in R and returning their result. By "continuous" I mean that we can travel without lifting up your pencil/digit. That is, we can move up, down, left, right, or diagonally.

To make this concrete, let's use a 3x3 matrix example:

mat_3x3 <- matrix(LETTERS[1:9], ncol = 3, byrow = TRUE)
mat_3x3
#      [,1] [,2] [,3]
# [1,] "A"  "B"  "C" 
# [2,] "D"  "E"  "F" 
# [3,] "G"  "H"  "I" 

This means we have the following valid and invalid paths:

valid and invalid paths

Some considerations:

  • The start position does not need to be position A (1, 1).
  • We cannot "double-back" or touch the same cell multiple times.
  • Short paths are possible (e.g. A -> B -> C is a valid path; likewise, A -> E -> I) -- that is, we do not need to pass through all the nodes.

If there is a package or concept that facilitates, please advise (most of the graph traversal packages I have seen are more "graph" than "matrix"). I'd imagine dynamic programming or recursion is probably of use here, but I am not sure how to start.


I believe the answer to the 2X2 case could be 60 per the following solution for one cell with paths = 15; 15 * 4 = 60:

2x2 case for one cell

However, things escalate quickly for a 3x3, 4x4, case...no longer just corners, the addition of "center" squares, etc...


If we think about this problem as more of a graph or network, we have the following for the 3X3 case:

network or graph visualization

Why though? I am just genuinely interested in this problem and find it fascinating. I'd like to understand how to program it in R, but I'd consider other answers if they exist (then perhaps translate them to R). It started as a thought experiment thinking about a "game" where you you slide your finger on a touch screen to create words from the string of characters. Rather than minimum cost, we'd like to maximize score -- using a Z scores more points than an E like in Scrabble, etc. But I suppose this has interesting applications in social networks, graph theory, transportation optimization, and other domains.

JasonAizkalns
  • 20,243
  • 8
  • 57
  • 116
  • 1
    You can build a graph that looks like a matrix. Here is one [example](https://stackoverflow.com/questions/59524874/python-iterate-through-connected-components-in-grayscale-image/59561214#59561214) in Python. – Mykola Zotko Jan 13 '20 at 19:43
  • 2
    I would start by trying to figure out an appropriate algorithm, *then* worrying about how to implement it in R. Googling "find all paths grid" seems like a reasonable start, although most of them are designed for finding paths between two particular nodes, rather than finding all paths that cover every node in the grid ... – Ben Bolker Jan 13 '20 at 19:57
  • 1
    I agree with Ben, find the proper methodology first is important. Given the assumption that each node is a unique id, though, leads me to think that `combinat::permn(m)` will give you the order of nodes for each possible traversal of nodes. 9 nodes produces 362,880 (`factorial(9)`) permutations, though, so now you have the problem of what to do with those. – r2evans Jan 13 '20 at 20:23
  • 1
    there is a solution here although it is not in R https://stackoverflow.com/questions/13410168/how-many-ways-to-visit-all-the-points-of-a-given-matrix – GordonShumway Jan 13 '20 at 20:54
  • 1
    You could make a list of all illegal moves (e.g., the path can't move from A to C, F, G, H, or I) and search the list of permutations for these illegal moves. Any paths that remain satisfy your requirements. This is a slow method but would work. – Noah Jan 13 '20 at 20:54
  • 2
    Oh, I thought the path had to pass through all of the nodes ... What's your use case? How big a grid are you actually going to try this with ... ? – Ben Bolker Jan 13 '20 at 21:01
  • @BenBolker, so did I, that puts it well over 600,000. \*shrug\* – r2evans Jan 13 '20 at 21:07
  • 1
    @BenBolker, 5x5 is probably a safe stop. I just find this problem fascinating. Stems from thinking about a "game" where you slide your finger on a touch screen to create words from the string of characters. Rather than minimum cost, we'd like to maximize score -- using a `Z` scores more than an `E`, etc. – JasonAizkalns Jan 13 '20 at 21:09
  • Is it not like finding all the path without cycle between all the possible pairs of nodes in the graph you defined? – glagla Jan 16 '20 at 14:10
  • @glagla Almost. Not just pairs. But without cycles is correct. I feel like it can be phrased as an undirected graph problem or find all subgraphs, etc. just not up to date on my verbiage. `A-B-D` is legit, `E-H-I-F` is legit, ... – JasonAizkalns Jan 16 '20 at 21:38
  • by pairs I meant all possible combinations of two nodes (I want to know all path between A and D, between E and F, etc) – glagla Jan 17 '20 at 11:08

1 Answers1

1

This will work any size matrix (limited by hardware) and does not require the matrix to be rectangular e.g. 3 x 4. It builds a validity matrix that has all of the original matrix positions as columns and the row will return TRUE if it's a valid move and FALSE if not. I didn't validate all results, but the spot checks I did worked.

library(gtools)

# convert matrix to numbers to reference by position
m <- matrix(seq_along(mat_3x3), ncol = ncol(mat_3x3))

# create blank matrix that is used to see if it is a valid move
mLength <- length(m)
mValid <- matrix(rep(FALSE, mLength ^ 2), ncol = mLength)

# create index to generate validity matrix
xIndex <- seq_len(ncol(m))
yIndex <- seq_len(nrow(m))

# wrap with NA to prevent out of bounds
mBounds <- rbind(NA, cbind(NA, m, NA), NA)

# set validity matrix TRUE if returns a value that is not NA
mValid[cbind(as.vector(mBounds[yIndex + 1, xIndex + 2]), seq_len(mLength))] <- TRUE
mValid[cbind(as.vector(mBounds[yIndex + 2, xIndex + 2]), seq_len(mLength))] <- TRUE
mValid[cbind(as.vector(mBounds[yIndex + 2, xIndex + 1]), seq_len(mLength))] <- TRUE
mValid[cbind(as.vector(mBounds[yIndex + 2, xIndex    ]), seq_len(mLength))] <- TRUE
mValid[cbind(as.vector(mBounds[yIndex + 1, xIndex    ]), seq_len(mLength))] <- TRUE
mValid[cbind(as.vector(mBounds[yIndex    , xIndex    ]), seq_len(mLength))] <- TRUE
mValid[cbind(as.vector(mBounds[yIndex    , xIndex + 1]), seq_len(mLength))] <- TRUE
mValid[cbind(as.vector(mBounds[yIndex    , xIndex + 2]), seq_len(mLength))] <- TRUE

# define function to check if provided sequence is valid
validate <- function(x) {
  all(mValid[cbind(x[-1], x[-length(x)])])
}

# generate all permutations
p1 <- permutations(mLength, mLength)
p2 <- apply(p1, 1, validate)
p2 <- p1[p2, ]

# some results
> mat_3x3[p2[1, ]]
[1] "A" "D" "G" "E" "B" "C" "F" "H" "I"

> mat_3x3[p2[531, ]]
[1] "C" "E" "H" "G" "D" "A" "B" "F" "I"

To generate other sequences that do not use all letters would require changing the permutations function above to limit the target vector length:

p1 <- permutations(mLength, mLength - 1)
p2 <- apply(p1, 1, validate)
p2 <- p1[p2, ]

> mat_3x3[p2[1701, ]]
[1] "C" "F" "B" "D" "G" "E" "I" "H"

Using combinat::permn to use the validate function while building the permutations.

library(combinat)
p <- list()
pTemp <- permn(mLength, function(x) x[validate(x)])
p[[mLength]] <- pTemp[lengths(pTemp) > 0]

# breaking all paths that use every option into smaller pieces to find shorter paths
for (i in seq_len(mLength)[-mLength]) {
  pTemp <- lapply(p[[mLength]], function(x, y) embed(rev(x), length(x) - y), y = i)
  p[[mLength - i]] <- unique(do.call(rbind, pTemp))
}

# total number of paths
sum(unlist(lapply(p, nrow)), length(p[[mLength]]))
manotheshark
  • 4,297
  • 17
  • 30
  • I like this. I wonder if it'd be possible to leverage `combinat::permn` instead of `gtools::permutations` to do the `validate` function "on-the-fly" – JasonAizkalns Jan 16 '20 at 02:21
  • @JasonAizkalns updated to use `combinat::permn` and all shorter paths. However, a better method of building the permutations might be required as increasing the matrix size quickly hits memory limits. – manotheshark Jan 16 '20 at 21:30
  • Thank you. I posted [this related question](https://stackoverflow.com/q/59762657/2572423) which may give some clues/ideas. I haven't had time to dig too deeply yet. – JasonAizkalns Jan 16 '20 at 21:41