Skip to content

Commit a84b940

Browse files
feat: add Smallest Range to greedy algorithms (#1023)
1 parent 8856177 commit a84b940

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@
212212
* [Two Satisfiability](https://github.com/TheAlgorithms/Rust/blob/master/src/graph/two_satisfiability.rs)
213213
* Greedy
214214
* [Minimum Coin Change](https://github.com/TheAlgorithms/Rust/blob/master/src/greedy/minimum_coin_changes.rs)
215+
* [Smallest Range](https://github.com/TheAlgorithms/Rust/blob/master/src/greedy/smallest_range.rs)
215216
* [Stable Matching](https://github.com/TheAlgorithms/Rust/blob/master/src/greedy/stable_matching.rs)
216217
* [Lib](https://github.com/TheAlgorithms/Rust/blob/master/src/lib.rs)
217218
* Machine Learning

src/greedy/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
mod minimum_coin_change;
2+
mod smallest_range;
23
mod stable_matching;
34

45
pub use self::minimum_coin_change::find_minimum_change;
6+
pub use self::smallest_range::smallest_range;
57
pub use self::stable_matching::stable_matching;

src/greedy/smallest_range.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
//! # Smallest Range Covering Elements from K Lists
2+
//!
3+
//! Given `k` sorted integer lists, finds the smallest range `[lo, hi]` such
4+
//! that at least one element from every list lies within that range.
5+
//!
6+
//! ## Algorithm
7+
//!
8+
//! A min-heap is seeded with the first element of each list. On every
9+
//! iteration the heap yields the current global minimum; the global maximum is
10+
//! maintained separately. If `[min, max]` is tighter than the best range seen
11+
//! so far, it is recorded. The minimum is then replaced by the next element
12+
//! from the same list. The loop stops as soon as any list is exhausted,
13+
//! because no further range can cover all lists.
14+
//!
15+
//! ## References
16+
//!
17+
//! - <https://en.wikipedia.org/wiki/Priority_queue>
18+
19+
use std::cmp::Reverse;
20+
use std::collections::BinaryHeap;
21+
22+
/// Finds the smallest range that includes at least one number from each of the
23+
/// given sorted lists.
24+
///
25+
/// Time complexity: `O(n log k)` where `n` is the total number of elements
26+
/// and `k` is the number of lists.
27+
///
28+
/// Space complexity: `O(k)` for the heap.
29+
///
30+
/// Returns `None` if any list is empty.
31+
pub fn smallest_range(nums: &[&[i64]]) -> Option<[i64; 2]> {
32+
// A range cannot cover an empty list
33+
if nums.iter().any(|list| list.is_empty()) {
34+
return None;
35+
}
36+
37+
// Heap entries: (Reverse(value), list_index, element_index).
38+
// Wrapping the value in Reverse turns BinaryHeap (max-heap) into a min-heap.
39+
let mut heap: BinaryHeap<(Reverse<i64>, usize, usize)> = BinaryHeap::new();
40+
let mut current_max = i64::MIN;
41+
42+
// Seed the heap with the first element from each list
43+
for (list_idx, list) in nums.iter().enumerate() {
44+
heap.push((Reverse(list[0]), list_idx, 0));
45+
current_max = current_max.max(list[0]);
46+
}
47+
48+
// Use Option to avoid sentinel arithmetic that could overflow
49+
let mut best: Option<[i64; 2]> = None;
50+
51+
let is_tighter = |candidate: [i64; 2], best: Option<[i64; 2]>| match best {
52+
None => true,
53+
Some(b) => (candidate[1] - candidate[0]) < (b[1] - b[0]),
54+
};
55+
56+
while let Some((Reverse(current_min), list_idx, elem_idx)) = heap.pop() {
57+
// Check if [current_min, current_max] beats the best range seen so far
58+
let candidate = [current_min, current_max];
59+
if is_tighter(candidate, best) {
60+
best = Some(candidate);
61+
}
62+
63+
// If this list is exhausted we can no longer cover all lists
64+
let next_idx = elem_idx + 1;
65+
if next_idx == nums[list_idx].len() {
66+
break;
67+
}
68+
69+
// Advance to the next element in the same list
70+
let next_val = nums[list_idx][next_idx];
71+
heap.push((Reverse(next_val), list_idx, next_idx));
72+
current_max = current_max.max(next_val);
73+
}
74+
75+
best
76+
}
77+
78+
#[cfg(test)]
79+
mod tests {
80+
use super::*;
81+
82+
#[test]
83+
fn mixed_lists() {
84+
assert_eq!(
85+
smallest_range(&[&[4, 10, 15, 24, 26], &[0, 9, 12, 20], &[5, 18, 22, 30]]),
86+
Some([20, 24])
87+
);
88+
}
89+
90+
#[test]
91+
fn identical_lists() {
92+
assert_eq!(
93+
smallest_range(&[&[1, 2, 3], &[1, 2, 3], &[1, 2, 3]]),
94+
Some([1, 1])
95+
);
96+
}
97+
98+
#[test]
99+
fn negative_and_positive() {
100+
assert_eq!(
101+
smallest_range(&[&[-3, -2, -1], &[0, 0, 0], &[1, 2, 3]]),
102+
Some([-1, 1])
103+
);
104+
}
105+
106+
#[test]
107+
fn non_overlapping() {
108+
assert_eq!(
109+
smallest_range(&[&[1, 2, 3], &[4, 5, 6], &[7, 8, 9]]),
110+
Some([3, 7])
111+
);
112+
}
113+
114+
#[test]
115+
fn all_zeros() {
116+
assert_eq!(
117+
smallest_range(&[&[0, 0, 0], &[0, 0, 0], &[0, 0, 0]]),
118+
Some([0, 0])
119+
);
120+
}
121+
122+
#[test]
123+
fn empty_lists() {
124+
assert_eq!(smallest_range(&[&[], &[], &[]]), None);
125+
}
126+
127+
#[test]
128+
fn single_elements() {
129+
assert_eq!(smallest_range(&[&[5], &[3], &[9]]), Some([3, 9]));
130+
}
131+
132+
#[test]
133+
fn single_list() {
134+
assert_eq!(smallest_range(&[&[1, 2, 3]]), Some([1, 1]));
135+
}
136+
137+
#[test]
138+
fn one_empty_among_non_empty() {
139+
assert_eq!(smallest_range(&[&[1, 2], &[], &[3, 4]]), None);
140+
}
141+
}

0 commit comments

Comments
 (0)