【问题标题】:How to find same-value rectangular areas of a given size in a matrix most efficiently?如何最有效地在矩阵中找到给定大小的相同值矩形区域?
【发布时间】:2011-06-07 02:26:48
【问题描述】:

我的问题很简单,但是我还没有找到有效的实现方式。

假设有这样一个矩阵A:

0 0 0 0 0 0 0
4 4 2 2 2 0 0
4 4 2 2 2 0 0
0 0 2 2 2 1 1
0 0 0 0 0 1 1

现在我想找到该矩阵中具有给定大小的矩形区域的所有起始位置。区域是 A 的子集,其中所有数字都相同。

假设宽度=2 和高度=3。有 3 个区域具有这种大小:

2 2   2 2   0 0
2 2   2 2   0 0
2 2   2 2   0 0

函数调用的结果将是这些区域的起始位置(x,y 从 0 开始)的列表。

List((2,1),(3,1),(5,0))

以下是我当前的实现。 “区域”在这里称为“表面”。

case class Dimension2D(width: Int, height: Int)
case class Position2D(x: Int, y: Int)

def findFlatSurfaces(matrix: Array[Array[Int]], surfaceSize: Dimension2D): List[Position2D] = {

    val matrixWidth = matrix.length
    val matrixHeight = matrix(0).length
    var resultPositions: List[Position2D] = Nil

    for (y <- 0 to matrixHeight - surfaceSize.height) {
        var x = 0
        while (x <= matrixWidth - surfaceSize.width) {
            val topLeft = matrix(x)(y)
            val topRight = matrix(x + surfaceSize.width - 1)(y)
            val bottomLeft = matrix(x)(y + surfaceSize.height - 1)
            val bottomRight = matrix(x + surfaceSize.width - 1)(y + surfaceSize.height - 1)
            // investigate further if corners are equal
            if (topLeft == bottomLeft && topLeft == topRight && topLeft == bottomRight) {
                breakable {
                    for (sx <- x until x + surfaceSize.width;
                         sy <- y until y + surfaceSize.height) {
                        if (matrix(sx)(sy) != topLeft) {
                            x = if (x == sx) sx + 1 else sx 
                            break
                        }
                    }
                    // found one!       
                    resultPositions ::= Position2D(x, y)
                    x += 1
                }
            } else if (topRight != bottomRight) {
                // can skip x a bit as there won't be a valid match in current row in this area
                x += surfaceSize.width 
            } else {
                x += 1
            }
        }   
    }
    return resultPositions
}

我已经尝试在其中包含一些优化,但我确信有更好的解决方案。是否存在我可以移植的 matlab 函数?我也想知道这个问题是否有它自己的名字,因为我不知道谷歌是为了什么。

感谢您的考虑!我很高兴看到您的建议或解决方案:)

编辑: 我的应用程序中的矩阵尺寸范围约为 300x300 到 3000x3000。此外,对于同一矩阵,该算法只会被调用一次。原因是矩阵之后总是会改变(大约 1-20%)。

结果

我实现了 Kevin、Nikita 和 Daniel 的算法,并在我的应用程序环境中对它们进行了基准测试,即这里没有孤立的综合基准测试,但特别注意以最高效的方式集成所有算法,这对于 Kevin 的方法尤其重要,因为它使用泛型(见下文)。

首先,原始结果,使用 Scala 2.8 和 jdk 1.6.0_23。作为解决特定应用问题的一部分,这些算法被执行了数百次。 “持续时间”表示应用程序算法完成所需的总时间(当然没有 jvm 启动等)。我的机器是 2.8GHz Core 2 Duo,有 2 个内核和 2gig 内存,-Xmx800M 被分配给 JVM。

重要提示:我认为我的基准设置对于像 Daniel 那样的并行算法并不公平。这是因为应用程序已经在计算多线程。所以这里的结果可能只显示了相当于单线程的速度。

矩阵大小 233x587:

                  duration | JVM memory | avg CPU utilization
original O(n^4) | 3000s      30M          100%  
original/-server| 840s       270M         100%
Nikita O(n^2)   | 5-6s       34M          70-80%
Nikita/-server  | 1-2s       300M         100%
Kevin/-server   | 7400s      800M         96-98%
Kevin/-server** | 4900s      800M         96-99%
Daniel/-server  | 240s       360M         96-99%

** 使用@specialized,通过避免类型擦除来生成generics faster

矩阵大小 2000x3000:

                  duration | JVM memory | avg CPU utilization
original O(n^4) | too long   100M         100%  
Nikita O(n^2)   | 150s       760M         70%
Nikita/-server  | 295s (!)   780M         100%
Kevin/-server   | too long, didn't try

首先,关于内存的一个小注释。 -server JVM 选项使用更多内存,具有更多优化和通常更快执行的优势。从第二张表中可以看出 Nikita 的算法在使用 -server 选项时速度较慢,这显然是由于达到了内存限制。我认为即使对于小矩阵,这也会减慢 Kevin 的算法,因为函数方法无论如何都会使用更多的内存。为了消除内存因素,我还用 50x50 矩阵尝试过一次,然后 Kevin 用了 5 秒,Nikita 用了 0 秒(嗯,几乎是 0)。因此,无论如何它仍然较慢,而不仅仅是因为内存。

从数字中可以看出,我显然会使用 Nikita 的算法,因为它非常快,而且在我的情况下这是绝对必要的。正如 Daniel 指出的那样,它也可以很容易地并行化。唯一的缺点是它不是真正的 scala 方式。

目前,Kevin 的算法总体上可能有点过于复杂,因此速度较慢,但​​我相信还有更多优化可能(请参阅他的回答中的最后一个 cmets)。

为了将 Nikita 的算法直接转换为函数式风格,Daniel 提出了一个已经相当快的解决方案,正如他所说,如果他可以使用 scanRight 甚至会更快(请参阅他回答中的最后一个 cmets)。

下一步是什么?

在技术方面:等待 Scala 2.9、ScalaCL,并进行综合基准测试以获得原始速度。

我的目标是拥有功能代码,但前提是它不会牺牲太多速度。

答案选择:

至于选择答案,我想将 Nikita 和 Daniel 的算法标记为答案,但我必须选择一个。我的问题的标题包括“最有效”,一个是命令式最快的,另一个是函数式的。尽管这个问题被标记为 Scala,但我选择了 Nikita 的命令式算法,因为 2s 与 240s 的差异仍然太大,我无法接受。我敢肯定差异仍然可以降低一点,有什么想法吗?

那么,非常非常感谢大家!虽然我不会使用函数式算法,但我对 Scala 有了很多新的见解,我想我慢慢地了解了所有函数式的疯狂及其潜力。 (当然,即使没有做太多函数式编程,Scala 也比 Java 更讨人喜欢……这也是学习它的另一个原因)

【问题讨论】:

  • 有几种算法可以找到区域(如在 Paint 程序中):Flood fillRegion extraction。但他们不强加一个矩形模式。凯文的回答看起来非常适合这个用例。
  • @neo,出于好奇,您需要处理多大的矩阵?
  • @Paul 在大约范围内。 300x300 到 3000x3000,这就是我真正追求 最有效算法的原因。我也对 ScalaCL 感兴趣,但不幸的是我的 gfx 卡太旧了……
  • 我删除了我的答案,因为我注意到算法存在严重缺陷。我会让凯文拿走这个。 :-)
  • @daniel 谢谢,我想... :)

标签: algorithm scala matrix scala-2.8


【解决方案1】:

您可以在O(n^2) 中相对容易地做到这一点。
首先,一些预先计算。对于矩阵中的每个单元格,计算它下面有多少个连续的单元格具有相同的数字。
对于您的示例,生成的矩阵 a(想不出更好的名称:/)将如下所示

0 0 0 0 0 2 2
1 1 2 2 2 1 1
0 0 1 1 1 0 0
1 1 0 0 0 1 1
0 0 0 0 0 0 0

可以在O(n^2)轻松制作。

现在,对于每一行 i,让我们找到所有矩形,其顶部位于 i 行中(底部位于 i + height - 1 行中)。
这是i = 1的插图

0 0 0 0 0 0 0
-------------
4 4 2 2 2 0 0
4 4 2 2 2 0 0
0 0 2 2 2 1 1
-------------
0 0 0 0 0 1 1

现在,主要思想

int current_width = 0;
for (int j = 0; j < matrix.width; ++j) {
    if (a[i][j] < height - 1) {
        // this column has different numbers in it, no game
        current_width = 0;
        continue;
    }

    if (current_width > 0) {
        // this column should consist of the same numbers as the one before
        if (matrix[i][j] != matrix[i][j - 1]) {
            current_width = 1; // start streak anew, from the current column
            continue;
        }
    }

    ++current_width;
    if (current_width >= width) {
        // we've found a rectangle!
    }
}

在上面的示例中 (i = 1) current_width 在每次迭代后将是 0, 0, 1, 2, 3, 0, 0

现在,我们需要遍历所有可能的i,我们有一个解决方案。

【讨论】:

  • 啊……Java!不洁,不洁!
  • “Big-O 本质上是对序列复杂性的度量” 不,不是。它是对所需操作次数的度量。在核心数量固定的情况下,O(n^4) 算法仍然是 O(n^4)。除非核心数大于或等于 n,否则不可变的声明性解决方案不能同时每一行,除非我错过了什么
  • @Kevin 我不是在谈论计数,而是在谈论预测未来。普通消费类笔记本电脑有 2-4 个内核(这个数字最近似乎不符合摩尔定律),10^6 操作和 10^12 操作之间的差异并不重要的时代不会很快到来。我不敢相信我什至不得不为此争论。 (更不用说,正如我上面提到的,在 Java 或 C 中绝对没有什么可以阻止并行化。)
  • @Nikita 当问题同时被标记为“scala”和“scala 2.8”时,你想相信它是在这里说的......我建议你回去阅读我的帖子,其中我 做描述它是如何工作的,如果你甚至不知道它是做什么的,你觉得如何将它诽谤为O(n^4)
  • @Nikita 大声笑....我刚刚做了一个更大的基准测试,当我在我的应用程序中使用你的基准测试(数百次)时,总共需要 6-7 秒,而我原来的解决方案需要 3065 秒: D 所以你的速度要快 400-500 倍……这真的可以吗?当然,这些测量绝不是科学的,但仍然给出了一个想法
【解决方案2】:

我将在这里扮演魔鬼的拥护者。我将展示Nikita's 以函数式风格编写的精确算法。我也会并行化它,只是为了表明它可以做到。

首先,计算单元格下方具有相同编号的连续单元格。与 Nikita 建议的输出相比,我通过返回所有值加一进行了轻微更改,以避免在代码的其他部分出现- 1

def computeHeights(column: Array[Int]) = (
    column
    .reverse
    .sliding(2)
    .map(pair => pair(0) == pair(1))
    .foldLeft(List(1)) ( 
        (list, flag) => (if (flag) list.head + 1 else 1) :: list
    )
)

我宁愿在反转之前使用.view,但这不适用于当前的 Scala 版本。如果这样做,它将节省重复的数组创建,这应该会大大加快代码的速度,如果没有其他原因的话,出于内存局部性和带宽的原因。

现在,所有列同时显示:

import scala.actors.Futures.future

def getGridHeights(grid: Array[Array[Int]]) = (
    grid
    .transpose
    .map(column => future(computeHeights(column)))
    .map(_())
    .toList
    .transpose
)

我不确定并行化开销是否会在这里得到回报,但这是 Stack Overflow 上第一个真正有机会的算法,因为在计算列方面付出了不小的努力。这是另一种编写方式,使用即将推出的 2.9 功能(它可能适用于 Scala 2.8.1——不确定是什么:

def getGridHeights(grid: Array[Array[Int]]) = (
    grid
    .transpose
    .toParSeq
    .map(computeHeights)
    .toList
    .transpose
)

现在,尼基塔算法的核心:

def computeWidths(height: Int, row: Array[Int], heightRow: List[Int]) = (
    row
    .sliding(2)
    .zip(heightRow.iterator)
    .toSeq
    .reverse
    .foldLeft(List(1)) { case (widths @ (currWidth :: _), (Array(prev, curr), currHeight)) =>
        (
            if (currHeight >= height && currWidth > 0 && prev == curr) currWidth + 1
            else 1
        ) :: widths
    }
    .toArray
)

我在这段代码中使用了模式匹配,尽管我担心它的速度,因为在所有的滑动、压缩和折叠中,这里有两件事要兼顾。而且,说到性能,我使用Array 而不是IndexedSeq,因为Array 是JVM 中唯一未被擦除的类型,因此Int 的性能要好得多。然后,还有.toSeq,由于内存局部性和带宽,我对此也不是特别满意。

另外,我是从右到左而不是 Nikita 的从左到右,这样我就可以找到左上角。

但是,与 Nikita 答案中的代码相同,除了与他的代码相比,我仍在当前宽度上加一,而不是在此处打印结果。不过,这里有很多没有明确来源的东西——rowheightRowheight... 让我们在上下文中查看这段代码——并并行化! -- 获取整体情况。

def getGridWidths(height: Int, grid: Array[Array[Int]]) = (
    grid
    .zip(getGridHeights(grid))
    .map { case (row, heightsRow) => future(computeWidths(height, row, heightsRow)) }
    .map(_())
)

还有2.9版本:

def getGridWidths(height: Int, grid: Array[Array[Int]]) = (
    grid
    .toParSeq
    .zip(getGridHeights(grid))
    .map { case (row, heightsRow) => computeWidths(height, row, heightsRow) }
)

而且,对于大结局,

def findRectangles(height: Int, width: Int, grid: Array[Array[Int]]) = {
    val gridWidths = getGridWidths(height, grid)
    for {
        y <- gridWidths.indices
        x <- gridWidths(y).indices
        if gridWidths(y)(x) >= width
    } yield (x, y)
}

所以...我毫不怀疑 Nikita 算法的命令式版本更快——它只使用Array,它使用原语比任何其他类型都快得多,并且它避免了大量创建临时集合——虽然 Scala 可以在这里做得更好。此外,没有闭包——尽管它们有很大帮助,但它们比没有闭包的代码慢。至少在 JVM 发展出一些可以帮助他们的东西之前。

此外,Nikita 的代码可以很容易地与线程并行化 - 最重要的是! -- 不费吹灰之力。

但我的意思是,Nikita 的代码并不是特别糟糕,因为它到处使用数组和可变变量。该算法干净利落地转换为更实用的风格。

编辑

因此,我决定尝试制作一个高效的功能版本。它的功能并不完全,因为我使用了Iterator,它是可变的,但它已经足够接近了。不幸的是,它不适用于 Scala 2.8.1,因为它在 Iterator 上缺少 scanLeft

这里还有另外两件不幸的事情。首先,我放弃了网格高度的并行化,因为我无法绕过至少一个transpose,以及所有需要复制的集合。不过,仍然至少有一份数据副本(请参阅toArray 电话以了解在哪里)。

另外,由于我正在使用Iterable,我失去了使用并行集合的能力。我想知道如果从一开始就让grid 成为并行集合的并行集合,代码是否会变得更好。

我不知道这是否比以前版本的 not 更有效。这是一个有趣的问题...

def getGridHeights(grid: Array[Array[Int]]) = (
    grid
    .sliding(2)
    .scanLeft(Array.fill(grid.head.size)(1)) { case (currHeightArray, Array(prevRow, nextRow)) =>
        (prevRow, nextRow, currHeightArray)
        .zipped
        .map { case (x, y, currHeight) =>  if (x == y) currHeight + 1 else 1 }
    }
)

def computeWidths(height: Int, row: Array[Int], heightRow: Array[Int]) = (
    row
    .sliding(2)
    .map { case Array(x, y) => x == y }
    .zip(heightRow.iterator)
    .scanLeft(1) { case (currWidth , (isConsecutive, currHeight)) =>
        if (currHeight >= height && currWidth > 0 && isConsecutive) currWidth + 1
        else 1
    }
    .toArray
)

import scala.actors.Futures.future

def getGridWidths(height: Int, grid: Array[Array[Int]]) = (
    grid
    .iterator
    .zip(getGridHeights(grid))
    .map { case (row, heightsRow) => future(computeWidths(height, row, heightsRow)) }
    .map(_())
    .toArray
)

def findRectangles(height: Int, width: Int, grid: Array[Array[Int]]) = {
    val gridWidths = getGridWidths(height, grid)
    for {
        y <- gridWidths.indices
        x <- gridWidths(y).indices
        if gridWidths(y)(x) >= width
    } yield (x - width + 1, y - height + 1)
}

【讨论】:

  • 谢谢!我一直都知道体面的算法可以在 scala 中实现 :) 有空我会多看一些,也许我会开始了解一些 scala。
  • @Daniel 哇,我没想到!看到你的所有想法,我发现我非常兴奋,现在即使使用演员框架,直到现在我只是在这里和那里读到它。
  • @Daniel 我只是尝试测试您的代码,但编译器不喜欢最后一次调用 getGridHeights 中的转置:“找不到参数 asArray 的隐式值:(List[Int]) => Array[U]”和“方法转置的参数不足:(implicit asArray: (List[Int]) => Array[U])Array[Array[U]]。未指定值参数 asArray。”
  • @neo 好的,我测试过了。我用这段代码玩了一会儿,反复研究要使用的集合。最初,我一直使用Array,假设会有很多索引访问。当我最终完成代码时,事实证明并非如此,所以我回去尝试减少集合转换。我显然忘了测试这段特殊的代码(我主要使用 2.9 版本),现在已修复。
  • @neo 顺便说一下,我在这里主要是为了功能风格的清晰代码,而不是速度。由于 Scala 库中的两个问题(一个可以说是一个错误),我有点受挫,这也会使代码更快。然而,没有什么比实现 Nikita 所示算法的速度 while 循环更好了。您可能会从并行化中获益,但在 Nikita 的代码中也可以做到这一点,只需很少的更改。
【解决方案3】:

首先,几个辅助函数:

//count the number of elements matching the head
def runLength[T](xs:List[T]) = xs.takeWhile(_ == xs.head).size

//pair each element with the number of subsequent occurrences
def runLengths[T](row:List[T]) : List[(T,Int)] = row match {
  case Nil => Nil
  case h :: t => (h, runLength(row)) :: runLengths(t)
}
//should be optimised for tail-call, but easier to understand this way

//sample input: 1,1,2,2,2,3,4,4,4,4,5,5,6
//output: (1,2), (1,1), (2,3), (2,2), (2,1), (3,1), (4,4), (4,3), (4,2), (4,1), (5,2), (5,1), (6,1)

这可以用于网格中的每一行:

val grid = List(
  List(0,0,0,0),
  List(0,1,1,0),
  List(0,1,1,0),
  List(0,0,0,0))

val stage1 = grid map runLengths
// returns stage1: List[List[(Int, Int)]] =
// 0,4  0,3  0,2  0,1
// 0,1  1,2  1,1  0,1
// 0,1  1,2  1,1  0,1
// 0,4  0,3  0,2  0,1

在完成了水平的行之后,我们现在对列执行完全相同的操作。这使用 Scala 标准集合库中可用的transpose 方法来交换行列,根据同名的数学矩阵运算。一旦完成,我们也会转回。

val stage2 = (stage1.transpose map runLengths).transpose
// returns stage2: List[List[((Int, Int), Int)]] =
// (0,4),1  (0,3),1  (0,2),1  (0,1),4
// (0,1),2  (1,2),2  (1,1),2  (0,1),3
// (0,1),1  (1,2),1  (1,1),1  (0,1),2
// (0,4),1  (0,3),1  (0,2),1  (0,1),1

这是什么意思?取一个元素:(1,2),2,表示该单元格包含值1,向右扫描发现包含1的行中有2个相邻单元格。向下扫描,有两个相邻的单元格具有相同的属性,包含值1,并且在其右侧具有相同数量的相等值。

整理一下就清楚了一点,将((a,b),c)形式的嵌套元组转换为(a,(b,c))

val stage3 = stage2 map { _.map {case ((a,b),c) => a->(b,c) } }
//returns stage3: List[List[(Int, (Int, Int))]] =
//  0,(4,1)  0,(3,1)  0,(2,1)  0,(1,4)
//  0,(1,2)  1,(2,2)  1,(1,2)  0,(1,3)
//  0,(1,1)  1,(2,1)  1,(1,1)  0,(1,2)
//  0,(4,1)  0,(3,1)  0,(2,1)  0,(1,1)

其中1,(2,2) 是指包含值1 的单元格,并且位于具有相似值的单元格的2x2 矩形的左上角。

从这里,发现相同大小的矩形是微不足道的。如果您还想排除属于较大矩形子集的区域,则需要做更多的工作。

更新:正如所写,该算法不适用于 (0,0) 处的单元格这样的情况,该单元格属于两个不同的矩形(1x4 和 4x1)。如果需要,这也可以使用相同的技术解决。 (使用 map/transpose/map/transpose 进行一次传递,然后使用 transpose/map/transpose/map 进行第二次传递,然后压缩并展平结果)

如果输入可能包含包含相同值的单元格的相邻矩形,则还需要修改,例如:

0 0 0 0 0 0 0 0
0 0 1 1 1 0 0 0
0 0 1 1 1 0 0 0
0 0 1 1 1 1 1 0
0 0 1 1 1 1 1 0
0 0 1 1 1 1 1 0
0 0 0 0 0 0 0 0

把它们放在一起,然后稍微清理一下:

type Grid[T] = List[List[T]]

def runLengths[T](row:List[T]) : List[(T,Int)] = row match {
  case Nil => Nil
  case h :: t => (h -> row.takeWhile(_ == h).size) :: runLengths(t)
}

def findRectangles[T](grid: Grid[T]) = {
  val step1 = (grid map runLengths)
  val step2 = (step1.transpose map runLengths).transpose
  step2 map { _ map { case ((a,b),c) => (a,(b,c)) } }
}

更新2

抓住你的帽子,这是一顶大帽子......

在编写一行新功能之前,我们将首先进行一些重构,将一些方法拉入具有隐式转换的 Ops 类中,这样它们就可以像定义在底层集合类型上的方法一样使用:

import annotation.tailrec

class RowOps[T](row: List[T]) {
  def withRunLengths[U](func: (T,Int)=>U) : List[U] = {
    @tailrec def recurse(row:List[T], acc:List[U]): List[U] = row match {
      case Nil => acc
      case head :: tail =>
        recurse(
          tail,
          func(head, row.takeWhile(head==).size) :: acc)
    }
    recurse(row, Nil).reverse
  }

  def mapRange(start: Int, len: Int)(func: T=>T) =
    row.splitAt(start) match {
      case (l,r) => l ::: r.take(len).map(func) ::: r.drop(len)
    }
}

implicit def rowToOps[T](row: List[T]) = new RowOps(row)

这会将withRunLengths 方法添加到列表中。这里一个显着的区别是,该方法不返回 (value, length) 对的列表,而是接受一个函数作为参数,为每个这样的对创建一个输出值。这个以后会派上用场的……

type Grid[T] = List[List[T]]

class GridOps[T](grid: Grid[T]) {
  def deepZip[U](other: Grid[U]) = (grid zip other) map { case (g,o) => g zip o}
  def deepMap[U](f: (T)=>U) = grid map { _ map f}
  def mapCols[U](f: List[T]=>List[U]) = (grid.transpose map f).transpose
  def height = grid.size
  def width = grid.head.size
  def coords = List.tabulate(height,width){ case (y,x) => (x,y) }
  def zipWithCoords = deepZip(coords)
  def deepMapRange(x: Int, y: Int, w: Int, h: Int)(func: T=>T) =
    grid mapRange (y,h){ _.mapRange(x,w)(func) }
}

implicit def gridToOps[T](grid: Grid[T]) = new GridOps(grid)

这里不应该有任何惊喜。 deepXXX 方法避免了编写list map { _ map { ... } } 形式的构造。 tabulate 对你来说可能也很陌生,但希望它的用途从使用上就很明显了。

使用这些,定义用于查找整个网格的水平和垂直运行长度的函数变得微不足道:

def withRowRunLengths[T,U](grid: Grid[T])(func: (T,Int)=>U) =
  grid map { _.withRunLengths(func) }

def withColRunLengths[T,U](grid: Grid[T])(func: (T,Int)=>U) =
  grid mapCols { _.withRunLengths(func) }

为什么是 2 个参数块而不是一个?我会尽快解释。

可以将这些定义为GridOps 中的方法,但它们似乎不适用于一般用途。随意在这里不同意我的意见:)

接下来,定义一些输入...

def parseIntGrid(rows: String*): Grid[Int] =
  rows.toList map { _ map {_.toString.toInt} toList }

val input: Grid[Int] = parseIntGrid("0000","0110","0110","0000")

...另一种有用的辅助类型...

case class Rect(w: Int, h: Int)
object Rect { def empty = Rect(0,0) }

作为元组的替代品,这确实有助于调试。深度嵌套的括号不容易在视觉上很容易。 (对不起 Lisp 粉丝!)

...并使用上面定义的函数:

val stage1w = withRowRunLengths(input) {
  case (cell,width) => (cell,width)
}

val stage2w = withColRunLengths(stage1w) {
  case ((cell,width),height) => Rect(width,height)
}


val stage1t = withColRunLengths(input) {
 case (cell,height) => (cell,height)
}

val stage2t = withRowRunLengths(stage1t) {
  case ((cell,height),width) => Rect(width,height)
}

以上所有块都应该是单行的,但我为 StackOverflow 重新格式化。

这个阶段的输出只是矩形的网格,我故意不提及矩形所包含的实际值。这绝对没问题,从网格中的坐标很容易找到,我们将在短时间内重新组合数据。

还记得我解释过RowOps#withRunLengths 将函数作为参数吗?嗯,这就是它被使用的地方。 case (cell,width) =&gt; (cell,width) 实际上是一个函数,它获取单元格值和运行长度(称为 cellwidth)然后返回 (cell,width) 元组。

这也是我在定义函数时使用两个参数块的原因,所以第二个参数可以在 { 大括号 } 中传递,并且使整个事情都很好并且类似于 DSL。

这里说明的另一个非常重要的原则是类型推断器连续作用于参数块,因此(还记得吗?):

def withRowRunLengths[T,U](grid: Grid[T])(func: (T,Int)=>U)

T 的类型将由提供的网格决定。然后编译器知道作为第二个参数提供的函数的输入类型, - Int 在这种情况下,因为第一个参数是 Grid[Int] - 这就是我能够编写的原因case (cell,width) =&gt; (cell,width) 并且不必在任何地方明确声明 cellwidth 是整数。在第二种用法中,提供了一个 Grid[(Int,Int)],这适合闭包 case ((cell,width),height) =&gt; Rect(width,height),而且它也能正常工作。

如果该闭包包含对网格的底层类型不起作用的任何内容,那么编译器会抱怨,这就是类型安全的全部意义......

计算完所有可能的矩形后,剩下的就是收集数据并以更便于分析的格式呈现。因为这个阶段的嵌套可能会变得非常混乱,我定义了另一个数据类型:

case class Cell[T](
  value: T,
  coords: (Int,Int) = (0,0),
  widest: Rect = Rect.empty,
  tallest: Rect = Rect.empty
)

这里没什么特别的,只是一个带有命名/默认参数的案例类。我也很高兴我有先见之明在上面定义Rect.empty :)

现在混合 4 个数据集(输入 vals、coords、最宽的 rects、最高的 rects),逐渐折叠到单元格中,轻轻搅拌,然后上桌:

val cellsWithCoords = input.zipWithCoords deepMap {
  case (v,(x,y)) => Cell(value=v, coords=(x,y))
}

val cellsWithWidest = cellsWithCoords deepZip stage2w deepMap {
  case (cell,rect) => cell.copy(widest=rect)
}

val cellsWithWidestAndTallest = cellsWithWidest deepZip stage2t deepMap {
  case (cell,rect) => cell.copy(tallest=rect)
}

val results = (cellsWithWidestAndTallest deepMap {
  case Cell(value, coords, widest, tallest) =>
    List((widest, value, coords), (tallest, value, coords))
  }
).flatten.flatten

最后一个阶段很有趣,它将每个单元格转换为一个大小为 2 的(矩形、值、坐标)元组列表(大小为 2,因为每个单元格都有最宽和最高的矩形)。调用 flatten 两次,然后将生成的 List[List[List[_]]] 降为单个 List。不再需要保留 2D 结构,因为必要的坐标已经嵌入到结果中。

瞧!

现在由您决定如何处理此列表。下一阶段可能是某种形式的排序和重复删除...

【讨论】:

  • Hmmm.... 不知道它是否只是 Eclipse 的 Scala 插件,但它不允许我编译它,因为这个错误:“方法 map2dRowFirst 的参数太多:(网格: HelpersTest.this.Grid[T])(func: (List[T]) => List[T])List[List[T]]" 我还没明白 map2dRowFirst 中的“2nd”参数列表是什么好因为,我的意思是一般来说,不仅仅是在你的代码中。你能解释一下吗?
  • @Kevin 您写道,如果输入包含相邻的矩形,您的代码将需要修改。因为这在我的情况下是绝对可能的,你能给我一个提示吗?
  • @neo 我的错误,我从 REPL 复制了不匹配的函数版本。现在应该都好了。
  • @neo 相邻的矩形大多是棘手的,因为它们可以以不同的方式被感知。例如,您会说我在回答中给出的样本代表 2 个重叠的矩形,还是 3 个不重叠的?
  • @Kevin 现在它抱怨“类型不匹配;发现:(List[Nothing]) => List[(Nothing, Int)] required: (List[Any]) => List[Any]”对于最后一行中的 runLengths 参数,这似乎是正确的,因为 List[(T,Int)] 不是 List[T]
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-20
  • 1970-01-01
  • 1970-01-01
  • 2020-07-23
  • 2021-09-05
  • 1970-01-01
相关资源
最近更新 更多