如何最有效地在矩阵中找到给定大小的相同值矩形区域?

2023-12-28

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

假设有一个这样的矩阵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 函数?我还想知道这个问题是否有自己的名字,因为我不知道该用谷歌搜索什么。

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

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

RESULTS

我实现了 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,使仿制药更快 http://lamp.epfl.ch/~dragos/files/scala-spec.pdf通过避免类型擦除

矩阵尺寸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 选项使用更多的内存,具有更多优化和通常更快执行的优点。正如您从第二个表中看到的那样,使用 -server 选项时 Nikita 的算法速度较慢,这显然是由于达到了内存限制。我认为这也会减慢凯文的算法,即使对于小矩阵也是如此,因为函数方法无论如何都会使用更多的内存。为了消除内存因素,我还用 50x50 矩阵尝试了一次,然后 Kevin 花了 5 秒,Nikita 花了 0 秒(好吧,几乎是 0)。所以无论如何,它仍然比较慢,而不仅仅是因为内存。

正如您从数字中看到的,我显然会使用 Nikita 的算法,因为它非常快,这对我来说是绝对必要的。正如丹尼尔指出的那样,它也可以轻松并行化。唯一的缺点是它不是真正的 scala 方式。

目前,凯文的算法可能总体上有点太复杂,因此很慢,但我确信还有更多的优化可能(请参阅他的答案中的最后评论)。

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

下一步是什么?

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

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

选择答案:

至于选择答案,我想将 Nikita 和 Daniel 的算法标记为答案,但我必须选择一个。我的问题的标题包括“最有效”,一个是命令式最快的,另一个是函数式风格最快的。虽然这个问题被标记为 Scala,但我选择了 Nikita 的命令式算法,因为 2 秒与 240 秒的差异仍然太大,让我无法接受。我确信差异仍然可以缩小一点,有什么想法吗?

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


首先,有几个辅助函数:

//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

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

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,并向右扫描,该行中有 2 个相邻单元格包含1。向下扫描,有两个相邻单元格具有包含该值的相同属性1并且在其右侧具有相同数量的相等值。

整理、转换形式的嵌套元组后会更清晰一些((a,b),c) to (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)

Where 1,(2,2)引用包含该值的单元格1,并且位于具有相似值的单元格的 2x2 矩形的左上角。

从这里开始,找到相同大小的矩形就很简单了。不过,如果您还想排除属于较大矩形子集的区域,则需要做更多的工作。

UPDATE:正如所写,该算法对于像 (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)) } }
}

UPDATE2

抓紧你的帽子,这是一个大帽子......

在编写一行新功能之前,我们首先进行一些重构,通过隐式转换将一些方法拉入 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)pairs 方法接受一个函数作为参数,为每个这样的对创建一个输出值。这个以后会派上用场的...

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 个参数块而不是 1 个?我很快就会解释。

I could已将这些定义为方法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) }

作为元组的替代方案,这确实有助于调试。深层嵌套的括号是not容易伤害眼睛。 (对不起 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) => (cell,width)实际上是一个获取单元格值和游程长度的函数(调用它们cell and width)然后返回(cell,width) Tuple.

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

Another 很重要这里说明的原理是类型推断器连续作用于参数块,因此对此(还记得吗?):

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

方式T将由提供的网格确定。然后,编译器知道作为第二个参数提供的函数的输入类型,-Int在这种情况下,因为第一个参数是Grid[Int]- 这就是为什么我能够写case (cell,width) => (cell,width)并且不必在任何地方明确说明cell and width是整数。在第二种用法中,Grid[(Int,Int)]已提供,适合封口case ((cell,width),height) => Rect(width,height)再说一遍,它确实有效。

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

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

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

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

现在混合 4 个数据集(输入值、坐标、最宽矩形、最高矩形),逐渐折叠到单元格中,轻轻搅拌,然后提供:

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 结构,因为必要的坐标已嵌入到结果中。

Voila!

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何最有效地在矩阵中找到给定大小的相同值矩形区域? 的相关文章

  • 如何从一个清晰的例子计算二维图像中的吉布斯能量

    我有一个关于矩阵的有趣问题 在吉布斯分布中 吉布斯能量U x 可以计算为 这是所有可能的派系 C 上的派系势 Vc x 的总和 右图 团 c 被定义为 S 中站点的子集 x 蓝色像素的邻域是左图中黄色像素的邻居 其中每对不同的站点都是邻居
  • 如何从java程序的main方法调用Scala程序的main方法?

    假设我在 Java 项目中有一个 Scala 类和一个 Java 类 scala 类如下所示 class Sam def main args Array String Unit println Hello 如何从同一项目中存在的 java
  • Scalaz 拆箱标记类型不会自动拆箱

    Reading http eed3si9n com learning scalaz Tagged type html http eed3si9n com learning scalaz Tagged type html并尝试示例代码 imp
  • 将当前类作为 scala 中的参数传递

    如何传递当前类作为参数 在java中我们这样做 mymethod this class or mymethod MyClass class 如何将 scala 当前类传递给此方法 this getClass or classOf MyCla
  • Scala 匿名函数中的 return 语句

    为什么显式 return 语句 使用return关键字 在匿名函数中从封闭的命名函数返回 而不仅仅是从匿名函数本身返回 例如 以下程序会导致类型错误 def foo String x Integer gt return x foo 我知道建
  • 相当于 scala 中的 python repr()

    有没有相当于Python的东西reprscala 中的函数 即 您可以给任何 scala 对象提供一个函数 它将生成该对象的字符串表示形式 该对象是有效的 scala 代码 eg val l List Map 1 gt a print re
  • 如何在 Spark 数据帧 groupBy 中执行 count(*)

    我的目的是做相当于基本sql的事情 select shipgrp shipstatus count cnt from shipstatus group by shipgrp shipstatus 我见过的 Spark 数据帧的示例包括其他列
  • 线性问题和非线性问题之间的区别?点积和核技巧的本质

    核技巧将非线性问题映射为线性问题 我的问题是 1 线性问题和非线性问题的主要区别是什么 这两类问题的差异背后的直觉是什么 核技巧如何帮助在非线性问题上使用线性分类器 2 为什么点积在这两种情况下如此重要 Thanks 当人们说到分类问题的线
  • 二分查找问题? [复制]

    这个问题在这里已经有答案了 可能的重复 实施二分查找有哪些陷阱 https stackoverflow com questions 504335 what are the pitfalls in implementing binary se
  • Scala:“递归值...需要类型”,但我只使用 Java 类型

    object Rec extends App val outStream new java io ByteArrayOutputStream val out new java io PrintStream new java io Buffe
  • xsbt 插件 1.0.0-M7 和 scalatra

    我尝试在我的 scalatra 项目中将 xsbt 插件升级到 1 0 0 M7 但 scalatra 似乎与此版本不兼容 当我尝试重新加载项目时 出现以下错误 我尝试过 scalatra 2 3 0 版本 问候 德斯 java lang
  • SBT插件——编译前执行自定义任务

    我刚刚编写了我的第一个 SBT 自动插件 它有一个生成设置文件的自定义任务 如果该文件尚不存在 当显式调用任务时 一切都会按预期工作 但我希望在使用插件编译项目之前自动调用它 无需项目修改其 build sbt 文件 有没有办法实现这一点
  • Scala apply 方法调用,因为括号与隐式参数冲突

    Cay Horstmann 的书 Scala for the Impressive 中有一段关于 apply 方法的注释 有时 表示法会与另一个 Scala 功能发生冲突 隐式参数 例如 表达式 Bonjour sorted 3 产生错误
  • 在 Scala 中反转地图的优雅方法

    目前正在学习Scala 需要反转Map 来进行一些反转值 gt 键查找 我一直在寻找一种简单的方法来做到这一点 但只想到了 Map origMap map kvp gt kvp 2 gt kvp 1 有人有更优雅的方法吗 假设值是唯一的 则
  • 在 AKKA 中,对主管调用 shutdown 是否会停止其监督的所有参与者?

    假设我有一位主管连接了 2 位演员 当我的应用程序关闭时 我想优雅地关闭这些参与者 调用supervisor shutdown 是否会停止所有参与者 还是我仍然需要手动停止我的参与者 gracias 阻止主管 https github co
  • 与文件名中的冒号“:”作斗争

    我有以下代码 用于加载大量 csv gz 并将它们转储到其他文件夹中 并将源文件名作为一列 object DailyMerger extends App def allFiles path File List File val parts
  • 如何改进 PHP 分页算法?

    我正在研究 PHP 中的分页算法 我可以猜测它需要改进的空间 所以我想对如何改进它有一些想法 无论是从 UI UX 的角度清理代码本身 还是你能想到的任何其他东西 该算法应输出如下所示的分页 1 2 3 6 7 8 97 98 99 or
  • 导入 sbt 项目时出错,服务器访问错误,未解决的依赖项

    我正在尝试从 IntelliJ IDE 15 0 2 的 build sbt 中导入我的项目中的库 我不断收到未解决的依赖项错误 我尝试更新不同论坛的设置来解决该问题 但没有任何效果 我尝试过的几件事 使用代理设置更新 sbtconfig
  • 具有轴和角度的 3D 旋转

    我知道 3D 旋转在 SO 和许多其他网站上都有详细记录 但尽管阅读了无数的解释 我仍然没有弄清楚我哪里出错了 我的背景是艺术和设计 而不是数学和编程 而且我从来都不确定我的攻击角度 没有双关语 是否正确 我没有粘贴我那令人沮丧的代码的拼凑
  • glm 中矩阵值的顺序不正确?

    我开始使用GLM http glm g truc net通过 OpenGL 3 和 GLSL 进行数学运算的库 我需要正交投影来绘制 2D 图形 所以我编写了这个简单的代码 glm mat4 projection 1 0 projectio

随机推荐