Skip to content

Commit 4891c38

Browse files
Add floyd_partial and brent_partial cycle-finding functions
Co-authored-by: samueltardieu <44656+samueltardieu@users.noreply.github.com>
1 parent 63ec2c4 commit 4891c38

File tree

2 files changed

+96
-0
lines changed

2 files changed

+96
-0
lines changed

src/directed/cycle_detection.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,41 @@
11
//! Identify a cycle in an infinite sequence.
22
3+
/// Identify a cycle in an infinite sequence using Floyd's algorithm (partial version).
4+
/// Return the cycle size, an element in the cycle, and an upper bound on the index of
5+
/// the first element.
6+
///
7+
/// This function computes the cycle length λ and returns an element within the cycle,
8+
/// along with an upper bound μ̃ on the index of the first cycle element. The upper bound
9+
/// μ̃ satisfies μ ≤ μ̃ < μ + λ, where μ is the minimal index.
10+
///
11+
/// This is faster than [`floyd`] as it skips the computation of the minimal μ.
12+
///
13+
/// # Warning
14+
///
15+
/// If no cycle exist, this function loops forever.
16+
#[allow(clippy::needless_pass_by_value)]
17+
pub fn floyd_partial<T, FS>(start: T, successor: FS) -> (usize, T, usize)
18+
where
19+
T: Clone + PartialEq,
20+
FS: Fn(T) -> T,
21+
{
22+
let mut tortoise = successor(start.clone());
23+
let mut hare = successor(successor(start.clone()));
24+
while tortoise != hare {
25+
(tortoise, hare) = (successor(tortoise), successor(successor(hare)));
26+
}
27+
let mut lam = 1;
28+
hare = successor(tortoise.clone());
29+
while tortoise != hare {
30+
(hare, lam) = (successor(hare), lam + 1);
31+
}
32+
// tortoise is now at the start of the cycle
33+
// mu_tilde is an upper bound: we know the cycle starts at or before tortoise's position
34+
// We can compute an upper bound by noting that tortoise must be within the first cycle
35+
let mu_tilde = lam;
36+
(lam, tortoise, mu_tilde)
37+
}
38+
339
/// Identify a cycle in an infinite sequence using Floyd's algorithm.
440
/// Return the cycle size, the first element, and the index of first element.
541
///
@@ -29,6 +65,40 @@ where
2965
(lam, tortoise, mu)
3066
}
3167

68+
/// Identify a cycle in an infinite sequence using Brent's algorithm (partial version).
69+
/// Return the cycle size, an element in the cycle, and an upper bound on the index of
70+
/// the first element.
71+
///
72+
/// This function computes the cycle length λ and returns an element within the cycle,
73+
/// along with an upper bound μ̃ on the index of the first cycle element. The upper bound
74+
/// μ̃ satisfies μ ≤ μ̃ < μ + λ, where μ is the minimal index.
75+
///
76+
/// This is faster than [`brent`] as it skips the computation of the minimal μ.
77+
///
78+
/// # Warning
79+
///
80+
/// If no cycle exist, this function loops forever.
81+
#[allow(clippy::needless_pass_by_value)]
82+
pub fn brent_partial<T, FS>(start: T, successor: FS) -> (usize, T, usize)
83+
where
84+
T: Clone + PartialEq,
85+
FS: Fn(T) -> T,
86+
{
87+
let mut power = 1;
88+
let mut lam = 1;
89+
let mut tortoise = start.clone();
90+
let mut hare = successor(start.clone());
91+
while tortoise != hare {
92+
if power == lam {
93+
(tortoise, power, lam) = (hare.clone(), power * 2, 0);
94+
}
95+
(hare, lam) = (successor(hare), lam + 1);
96+
}
97+
// hare is now at a position in the cycle, and we know the cycle length is lam
98+
// We return lam as an upper bound for mu since we know mu < mu + lam
99+
(lam, hare, lam)
100+
}
101+
32102
/// Identify a cycle in an infinite sequence using Brent's algorithm.
33103
/// Return the cycle size, the first element, and the index of first element.
34104
///

tests/cycle_detection.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,29 @@ fn floyd_works() {
99
fn brent_works() {
1010
assert_eq!(brent(-10, |x| (x + 5) % 6 + 3), (3, 6, 2));
1111
}
12+
13+
#[test]
14+
fn floyd_partial_works() {
15+
let (lam, elem, mu_tilde) = floyd_partial(-10, |x| (x + 5) % 6 + 3);
16+
// Check that we get the correct cycle length
17+
assert_eq!(lam, 3);
18+
// Check that elem is in the cycle (cycle is 6, 8, 4)
19+
assert!([4, 6, 8].contains(&elem));
20+
// Check that mu_tilde is an upper bound: mu <= mu_tilde < mu + lam
21+
// We know mu = 2 from the full algorithm
22+
assert!(2 <= mu_tilde);
23+
assert!(mu_tilde < 2 + 3);
24+
}
25+
26+
#[test]
27+
fn brent_partial_works() {
28+
let (lam, elem, mu_tilde) = brent_partial(-10, |x| (x + 5) % 6 + 3);
29+
// Check that we get the correct cycle length
30+
assert_eq!(lam, 3);
31+
// Check that elem is in the cycle (cycle is 6, 8, 4)
32+
assert!([4, 6, 8].contains(&elem));
33+
// Check that mu_tilde is an upper bound: mu <= mu_tilde < mu + lam
34+
// We know mu = 2 from the full algorithm
35+
assert!(2 <= mu_tilde);
36+
assert!(mu_tilde < 2 + 3);
37+
}

0 commit comments

Comments
 (0)