Skip to content

Commit 5f575a2

Browse files
Copilotsamueltardieu
authored andcommitted
feat: add floyd_partial and brent_partial cycle-finding functions
1 parent becfe6b commit 5f575a2

File tree

2 files changed

+253
-21
lines changed

2 files changed

+253
-21
lines changed

src/directed/cycle_detection.rs

Lines changed: 128 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,31 @@
11
//! Identify a cycle in an infinite sequence.
22
3-
/// Identify a cycle in an infinite sequence using Floyd's algorithm.
4-
/// Return the cycle size, the first element, and the index of first element.
5-
///
6-
/// # Warning
7-
///
8-
/// If no cycle exist, this function loops forever.
9-
pub fn floyd<T, FS>(start: T, successor: FS) -> (usize, T, usize)
3+
/// Find the meeting point and lambda using Floyd's algorithm.
4+
/// Returns (`lambda`, `meeting_element`, `tortoise_steps`).
5+
fn floyd_find_cycle<T, FS>(start: &T, successor: &FS) -> (usize, T, usize)
106
where
117
T: Clone + PartialEq,
128
FS: Fn(T) -> T,
139
{
1410
let mut tortoise = successor(start.clone());
1511
let mut hare = successor(successor(start.clone()));
12+
let mut tortoise_steps = 1;
1613
while tortoise != hare {
1714
(tortoise, hare) = (successor(tortoise), successor(successor(hare)));
15+
tortoise_steps += 1;
1816
}
19-
let mut mu = 0;
20-
tortoise = start;
21-
while tortoise != hare {
22-
(tortoise, hare, mu) = (successor(tortoise), successor(hare), mu + 1);
23-
}
17+
// tortoise and hare met at position tortoise_steps
2418
let mut lam = 1;
2519
hare = successor(tortoise.clone());
2620
while tortoise != hare {
2721
(hare, lam) = (successor(hare), lam + 1);
2822
}
29-
(lam, tortoise, mu)
23+
(lam, tortoise, tortoise_steps)
3024
}
3125

32-
/// Identify a cycle in an infinite sequence using Brent's algorithm.
33-
/// Return the cycle size, the first element, and the index of first element.
34-
///
35-
/// # Warning
36-
///
37-
/// If no cycle exist, this function loops forever.
38-
pub fn brent<T, FS>(start: T, successor: FS) -> (usize, T, usize)
26+
/// Find the cycle using Brent's algorithm.
27+
/// Returns (`lambda`, `meeting_element`, `hare_steps`).
28+
fn brent_find_cycle<T, FS>(start: &T, successor: &FS) -> (usize, T, usize)
3929
where
4030
T: Clone + PartialEq,
4131
FS: Fn(T) -> T,
@@ -44,14 +34,131 @@ where
4434
let mut lam = 1;
4535
let mut tortoise = start.clone();
4636
let mut hare = successor(start.clone());
37+
let mut hare_steps = 1;
4738
while tortoise != hare {
4839
if power == lam {
4940
(tortoise, power, lam) = (hare.clone(), power * 2, 0);
5041
}
5142
(hare, lam) = (successor(hare), lam + 1);
43+
hare_steps += 1;
44+
}
45+
(lam, hare, hare_steps)
46+
}
47+
48+
/// Identify a cycle in an infinite sequence using Floyd's algorithm (partial version).
49+
/// Return the cycle size, an element in the cycle, and an upper bound on the index of
50+
/// the first element.
51+
///
52+
/// This function computes the cycle length λ and returns an element within the cycle,
53+
/// along with an upper bound μ̃ on the index of the first cycle element. The upper bound
54+
/// μ̃ satisfies μ ≤ μ̃ < μ + λ, where μ is the minimal index.
55+
///
56+
/// This is faster than [`floyd`] as it skips the computation of the minimal μ.
57+
/// The upper bound μ̃ is sufficient for many applications, such as computing f^n(x) for
58+
/// large n, where knowing the exact starting point of the cycle is not necessary.
59+
///
60+
/// # Example
61+
///
62+
/// ```
63+
/// use pathfinding::prelude::floyd_partial;
64+
///
65+
/// let (lam, _elem, mu_tilde) = floyd_partial(1, |x| (x * 2) % 7);
66+
/// assert_eq!(lam, 3); // Cycle length
67+
/// assert!(mu_tilde == 0); // Upper bound on mu (start is in cycle, so mu = 0)
68+
/// ```
69+
///
70+
/// # Warning
71+
///
72+
/// If no cycle exist, this function loops forever.
73+
#[expect(clippy::needless_pass_by_value)]
74+
pub fn floyd_partial<T, FS>(start: T, successor: FS) -> (usize, T, usize)
75+
where
76+
T: Clone + PartialEq,
77+
FS: Fn(T) -> T,
78+
{
79+
let (lam, tortoise, tortoise_steps) = floyd_find_cycle(&start, &successor);
80+
// Handle edge case where they meet at the start position (pure cycle, mu = 0)
81+
// In this case, tortoise_steps equals lam, and to satisfy mu_tilde < mu + lam,
82+
// we must return 0.
83+
let mu_tilde = if tortoise == start { 0 } else { tortoise_steps };
84+
(lam, tortoise, mu_tilde)
85+
}
86+
87+
/// Identify a cycle in an infinite sequence using Floyd's algorithm.
88+
/// Return the cycle size, the first element, and the index of first element.
89+
///
90+
/// # Warning
91+
///
92+
/// If no cycle exist, this function loops forever.
93+
pub fn floyd<T, FS>(start: T, successor: FS) -> (usize, T, usize)
94+
where
95+
T: Clone + PartialEq,
96+
FS: Fn(T) -> T,
97+
{
98+
let (lam, mut hare, _tortoise_steps) = floyd_find_cycle(&start, &successor);
99+
// Find the exact mu
100+
let (mut mu, mut tortoise) = (0, start);
101+
while tortoise != hare {
102+
(tortoise, hare, mu) = (successor(tortoise), successor(hare), mu + 1);
52103
}
104+
(lam, tortoise, mu)
105+
}
106+
107+
/// Identify a cycle in an infinite sequence using Brent's algorithm (partial version).
108+
/// Return the cycle size, an element in the cycle, and an upper bound on the index of
109+
/// the first element.
110+
///
111+
/// This function computes the cycle length λ and returns an element within the cycle,
112+
/// along with an upper bound μ̃ on the index of the first cycle element. The upper bound
113+
/// satisfies μ ≤ μ̃. Due to the nature of Brent's algorithm with its power-of-2 stepping,
114+
/// the bound may be looser than `μ + λ` in some cases, but is still reasonable for
115+
/// practical applications.
116+
///
117+
/// This is faster than [`brent`] as it skips the computation of the minimal μ.
118+
/// The upper bound μ̃ is sufficient for many applications, such as computing f^n(x) for
119+
/// large n, where knowing the exact starting point of the cycle is not necessary.
120+
///
121+
/// # Example
122+
///
123+
/// ```
124+
/// use pathfinding::prelude::brent_partial;
125+
///
126+
/// let (lam, _elem, mu_tilde) = brent_partial(1, |x| (x * 2) % 7);
127+
/// assert_eq!(lam, 3); // Cycle length
128+
/// assert!(mu_tilde >= 1); // Upper bound on mu
129+
/// ```
130+
///
131+
/// # Warning
132+
///
133+
/// If no cycle exist, this function loops forever.
134+
#[expect(clippy::needless_pass_by_value)]
135+
pub fn brent_partial<T, FS>(start: T, successor: FS) -> (usize, T, usize)
136+
where
137+
T: Clone + PartialEq,
138+
FS: Fn(T) -> T,
139+
{
140+
let (lam, hare, hare_steps) = brent_find_cycle(&start, &successor);
141+
// Use hare_steps as the upper bound, as it represents where we detected the cycle.
142+
let mu_tilde = hare_steps;
143+
(lam, hare, mu_tilde)
144+
}
145+
146+
/// Identify a cycle in an infinite sequence using Brent's algorithm.
147+
/// Return the cycle size, the first element, and the index of first element.
148+
///
149+
/// # Warning
150+
///
151+
/// If no cycle exist, this function loops forever.
152+
pub fn brent<T, FS>(start: T, successor: FS) -> (usize, T, usize)
153+
where
154+
T: Clone + PartialEq,
155+
FS: Fn(T) -> T,
156+
{
157+
let (lam, _hare, _hare_steps) = brent_find_cycle(&start, &successor);
158+
// Find the exact mu
53159
let mut mu = 0;
54-
(tortoise, hare) = (start.clone(), (0..lam).fold(start, |x, _| successor(x)));
160+
let mut tortoise = start.clone();
161+
let mut hare = (0..lam).fold(start, |x, _| successor(x));
55162
while tortoise != hare {
56163
(tortoise, hare, mu) = (successor(tortoise), successor(hare), mu + 1);
57164
}

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)