Skip to content

Commit 299f425

Browse files
Copilotsamueltardieu
authored andcommitted
feat: add floyd_partial and brent_partial cycle-finding functions
1 parent 940d423 commit 299f425

File tree

2 files changed

+221
-13
lines changed

2 files changed

+221
-13
lines changed

src/directed/cycle_detection.rs

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,140 @@
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.
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+
/// ```
524
///
625
/// # Warning
726
///
827
/// If no cycle exist, this function loops forever.
9-
pub fn floyd<T, FS>(start: T, successor: FS) -> (usize, T, usize)
28+
#[expect(clippy::needless_pass_by_value)]
29+
pub fn floyd_partial<T, FS>(start: T, successor: FS) -> (usize, T, usize)
1030
where
1131
T: Clone + PartialEq,
1232
FS: Fn(T) -> T,
1333
{
1434
let mut tortoise = successor(start.clone());
1535
let mut hare = successor(successor(start.clone()));
36+
let mut tortoise_steps = 1;
1637
while tortoise != hare {
1738
(tortoise, hare) = (successor(tortoise), successor(successor(hare)));
39+
tortoise_steps += 1;
1840
}
19-
let mut mu = 0;
20-
tortoise = start;
21-
while tortoise != hare {
22-
(tortoise, hare, mu) = (successor(tortoise), successor(hare), mu + 1);
23-
}
41+
// tortoise and hare met at position tortoise_steps
2442
let mut lam = 1;
2543
hare = successor(tortoise.clone());
2644
while tortoise != hare {
2745
(hare, lam) = (successor(hare), lam + 1);
2846
}
29-
(lam, tortoise, mu)
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)
3052
}
3153

32-
/// Identify a cycle in an infinite sequence using Brent's algorithm.
54+
/// Identify a cycle in an infinite sequence using Floyd's algorithm.
3355
/// Return the cycle size, the first element, and the index of first element.
3456
///
3557
/// # Warning
3658
///
3759
/// If no cycle exist, this function loops forever.
38-
pub fn brent<T, FS>(start: T, successor: FS) -> (usize, T, usize)
60+
pub fn floyd<T, FS>(start: T, successor: FS) -> (usize, T, usize)
61+
where
62+
T: Clone + PartialEq,
63+
FS: Fn(T) -> T,
64+
{
65+
let (lam, mut hare, _) = floyd_partial(start.clone(), &successor);
66+
// Find the exact mu
67+
let (mut mu, mut tortoise) = (0, start);
68+
while tortoise != hare {
69+
(tortoise, hare, mu) = (successor(tortoise), successor(hare), mu + 1);
70+
}
71+
(lam, tortoise, mu)
72+
}
73+
74+
/// Identify a cycle in an infinite sequence using Brent's algorithm (partial version).
75+
/// Return the cycle size, an element in the cycle, and an upper bound on the index of
76+
/// the first element.
77+
///
78+
/// This function computes the cycle length λ and returns an element within the cycle,
79+
/// along with an upper bound μ̃ on the index of the first cycle element. The upper bound
80+
/// satisfies μ ≤ μ̃. Due to the nature of Brent's algorithm with its power-of-2 stepping,
81+
/// the bound may be looser than `μ + λ` in some cases, but is still reasonable for
82+
/// practical applications.
83+
///
84+
/// This is faster than [`brent`] as it skips the computation of the minimal μ.
85+
/// The upper bound μ̃ is sufficient for many applications, such as computing f^n(x) for
86+
/// large n, where knowing the exact starting point of the cycle is not necessary.
87+
///
88+
/// # Example
89+
///
90+
/// ```
91+
/// use pathfinding::prelude::brent_partial;
92+
///
93+
/// let (lam, _elem, mu_tilde) = brent_partial(1, |x| (x * 2) % 7);
94+
/// assert_eq!(lam, 3); // Cycle length
95+
/// assert!(mu_tilde >= 1); // Upper bound on mu
96+
/// ```
97+
///
98+
/// # Warning
99+
///
100+
/// If no cycle exist, this function loops forever.
101+
pub fn brent_partial<T, FS>(start: T, successor: FS) -> (usize, T, usize)
39102
where
40103
T: Clone + PartialEq,
41104
FS: Fn(T) -> T,
42105
{
43106
let mut power = 1;
44107
let mut lam = 1;
45108
let mut tortoise = start.clone();
46-
let mut hare = successor(start.clone());
109+
let mut hare = successor(start);
110+
let mut hare_steps = 1;
47111
while tortoise != hare {
48112
if power == lam {
49113
(tortoise, power, lam) = (hare.clone(), power * 2, 0);
50114
}
51115
(hare, lam) = (successor(hare), lam + 1);
116+
hare_steps += 1;
52117
}
118+
// Use hare_steps as the upper bound, as it represents where we detected the cycle.
119+
(lam, hare, hare_steps)
120+
}
121+
122+
/// Identify a cycle in an infinite sequence using Brent's algorithm.
123+
/// Return the cycle size, the first element, and the index of first element.
124+
///
125+
/// # Warning
126+
///
127+
/// If no cycle exist, this function loops forever.
128+
pub fn brent<T, FS>(start: T, successor: FS) -> (usize, T, usize)
129+
where
130+
T: Clone + PartialEq,
131+
FS: Fn(T) -> T,
132+
{
133+
let (lam, _hare, _hare_steps) = brent_partial(start.clone(), &successor);
134+
// Find the exact mu
53135
let mut mu = 0;
54-
(tortoise, hare) = (start.clone(), (0..lam).fold(start, |x, _| successor(x)));
136+
let mut tortoise = start.clone();
137+
let mut hare = (0..lam).fold(start, |x, _| successor(x));
55138
while tortoise != hare {
56139
(tortoise, hare, mu) = (successor(tortoise), successor(hare), mu + 1);
57140
}

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)