我相信,有一种算法具有 O(N log N) 时间复杂度(或许可以将其提高到 O(N)),并且 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 + L。
在算法中,我们必须遍历序列。我将当前位置称为“指针”。在数字序列中,指针仅表示数字。最初指针等于序列的起始元素,为了将指针向前移动 1 步,我们必须对其应用递归公式。
现在是算法:
- 以两个指向序列开头的指针开始:“慢”和“快”。慢速指针一次移动 1 步,快速指针一次移动 2 步(即递归公式的 2 次应用)。
-
最初指针是相等的。开始向前移动它们直到它们再次相等,并跟踪慢指针的步数。让我们命名指针再次相等的步数K0。可以证明 P ≤ K0 P+L 和K0 = 0 (mod L)。
这一步我们还要特别注意的情况,当P = 0:当指针变得相等时,如果它们也等于起始元素,我们应该设置 K0 = 0,以便以后能够区分这种情况。
这一步的时间复杂度是O(N)。
-
现在指针肯定在序列的循环内。再次开始向前移动它们并跟踪慢速指针的步数,直到它们再次相等。这个步数就是序列的循环L的长度。 (也可以在这一步只移动慢指针,直到它回到相同的位置,但我会复用这个函数来移动两者,不会增加时间复杂度)
这一步的时间复杂度是O(N)。
-
现在我们要计算 P。我们可以注意到,如果在算法的第 2 步)中,我们不是从头开始“快速”指针,而是有一些移位 S:0 ≤ S L,那么结果就会不同:KS = K0 – S,如果 S ≤ K0 – P;或 KS = K0 + L – S强>否则。所以,我们可以用二分查找最大位移S*:0≤S*L,其中 KS* = K0 – S*。然后我们可以找到P = K0 - S*,并返回P + L = K0 – 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)))
}