blog IV | 循环节,龟兔赛跑算法
通常我们讨论函数的“循环节”时,往往指的是迭代函数(即连续地应用 f,比如构造序列
)时,这个序列是否最终进入循环,以及循环部分的长度是多少。如果函数的定义域是有限集(或者你只关注迭代轨迹),那么必然会出现循环。
一个常用且高效的方法是弗洛伊德判圈算法(Floyd’s Cycle Detection,也叫龟兔赛跑算法)。它的基本思路如下:
-
检测循环
定义两个指针:- 慢指针(tortoise) 每次移动一步:
x_{k+1} = f(x_k) 。 - 快指针(hare) 每次移动两步:
x_{k+1} = f(f(x_k)) 。
从某个初始值
x_0 开始,同时移动两个指针,直到两者相遇为止。相遇时可以证明序列已经进入了循环状态。 - 慢指针(tortoise) 每次移动一步:
-
求循环长度
\lambda
当慢指针和快指针第一次相遇后,固定其中一个指针,再沿着序列走一圈(即不断调用 f),直到再次回到相遇点。计数所走的步数,这个数即为循环长度。 -
求循环前的非循环部分长度
\mu
将其中一个指针重置到起始点x_0 ,然后两个指针都每次走一步,直到它们再次相遇。它们相遇的位置就是循环开始的位置,移动的步数就是\mu ,即序列进入循环前的步数。
这种方法的时间复杂度是
此外,还有另一种算法叫 Brent 算法,它的时间复杂度与弗洛伊德算法相当,有时在函数调用代价较高时能减少函数调用次数。
总结来说:
- 判断是否有循环: 对迭代序列使用弗洛伊德算法,当快慢指针相遇时,就证明有循环。
- 求循环节长度: 在快慢指针相遇后,从相遇点再走一圈得到循环长度
\lambda 。 - 求非循环部分长度: 重新设置指针从起点开始走,直到与在循环中的指针相遇,得到
\mu 。
这种方法既简单又高效,是判断函数迭代是否有循环及其周期长度的常用快速方法。
下面是一份使用 C++ 实现弗洛伊德判圈算法(龟兔赛跑算法)的示例代码,该代码演示了如何判断一个函数迭代序列是否存在循环,并求出循环前的非循环部分长度(
下面代码中,我们定义了一个示例函数
并从初始值
#include <iostream>
using namespace std;
// 示例函数 f: f(x) = (x*x + 1) mod 255
int f(int x) {
return (x * x + 1) % 255;
}
int main(){
// 初始值
int x0 = 2;
// Phase 1: 使用快慢指针查找循环
int tortoise = f(x0); // 慢指针走一步
int hare = f(f(x0)); // 快指针走两步
while(tortoise != hare) {
tortoise = f(tortoise);
hare = f(f(hare));
}
// Phase 2: 查找循环起始位置 mu
int mu = 0;
tortoise = x0;
while(tortoise != hare) {
tortoise = f(tortoise);
hare = f(hare);
mu++;
}
// Phase 3: 求循环长度 lambda
int lambda = 1;
hare = f(tortoise);
while(tortoise != hare) {
hare = f(hare);
lambda++;
}
cout << "循环前的非循环部分长度 mu = " << mu << "\n";
cout << "循环长度 lambda = " << lambda << "\n";
return 0;
}
代码说明
-
循环检测(Phase 1):
- 我们定义两个指针:
tortoise每次走一步,hare每次走两步。 - 当两者相遇时,证明迭代序列中存在循环。
- 我们定义两个指针:
-
求非循环部分长度
\mu (Phase 2):- 将其中一个指针重置到起始值,然后两个指针同步前进,当它们再次相遇时,相遇位置即为循环开始的位置,前进的步数就是
\mu 。
- 将其中一个指针重置到起始值,然后两个指针同步前进,当它们再次相遇时,相遇位置即为循环开始的位置,前进的步数就是
-
求循环长度
\lambda (Phase 3):- 从相遇点出发,固定该点,再沿着序列走一圈,直到再次回到该点,所走的步数即为
\lambda 。
- 从相遇点出发,固定该点,再沿着序列走一圈,直到再次回到该点,所走的步数即为
这种方法时间复杂度为
以上内容由 GPT-o3-mini 生成