我已经来过this question on reddit - 我对此感到非常困惑,但我不知道(似乎没有令人满意的答案)。我敢在这里复制一下:
假设我们有一个流,每个元素取决于它的前身,例如siimple伪随机序列,例如:
def neum(a:Int): Stream[Int] = Stream.iterate(a)(a => (a*a/100)%10000)
这是von-Neumann的随机函数来自问题所引用的exercise
从给定值开始,我们想知道序列何时进入循环。我们可以使用Set for stored values:
以命令式形式轻松完成此操作// like in java
Set<Integer> values = new HashSet<>();
while (true) {
int x = nextValueInSequence(x)
if (values.contains(x)) {
break;
}
values.add(x);
}
然而,对于Scala来说,提出&#34; functional&#34;解。但问题的作者似乎不知道如何在保持O(N)
时间复杂性的情况下实现这一目标。我也是。唯一的评论似乎就是建议直截了当的O(N^2)
解决方案。
答案 0 :(得分:2)
我相信,有一种算法具有 O(N log N)时间复杂度(可能会将其提高到 O(N)),并且< strong> O(1)总内存消耗。也就是说,我们不必记住以前的大部分数字。不变因素虽然很高。
此内存消耗不是使用Stream
计算的,而是使用由起始元素和递归公式定义的常规数字序列。例如Iterator.iterate(start)(a => a * a / 100 % 10000)
。
Stream
会记住以前的结果,并有效地使其成为 O(N)记忆。
让我们说在循环开始之前序列有 P ≥0个元素,并且循环中 L ≥1个元素。例如,序列[2, 10, 13, 9, 11, 17, 11, 17, ...]
的 P = 4且 L = 2.我们需要找到 P + →强>
在算法中我们必须遍历序列。我将当前位置称为&#34;指针&#34;。在数字序列中,指针仅表示数字。最初指针等于序列的起始元素,并且要将指针向前移动1步,我们必须将递归公式应用于它。
现在算法:
最初指针是相等的。开始向前移动它们直到它们再次相等,并跟踪慢速指针的步数。让我们命名指针再次相等的步数 K 0 。可以证明, P ≤ K 0 &lt; P + L 和 K 0 = 0( mod L < /强>)。
在这一步中我们还应该特别注意这种情况,当 P = 0时:当指针变得相等时,如果它们也等于起始元素,我们应该设置 K 0 = 0,以便以后能够区分这种情况。
此步骤的时间复杂度为 O(N)。
现在指针肯定在序列的循环中。再次开始向前移动它们并跟踪慢速指针的步数,直到它们再次相等。该步数是序列的循环 L 的长度。 (您也可以在此步骤中仅移动慢速指针,直到它返回到相同位置,但我会重复使用该功能移动两者,这不会增加时间复杂度)
此步骤的时间复杂度为 O(N)。
现在我们必须计算 P 。我们可以注意到,如果在算法的步骤2)中我们启动&#34; fast&#34;指针不是从一开始,但有一些移位 S :0≤ S &lt; L ,结果会有所不同: K S = K 0 - S ,如果 S ≤ K 0 - P ;或 K S = K 0 + L - S 强>否则。因此,我们可以使用二元搜索来找到最大偏移 S * :0≤ S * &lt; L , K S * = K 0 - S * 。然后我们可以找到 P = K 0 - S * ,并返回< strong> P + L = K 0 - S * + L
此步骤的时间复杂度为 O(N log N),因为二进制搜索中的每个步骤都需要 O(N)。
所以我们有一个算法,在 O(N log N)中使用 O(1)内存。这是一个代码示例:
case class Sequence[T](start: T)(f: T => T) {
def next = Sequence(f(start))(f)
def forward(steps: Int) =
Sequence(Function.chain(List.fill(steps)(f))(start))(f)
}
object Sequence {
def neum(a: Int) = Sequence(a)(a => a * a / 100 % 10000)
def movesToEquality[T](
slow: Sequence[T], fast: Sequence[T], count: Int = 1
): (Sequence[T], Int) = {
val nextSlow = slow.next
val nextFast = fast.forward(2)
if (nextSlow == nextFast) (nextSlow, count)
else movesToEquality(nextSlow, nextFast, count+1)
}
def findLoopStart[T](seq: Sequence[T]): Int = {
val (inLoop, k0) = movesToEquality(seq, seq) match {
case (c, k) if c == seq => (c, 0)
case other => other
}
val (_, loopSize) = movesToEquality(inLoop, inLoop)
def binarySearch(lo: Int, hi: Int): Int = {
if (lo + 1 >= hi) lo
else {
val mid = (lo + hi) / 2
if (movesToEquality(seq, seq.forward(mid))._2 == k0 - mid)
binarySearch(mid, hi)
else
binarySearch(lo, mid)
}
}
k0 - binarySearch(0, loopSize) + loopSize
}
}
object Main extends App {
println(Sequence.findLoopStart(Sequence.neum(1)))
println(Sequence.findLoopStart(Sequence.neum(4100)))
println(Sequence.findLoopStart(Sequence.neum(5761)))
}
答案 1 :(得分:2)
以下是Kolmar在 O(N)时间和 O(1)空间中运行的答案的简化版本。 它基本上是这样做的:
代码:
def cycleOf[T](seq: => Iterator[T]): (Iterator[T], Iterator[T]) = {
def fast = seq.sliding(1, 2) map (_.head)
val meet = seq zip fast drop 1 dropWhile { case (x, y) => x != y }
val met = meet.next()
val period = (meet indexOf met) + 1
val start = seq drop period zip seq indexWhere { case (x, y) => x == y }
(seq take start, seq.slice(start, start + period))
}
您可以通过
尝试val (prefix, cycle) = cycleOf(neum(5761).iterator)
然后prefix.toList
列表(5761,1891,5758,1545,3870,9769,4333,7748,315,992,9840, 8256,1615,6082,9907,1486,2081,3305,9230,1929,7210,9841, 8452,4363,357,1274,6230,8129,806,6496,1980,9204,7136,9224, 821,6740,4276,2841,712,5069,6947,2608,8016,2562,5638,7870, 9369,7781,5439,5827,9539,9925,5056,5631,7081,1405,9740, 8676,2729,4474,166,275,756,5715,6612,7185,6242,9625,6406, 368,1354,8333,4388,2545,4770,7529,6858,321,1030,609,3708, 7492,1300,6900)
和cycle.toList
是
List(6100, 2100, 4100, 8100)
另请注意SpiderPig的建议:您只需将Stream
替换为Iterator
定义中的neum
即可获得更高内存效率的版本。
答案 2 :(得分:1)
我确信有更好的方式来写这个,但这是我的第一次尝试:
def loop[A](xs: Stream[A]): Set[A] =
xs.scanLeft(Set.empty[A])(_ + _).sliding(2)
.find(_.map(_.size).toSet.size == 1).get.head
scala> neum(93).take(8).toList
res0: List[Int] = List(93, 86, 73, 53, 28, 7, 0, 0)
scala> loop(neum(93))
res1: Set[Int] = Set(0, 93, 28, 53, 73, 86, 7)
答案 3 :(得分:1)
我相信在尾递归函数的帮助下可以使用Set
的解决方案:
@tailrec
def neumannCount(x: Int, m: Set[Int] = Set[Int]()): Int = {
if (m.contains(x)) m.size else neumannCount(x * x / 100 % 10000, m + x)
}
该函数只获取当前值和先前元素的集合。它检查Set中是否存在值,如果没有 - 则生成下一个元素,另一个元素添加当前元素 - 并将它们传递给另一个调用同一函数。当值最后遇到时 - 我们只返回Set的大小作为结果(因此该函数是尾递归的)。
我相信这应该是时间上的O(1)和空间中的O(N),这要归功于相互建立的不可变集合(如果我理解它们的话)。
答案 4 :(得分:0)
它必须是流吗?迭代器更快。 这是两种不同的解决方案。两者都是功能性的,不会改变任何状态。
def neumann(seed: Int): Int = {
Iterator.iterate(seed)(s => ((s * s)/100)%10000)
.scanLeft(Set.empty[Int])((set, n) => if(set(n)) Set(-1) else set + n)
.takeWhile(_ != Set(-1)).size - 1
}
def neumann(seed: Int): Int = {
def search(s: Int, set: Set[Int], count: Int): Int = {
if(set(s)) count
else search(((s * s)/100)%10000, set + s, count + 1)
}
search(seed, Set.empty[Int], 0)
}