问题描述
我已经实现了 PapadimitrIoU 的算法来解决 Scala 中的 two-satisfiability 问题。为了提高我的算法的速度,我首先进行了一个缩减步骤,它消除了所有子句中的一个值在整个子句集中只出现(异或)否定或非否定的所有子句。
该算法是一种局部搜索算法,需要在每个循环中设置一个随机初始状态。我不是完全随机的,而是从减少步骤中知道某些位的状态。因此,我的代码生成了一个初始状态的数组缓冲区,其输入是位集的总长度,以及从缩减步骤计算出的约束列表。
最初我的实现只是使用列表来检查特定位是否存在约束,或者我是否需要随机生成位:
// Is bit number x false (negX = true)?
case class Constraint(negX: Boolean,x: Int) extends Ordered[Constraint] {
def compare(that: Constraint) = this.x - that.x
}
private def getinitial(len: Int,allConstraints: List[Constraint]): ArrayBuffer[Boolean] = {
var constraints = allConstraints.sorted
val buff = ArrayBuffer.fill(len)(false)
for (i <- 0 to (len - 1)) {
if (constraints.length > 0 && constraints.head.x == i) {
buff(i) = !constraints.head.negX
constraints = constraints.tail
} else {
buff(i) = math.random < 0.5
}
}
buff
}
以上工作完美,但在分析我的代码后,我发现它很慢。我知道列表的查找通常很慢,因为每次要搜索一个值时都必须遍历整个列表,但我认为当我对列表进行排序并且只是检查头部时,它不会太慢。
无论如何,我尝试了使用 map 的第二个实现:
private def getinitial(len: Int,allConstraints: List[Constraint]): ArrayBuffer[Boolean] = {
var constraintMap = allConstraints.map{ case Constraint(negX,x) => (x,negX)}.toMap
val buff = ArrayBuffer.fill(len)(false)
for (i <- 0 to (len - 1)) {
buff(i) = !constraintMap.get(i).getorElse(math.random < 0.5)
}
buff
}
令我惊讶的是,这明显更快。所以我的问题是为什么?
我有六个不同的数据集,因此我尝试对最小的数据集(100,000 个变量/子句)和最大的数据集(1,000,000 个变量/子句)进行一些时间分析。进行多次运行,我得到了以下最佳时间(由于算法的性质,时间确实会有所不同,最佳时间的重复次数较少,因此生成初始条件的时间将占主导地位)
/* Small dataset */
// Using List
( scala target/scala-2.13/papadimitrIoU-s-algorithmn_2.13-1.0.jar main1; ) 9.71s user 0.16s system 116% cpu 8.451 total
// Using Map
( scala target/scala-2.13/papadimitrIoU-s-algorithmn_2.13-1.0.jar main1; ) 3.94s user 0.14s system 301% cpu 1.351 total
/* Large dataset */
// Using List
// ...never finished within 15 minutes
// Using Map
( scala target/scala-2.13/papadimitrIoU-s-algorithmn_2.13-1.0.jar main6; ) 72.85s user 1.70s system 428% cpu 17.384 total
解决方法
正确的答案是在评论中,但我的美学家无法避免注意到这在实际惯用的 scala 中看起来有多好:)
def getInitial(len: Int,allConstraints: List[Constraint]) = {
@tailrec
def loop(i: Int,constraints: List[Constraint],result: List[Boolean]): List[Boolean] =
(i,constraints) match {
case (0,_) => result.reversed
case (_,head :: tail) if head.x == i => loop(i - 1,tail,!head.negX :: result)
case _ => loop(i-1,constraints,Random.nextBoolean :: result)
}
loop(len-1,allConstraints.sorted.reversed)
}
我认为,如果您使用预填充数组作为结果,您也可以不进行排序(变异数组元素很糟糕,但如果性能很重要,并且约束的大小很大,这将减少几个周期:
def getInitial(len: Int,allConstraints: List[Constraint]) = {
val result = Array.fill(len)(Random.nextBoolean)
allConstraints.foreach { c => result(c.x) = !c.negX }
result
}