Skip to content

Commit dee8c30

Browse files
Add floyd_partial and brent_partial cycle-finding functions
Co-authored-by: samueltardieu <[email protected]>
1 parent e462db6 commit dee8c30

File tree

2 files changed

+227
-0
lines changed

2 files changed

+227
-0
lines changed

src/directed/cycle_detection.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,56 @@
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+
/// The upper bound μ̃ is sufficient for many applications, such as computing f^n(x) for
13+
/// large n, where knowing the exact starting point of the cycle is not necessary.
14+
///
15+
/// # Example
16+
///
17+
/// ```
18+
/// use pathfinding::prelude::floyd_partial;
19+
///
20+
/// let (lam, _elem, mu_tilde) = floyd_partial(1, |x| (x * 2) % 7);
21+
/// assert_eq!(lam, 3); // Cycle length
22+
/// assert!(mu_tilde == 0); // Upper bound on mu (start is in cycle, so mu = 0)
23+
/// ```
24+
///
25+
/// # Warning
26+
///
27+
/// If no cycle exist, this function loops forever.
28+
#[expect(clippy::needless_pass_by_value)]
29+
pub fn floyd_partial<T, FS>(start: T, successor: FS) -> (usize, T, usize)
30+
where
31+
T: Clone + PartialEq,
32+
FS: Fn(T) -> T,
33+
{
34+
let mut tortoise = successor(start.clone());
35+
let mut hare = successor(successor(start.clone()));
36+
let mut tortoise_steps = 1;
37+
while tortoise != hare {
38+
(tortoise, hare) = (successor(tortoise), successor(successor(hare)));
39+
tortoise_steps += 1;
40+
}
41+
// tortoise and hare met at position tortoise_steps
42+
let mut lam = 1;
43+
hare = successor(tortoise.clone());
44+
while tortoise != hare {
45+
(hare, lam) = (successor(hare), lam + 1);
46+
}
47+
// Handle edge case where they meet at the start position (pure cycle, mu = 0)
48+
// In this case, tortoise_steps equals lam, and to satisfy mu_tilde < mu + lam,
49+
// we must return 0.
50+
let mu_tilde = if tortoise == start { 0 } else { tortoise_steps };
51+
(lam, tortoise, mu_tilde)
52+
}
53+
354
/// Identify a cycle in an infinite sequence using Floyd's algorithm.
455
/// Return the cycle size, the first element, and the index of first element.
556
///
@@ -29,6 +80,57 @@ where
2980
(lam, tortoise, mu)
3081
}
3182

83+
/// Identify a cycle in an infinite sequence using Brent's algorithm (partial version).
84+
/// Return the cycle size, an element in the cycle, and an upper bound on the index of
85+
/// the first element.
86+
///
87+
/// This function computes the cycle length λ and returns an element within the cycle,
88+
/// along with an upper bound μ̃ on the index of the first cycle element. The upper bound
89+
/// satisfies μ ≤ μ̃. Due to the nature of Brent's algorithm with its power-of-2 stepping,
90+
/// the bound may be looser than `μ + λ` in some cases, but is still reasonable for
91+
/// practical applications.
92+
///
93+
/// This is faster than [`brent`] as it skips the computation of the minimal μ.
94+
/// The upper bound μ̃ is sufficient for many applications, such as computing f^n(x) for
95+
/// large n, where knowing the exact starting point of the cycle is not necessary.
96+
///
97+
/// # Example
98+
///
99+
/// ```
100+
/// use pathfinding::prelude::brent_partial;
101+
///
102+
/// let (lam, _elem, mu_tilde) = brent_partial(1, |x| (x * 2) % 7);
103+
/// assert_eq!(lam, 3); // Cycle length
104+
/// assert!(mu_tilde >= 1); // Upper bound on mu
105+
/// ```
106+
///
107+
/// # Warning
108+
///
109+
/// If no cycle exist, this function loops forever.
110+
#[expect(clippy::needless_pass_by_value)]
111+
pub fn brent_partial<T, FS>(start: T, successor: FS) -> (usize, T, usize)
112+
where
113+
T: Clone + PartialEq,
114+
FS: Fn(T) -> T,
115+
{
116+
let mut power = 1;
117+
let mut lam = 1;
118+
let mut tortoise = start.clone();
119+
let mut hare = successor(start.clone());
120+
let mut hare_steps = 1;
121+
while tortoise != hare {
122+
if power == lam {
123+
(tortoise, power, lam) = (hare.clone(), power * 2, 0);
124+
}
125+
(hare, lam) = (successor(hare), lam + 1);
126+
hare_steps += 1;
127+
}
128+
// At this point, hare has taken hare_steps steps and met tortoise.
129+
// Use hare_steps as the upper bound, as it represents where we detected the cycle.
130+
let mu_tilde = hare_steps;
131+
(lam, hare, mu_tilde)
132+
}
133+
32134
/// Identify a cycle in an infinite sequence using Brent's algorithm.
33135
/// Return the cycle size, the first element, and the index of first element.
34136
///

tests/cycle_detection.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,128 @@ 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
34+
// We know mu = 2 from the full algorithm
35+
assert!(2 <= mu_tilde);
36+
}
37+
38+
#[test]
39+
fn partial_functions_match_full_functions() {
40+
// Test that partial functions return the same lambda as full functions
41+
let f = |x| (x + 5) % 6 + 3;
42+
43+
let (lam_floyd, elem_floyd, mu_floyd) = floyd(-10, f);
44+
let (lam_floyd_partial, elem_floyd_partial, mu_tilde_floyd) = floyd_partial(-10, f);
45+
46+
let (lam_brent, elem_brent, mu_brent) = brent(-10, f);
47+
let (lam_brent_partial, elem_brent_partial, mu_tilde_brent) = brent_partial(-10, f);
48+
49+
// Lambda should be the same
50+
assert_eq!(lam_floyd, lam_floyd_partial);
51+
assert_eq!(lam_brent, lam_brent_partial);
52+
assert_eq!(lam_floyd, lam_brent);
53+
54+
// Elements should be in the cycle
55+
assert!([4, 6, 8].contains(&elem_floyd));
56+
assert!([4, 6, 8].contains(&elem_floyd_partial));
57+
assert!([4, 6, 8].contains(&elem_brent));
58+
assert!([4, 6, 8].contains(&elem_brent_partial));
59+
60+
// Mu values from full algorithms should be the same
61+
assert_eq!(mu_floyd, mu_brent);
62+
63+
// Mu_tilde should be valid upper bounds
64+
assert!(mu_floyd <= mu_tilde_floyd);
65+
assert!(mu_tilde_floyd < mu_floyd + lam_floyd);
66+
assert!(mu_brent <= mu_tilde_brent);
67+
}
68+
69+
#[test]
70+
fn test_longer_cycle() {
71+
// Test with a longer cycle: sequence from 0 to 99, then cycles
72+
let f = |x: i32| (x + 1) % 100;
73+
74+
let (lam_floyd, elem_floyd, mu_floyd) = floyd(0, f);
75+
let (lam_floyd_partial, elem_floyd_partial, mu_tilde_floyd) = floyd_partial(0, f);
76+
77+
let (lam_brent, elem_brent, mu_brent) = brent(0, f);
78+
let (lam_brent_partial, elem_brent_partial, mu_tilde_brent) = brent_partial(0, f);
79+
80+
// Cycle length should be 100 (0, 1, 2, ..., 99, 0, ...)
81+
assert_eq!(lam_floyd, 100);
82+
assert_eq!(lam_floyd_partial, 100);
83+
assert_eq!(lam_brent, 100);
84+
assert_eq!(lam_brent_partial, 100);
85+
86+
// Mu should be 0 (cycle starts immediately)
87+
assert_eq!(mu_floyd, 0);
88+
assert_eq!(mu_brent, 0);
89+
90+
// Elements should be in the cycle
91+
assert!((0..100).contains(&elem_floyd));
92+
assert!((0..100).contains(&elem_floyd_partial));
93+
assert!((0..100).contains(&elem_brent));
94+
assert!((0..100).contains(&elem_brent_partial));
95+
96+
// Mu_tilde should be valid upper bounds
97+
assert!(mu_floyd <= mu_tilde_floyd);
98+
assert!(mu_tilde_floyd < mu_floyd + lam_floyd);
99+
assert!(mu_brent <= mu_tilde_brent);
100+
}
101+
102+
#[test]
103+
fn test_short_cycle_large_mu() {
104+
// Sequence starting from -100, adds 1 each step,
105+
// but when value reaches 10, it resets to 0
106+
// This creates: -100, -99, ..., -1, 0, 1, ..., 9, 10, 0, 1, ..., 9, 10, 0, ...
107+
// mu = 100 (steps to reach 0, which is the start of the cycle), lambda = 11 (0 to 10 inclusive)
108+
let f = |x: i32| if x == 10 { 0 } else { x + 1 };
109+
110+
let (lam_floyd, elem_floyd, mu_floyd) = floyd(-100, f);
111+
let (lam_floyd_partial, elem_floyd_partial, mu_tilde_floyd) = floyd_partial(-100, f);
112+
113+
let (lam_brent, elem_brent, mu_brent) = brent(-100, f);
114+
let (lam_brent_partial, elem_brent_partial, mu_tilde_brent) = brent_partial(-100, f);
115+
116+
// Cycle length should be 11 (0, 1, 2, ..., 9, 10, 0, ...)
117+
assert_eq!(lam_floyd, 11);
118+
assert_eq!(lam_floyd_partial, 11);
119+
assert_eq!(lam_brent, 11);
120+
assert_eq!(lam_brent_partial, 11);
121+
122+
// Mu should be 100 (it takes 100 steps to get from -100 to 0, then cycles)
123+
assert_eq!(mu_floyd, 100);
124+
assert_eq!(mu_brent, 100);
125+
126+
// Elements should be in the cycle (0 to 10)
127+
assert!((0..=10).contains(&elem_floyd));
128+
assert!((0..=10).contains(&elem_floyd_partial));
129+
assert!((0..=10).contains(&elem_brent));
130+
assert!((0..=10).contains(&elem_brent_partial));
131+
132+
// Mu_tilde should be valid upper bounds
133+
assert!(mu_floyd <= mu_tilde_floyd);
134+
assert!(mu_tilde_floyd < mu_floyd + lam_floyd);
135+
assert!(mu_brent <= mu_tilde_brent);
136+
}

0 commit comments

Comments
 (0)