kotlinmatrixmultidimensional-arraywindowed

How to collect m size 2D arrays/lists (sub matrices) of n 2D array/list (matrix) where m < n in Kotlin functionally?


I want to find overlapping 2D list/arrays (sub matrices) of a 2D list/array (matrix) looking like:

[
  [ 1, 2, 3, 4 ],
  [ 5, 6, 7, 8 ],
  [ 9, 1, 2, 3 ]
]

In simple case I need to get all overlapping squares of size m, that is m x m size overlapping 2D list/arrays. For example, in the case above let m be 3.

In imperative way I do the following:

for (i in 0 until matrix.size - m) {
  for (j in 0 until matrix.size - m) {
    var q = mutableListOf<Int>()       // can be a real 2D list, here a plain list to simplify
    for (k in i..i + m) {
      for (l in j..j + m) {
        q.add(matrix[m][n])
      }
    }
    // add an m size matrix to result here
  }
}

I have tried some stuff with windowed(m) and mapping ranges using m as upper bound inside but can't come up with a working solution.

Please share if you know how to make it in Kotlin in functional way. One-liner is not a must yet would be great.


Solution

  • You can use this:

    fun Iterable<Iterable<Int>>.subMatrices(size: Int): List<List<List<Int>>> =
        windowed(size) { window ->
            window.map {
                it.windowed(size)
            }.fold(emptyList<List<List<Int>>>()) { acc, it ->
                if (acc.isEmpty())
                    it.map(::listOf)
                else
                    acc.zip(it) { a, b -> a.plus(element = b) }
            }
        }.flatten()
    

    You only specify n and m in the example, where n = 5 and m = 3. Your initial matrix is of size 3/5, but the 3 does seem to be only coincidently the same as m. To handle a matrix with more rows the first windowed is necessary to create m-sized chunks.

    The second windowed is probably what you had in mind, it slides over the columns. What you were missing is the subsequent fold. It allows you to properly merge the windowed rows.

    With this, calling

    listOf(
        listOf(1, 2, 3, 4),
        listOf(5, 6, 7, 8),
        listOf(9, 1, 2, 3),
    ).subMatrices(3)
    

    produces

    [
        [
            [1, 2, 3],
            [5, 6, 7],
            [9, 1, 2],
        ],
        [
            [2, 3, 4],
            [6, 7, 8],
            [1, 2, 3],
        ],
    ]
    

    Edit:

    Depending on how large your matrices are, performance may be an issue. In that case you could make the accumulator for the fold operation mutable, so adding to a list doesn't create new lists with the previous ones thrown away:

    fun Iterable<Collection<Int>>.subMatrices(size: Int): List<List<List<Int>>> =
        windowed(size) { window ->
            window.map {
                it.windowed(size)
            }.fold(List(maxOf { it.size }) { mutableListOf<List<Int>>() }) { acc, it ->
                acc.zip(it) { a, b -> a.apply { add(b) } }
            }
        }.flatten()