Thomas
Thomas

Reputation: 44525

Build a square-ish matrix with a specified number of cells

I would like to write a function that transforms an integer, n, (specifying the number of cells in a matrix) into a square-ish matrix that contain the sequence 1:n. The goal is to make the matrix as "square" as possible.

This involves a couple of considerations:

  1. How to maximize "square"-ness? I was thinking of a penalty equal to the difference in the dimensions of the matrix, e.g. penalty <- abs(dim(mat)[1]-dim(mat)[2]), such that penalty==0 when the matrix is square and is positive otherwise. Ideally this would then, e.g., for n==12 lead to a preference for a 3x4 rather than 2x6 matrix. But I'm not sure the best way to do this.

  2. Account for odd-numbered values of n. Odd-numbered values of n do not necessarily produce an obvious choice of matrix (unless they have an integer square root, like n==9. I thought about simply adding 1 to n, and then handling as an even number and allowing for one blank cell, but I'm not sure if this is the best approach. I imagine it might be possible to obtain a more square matrix (by the definition in 1) by adding more than 1 to n.

  3. Allow the function to trade-off squareness (as described in #1) and the number of blank cells (as described in #2), so the function should have some kind of parameter(s) to address this trade-off. For example, for n==11, a 3x4 matrix is pretty square but not as square as a 4x4, but the 4x4 would have many more blank cells than the 3x4.

  4. The function needs to optionally produce wider or taller matrices, so that n==12 can produce either a 3x4 or a 4x3 matrix. But this would be easy to handle with a t() of the resulting matrix.


Here's some intended output:

> makemat(2)
     [,1]
[1,]    1
[2,]    2

> makemat(3)
     [,1] [,2]
[1,]    1    3
[2,]    2    4

> makemat(9)
     [,1] [,2] [,3]
[1,]    1    4    7
[2,]    2    5    8
[3,]    3    6    9

> makemat(11)
     [,1] [,2] [,3] [,4]
[1,]    1    4    7    10
[2,]    2    5    8    11
[3,]    3    6    9    12

Here's basically a really terrible start to this problem.

makemat <- function(n) {
    n <- abs(as.integer(n))
    d <- seq_len(n)
    out <- d[n %% d == 0]
    if(length(out)<2)
        stop('n has fewer than two factors')
    dim1a <- out[length(out)-1]
    m <- matrix(1:n, ncol=dim1a)
    m
}

As you'll see I haven't really been able to account for odd-numbered values of n (look at the output of makemat(7) or makemat(11) as described in #2, or enforce the "squareness" rule described in #1, or the trade-off between them as described in #3.

Upvotes: 4

Views: 308

Answers (3)

nograpes
nograpes

Reputation: 18323

I think this function implicitly satisfies your constraints. The parameter can range from 0 to Inf. The function always returns either a square matrix with sides of ceiling(sqrt(n)), or a (maybe) rectangular matrix with rows floor(sqrt(n)) and just enough columns to "fill it out". The parameter trades off the selection between the two: if it is less than 1, then the second, more rectangular matrices are preferred, and if greater than 1, the first, always square matrices are preferred. A param of 1 weights them equally.

makemat<-function(n,param=1,wide=TRUE){
  if (n<1) stop('n must be positive')
  s<-sqrt(n)
  bottom<-n-(floor(s)^2)
  top<-(ceiling(s)^2)-n
  if((bottom*param)<top) {
      rows<-floor(s)
      cols<-rows + ceiling(bottom / rows) 
  } else {
    cols<-rows<-ceiling(s)
  }
  if(!wide) {
    hold<-rows
    rows<-cols
    cols<-hold
  }
  m<-seq.int(rows*cols)
  dim(m)<-c(rows,cols)
  m
}

Here is an example where the parameter is set to default, and equally trades off the distance equally:

lapply(c(2,3,9,11),makemat)

# [[1]]
#      [,1] [,2]
# [1,]    1    2
# 
# [[2]]
#      [,1] [,2]
# [1,]    1    3
# [2,]    2    4
# 
# [[3]]
#      [,1] [,2] [,3]
# [1,]    1    4    7
# [2,]    2    5    8
# [3,]    3    6    9
# 
# [[4]]
#      [,1] [,2] [,3] [,4]
# [1,]    1    4    7   10
# [2,]    2    5    8   11
# [3,]    3    6    9   12

Here is an example of using the param with 11, to get a 4x4 matrix.

makemat(11,3)
#      [,1] [,2] [,3] [,4]
# [1,]    1    5    9   13
# [2,]    2    6   10   14
# [3,]    3    7   11   15
# [4,]    4    8   12   16   

Upvotes: 6

Gavin Simpson
Gavin Simpson

Reputation: 174788

I think the logic you want is already in the utility function n2mfrow(), which as its name suggests is for creating input to the mfrow graphical parameter and takes an integer input and returns the number of panels in rows and columns to split the display into:

> n2mfrow(11)
[1] 4 3

It favours tall layouts over wide ones, but that is easily fixed via rev() on the output or t() on a matrix produced from the results of n2mfrow().

makemat <- function(n, wide = FALSE) {
  if(isTRUE(all.equal(n, 3))) {
    dims <- c(2,2)
  } else {
    dims <- n2mfrow(n)
  }
  if(wide)
    dims <- rev(dims)
  m <- matrix(seq_len(prod(dims)), nrow = dims[1], ncol = dims[2])
  m
}

Notice I have to special-case n = 3 as we are abusing a function intended for another use and a 3x1 layout on a plot makes more sense than a 2x2 with an empty space.

In use we have:

> makemat(2)
     [,1]
[1,]    1
[2,]    2
> makemat(3)
     [,1] [,2]
[1,]    1    3
[2,]    2    4
> makemat(9)
     [,1] [,2] [,3]
[1,]    1    4    7
[2,]    2    5    8
[3,]    3    6    9
> makemat(11)
     [,1] [,2] [,3]
[1,]    1    5    9
[2,]    2    6   10
[3,]    3    7   11
[4,]    4    8   12
> makemat(11, wide = TRUE)
     [,1] [,2] [,3] [,4]
[1,]    1    4    7   10
[2,]    2    5    8   11
[3,]    3    6    9   12

Edit:

The original function padded seq_len(n) with NA, but I realised the OP wanted to have a sequence from 1 to prod(nrows, ncols), which is what the version above does. The one below pads with NA.

makemat <- function(n, wide = FALSE) {
  if(isTRUE(all.equal(n, 3))) {
    dims <- c(2,2)
  } else {
    dims <- n2mfrow(n)
  }
  if(wide)
    dims <- rev(dims)
  s <- rep(NA, prod(dims))
  ind <- seq_len(n)
  s[ind] <- ind
  m <- matrix(s, nrow = dims[1], ncol = dims[2])
  m
}

Upvotes: 12

Ari B. Friedman
Ari B. Friedman

Reputation: 72731

What about something fairly simple and you can handle the exceptions and other requests in a wrapper?

library(taRifx)
neven <- 8
nodd <- 11
nsquareodd <- 9
nsquareeven <- 16

makemat <- function(n) {
  s <- seq(n)
  if( odd(n) ) {
    s[ length(s)+1 ] <- NA
    n <- n+1
  }
  sq <- sqrt( n )
  dimx <- ceiling( sq )
  dimy <- floor( sq )
  if( dimx*dimy < length(s) )  dimy <- ceiling( sq )
  l <- dimx*dimy
  ldiff <- l - length(s)
  stopifnot( ldiff >= 0 )
  if( ldiff > 0 )  s[ seq( length(s) + 1, length(s) + ldiff ) ] <- NA
  matrix( s, nrow = dimx, ncol = dimy )
}

> makemat(neven)
     [,1] [,2] [,3]
[1,]    1    4    7
[2,]    2    5    8
[3,]    3    6   NA
> makemat(nodd)
     [,1] [,2] [,3]
[1,]    1    5    9
[2,]    2    6   10
[3,]    3    7   11
[4,]    4    8   NA
> makemat(nsquareodd)
     [,1] [,2] [,3]
[1,]    1    5    9
[2,]    2    6   NA
[3,]    3    7   NA
[4,]    4    8   NA
> makemat(nsquareeven)
     [,1] [,2] [,3] [,4]
[1,]    1    5    9   13
[2,]    2    6   10   14
[3,]    3    7   11   15
[4,]    4    8   12   16

Upvotes: 4

Related Questions