【发布时间】: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