Skip to content

Commit c4c395f

Browse files
feat: add job sequencing algorithm (#1038)
1 parent 3bccfa8 commit c4c395f

3 files changed

Lines changed: 272 additions & 0 deletions

File tree

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@
209209
* [Topological Sort](https://github.com/TheAlgorithms/Rust/blob/master/src/graph/topological_sort.rs)
210210
* [Two Satisfiability](https://github.com/TheAlgorithms/Rust/blob/master/src/graph/two_satisfiability.rs)
211211
* Greedy
212+
* [Job Sequencing](https://github.com/TheAlgorithms/Rust/blob/master/src/greedy/job_sequencing.rs)
212213
* [Minimum Coin Change](https://github.com/TheAlgorithms/Rust/blob/master/src/greedy/minimum_coin_changes.rs)
213214
* [Smallest Range](https://github.com/TheAlgorithms/Rust/blob/master/src/greedy/smallest_range.rs)
214215
* [Stable Matching](https://github.com/TheAlgorithms/Rust/blob/master/src/greedy/stable_matching.rs)

src/greedy/job_sequencing.rs

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
//! Job Sequencing
2+
//!
3+
//! Given a set of jobs, each with a deadline and profit, schedule jobs to
4+
//! maximise total profit. Each job takes exactly one unit of time and must
5+
//! be completed on or before its deadline. Only one job can run at a time.
6+
//!
7+
//! # Algorithm (greedy)
8+
//! 1. Sort jobs by profit in descending order.
9+
//! 2. For each job (highest profit first), find the latest free time-slot
10+
//! that is ≤ the job's deadline and assign the job there.
11+
//! 3. Return the sequence of scheduled jobs and the total profit earned.
12+
//!
13+
//! # Complexity
14+
//! - Time: O(n·D) — for each of the n jobs we may scan backwards through up to D slots,
15+
//! where D is the maximum deadline.
16+
//! - Space: O(min(n, D)) — slot array is capped at the number of jobs, since at most
17+
//! n jobs can ever be scheduled regardless of how large D is.
18+
//!
19+
//! # References
20+
//! - Cormen et al., *Introduction to Algorithms*, 4th ed., §16.5
21+
//! - <https://en.wikipedia.org/wiki/Optimal_job_scheduling>
22+
23+
/// A single job described by a name, a deadline (1-indexed, in time units),
24+
/// and the profit earned if the job is completed on time.
25+
#[derive(Debug, Clone, PartialEq, Eq)]
26+
pub struct Job {
27+
pub name: String,
28+
pub deadline: usize,
29+
pub profit: u64,
30+
}
31+
32+
impl Job {
33+
/// Constructs a new [`Job`].
34+
///
35+
/// # Panics
36+
/// Panics if `deadline` is zero, because every job must be completable
37+
/// in at least the first time-slot.
38+
pub fn new(name: impl Into<String>, deadline: usize, profit: u64) -> Self {
39+
assert!(deadline >= 1, "deadline must be at least 1");
40+
Self {
41+
name: name.into(),
42+
deadline,
43+
profit,
44+
}
45+
}
46+
}
47+
48+
/// Result returned by [`schedule_jobs`].
49+
#[derive(Debug, PartialEq, Eq)]
50+
pub struct ScheduleResult {
51+
/// Names of the scheduled jobs in slot order (slot 1 first).
52+
pub job_sequence: Vec<String>,
53+
/// Total profit from the scheduled jobs.
54+
pub total_profit: u64,
55+
}
56+
57+
/// Schedules jobs to maximise total profit under deadline constraints.
58+
///
59+
/// Returns the optimal [`ScheduleResult`] — the scheduled job sequence
60+
/// (in time-slot order) and the corresponding total profit.
61+
///
62+
/// # Examples
63+
///
64+
/// ```
65+
/// use the_algorithms_rust::greedy::{Job, schedule_jobs};
66+
///
67+
/// let jobs = vec![
68+
/// Job::new("A", 2, 100),
69+
/// Job::new("B", 1, 19),
70+
/// Job::new("C", 2, 27),
71+
/// Job::new("D", 1, 25),
72+
/// Job::new("E", 3, 15),
73+
/// ];
74+
///
75+
/// let result = schedule_jobs(jobs);
76+
/// assert_eq!(result.total_profit, 142);
77+
/// assert_eq!(result.job_sequence, vec!["C", "A", "E"]);
78+
/// ```
79+
pub fn schedule_jobs(mut jobs: Vec<Job>) -> ScheduleResult {
80+
if jobs.is_empty() {
81+
return ScheduleResult {
82+
job_sequence: vec![],
83+
total_profit: 0,
84+
};
85+
}
86+
87+
// Step 1 – sort jobs by profit, highest first.
88+
jobs.sort_unstable_by(|a, b| b.profit.cmp(&a.profit));
89+
90+
// Step 2 – allocate slots.
91+
// At most n jobs can ever be scheduled, so cap the slot count at jobs.len()
92+
// to avoid huge allocations when max_deadline is large.
93+
let max_deadline = jobs.iter().map(|j| j.deadline).max().unwrap_or(0);
94+
let num_slots = max_deadline.min(jobs.len());
95+
96+
// slots[i] holds the name of the job assigned to time-slot (i + 1),
97+
// or None if the slot is still free.
98+
let mut slots: Vec<Option<String>> = vec![None; num_slots];
99+
100+
let mut total_profit: u64 = 0;
101+
102+
// Consume jobs by value to move names directly into slots (no clone needed).
103+
for job in jobs {
104+
// Find the latest free slot at or before this job's deadline.
105+
// Slots are 1-indexed in the problem but 0-indexed in our Vec.
106+
// Also bound the search to num_slots to stay within the allocated range.
107+
let deadline_bound = job.deadline.min(num_slots);
108+
if let Some(slot) = (0..deadline_bound).rev().find(|&s| slots[s].is_none()) {
109+
slots[slot] = Some(job.name);
110+
total_profit += job.profit;
111+
}
112+
// If no free slot is found the job is skipped (greedy choice).
113+
}
114+
115+
// Step 3 – collect scheduled jobs in slot (time) order, skipping empty slots.
116+
let job_sequence = slots.into_iter().flatten().collect();
117+
118+
ScheduleResult {
119+
job_sequence,
120+
total_profit,
121+
}
122+
}
123+
124+
#[cfg(test)]
125+
mod tests {
126+
use super::*;
127+
128+
// -----------------------------------------------------------------------
129+
// Helper
130+
// -----------------------------------------------------------------------
131+
132+
fn make_result(jobs: &[&str], profit: u64) -> ScheduleResult {
133+
ScheduleResult {
134+
job_sequence: jobs.iter().map(|&s| s.to_string()).collect(),
135+
total_profit: profit,
136+
}
137+
}
138+
139+
// -----------------------------------------------------------------------
140+
// Basic correctness
141+
// -----------------------------------------------------------------------
142+
143+
/// Classic textbook example from Cormen et al. §16.5.
144+
#[test]
145+
fn test_classic_example() {
146+
let jobs = vec![
147+
Job::new("A", 2, 100),
148+
Job::new("B", 1, 19),
149+
Job::new("C", 2, 27),
150+
Job::new("D", 1, 25),
151+
Job::new("E", 3, 15),
152+
];
153+
// Optimal: A in slot 2, C in slot 1 (or same profit arrangement),
154+
// and E in slot 3 → total = 100 + 27 + 15 = 142.
155+
let result = schedule_jobs(jobs);
156+
assert_eq!(result.total_profit, 142);
157+
assert_eq!(result.job_sequence, vec!["C", "A", "E"]);
158+
}
159+
160+
/// All jobs have the same deadline (1) — only the most profitable fits.
161+
#[test]
162+
fn test_all_same_deadline() {
163+
let jobs = vec![
164+
Job::new("X", 1, 50),
165+
Job::new("Y", 1, 80),
166+
Job::new("Z", 1, 30),
167+
];
168+
let result = schedule_jobs(jobs);
169+
assert_eq!(result, make_result(&["Y"], 80));
170+
}
171+
172+
/// Every job can be scheduled (all deadlines are distinct and large enough).
173+
#[test]
174+
fn test_all_jobs_scheduled() {
175+
let jobs = vec![
176+
Job::new("P", 3, 10),
177+
Job::new("Q", 2, 20),
178+
Job::new("R", 1, 30),
179+
];
180+
// R (profit 30) → slot 1, Q (profit 20) → slot 2, P (profit 10) → slot 3.
181+
let result = schedule_jobs(jobs);
182+
assert_eq!(result, make_result(&["R", "Q", "P"], 60));
183+
}
184+
185+
// -----------------------------------------------------------------------
186+
// Edge cases
187+
// -----------------------------------------------------------------------
188+
189+
#[test]
190+
fn test_empty_input() {
191+
let result = schedule_jobs(vec![]);
192+
assert_eq!(result, make_result(&[], 0));
193+
}
194+
195+
#[test]
196+
fn test_single_job() {
197+
let result = schedule_jobs(vec![Job::new("Solo", 1, 42)]);
198+
assert_eq!(result, make_result(&["Solo"], 42));
199+
}
200+
201+
/// Jobs with equal profit: the algorithm must still produce a valid
202+
/// (though not necessarily unique) schedule with the correct total profit.
203+
#[test]
204+
fn test_equal_profits() {
205+
let jobs = vec![
206+
Job::new("A", 1, 10),
207+
Job::new("B", 2, 10),
208+
Job::new("C", 3, 10),
209+
];
210+
let result = schedule_jobs(jobs);
211+
// All three should be scheduled since their deadlines are distinct.
212+
assert_eq!(result.total_profit, 30);
213+
assert_eq!(result.job_sequence.len(), 3);
214+
}
215+
216+
/// A large deadline value — verifies that the slot array is capped at
217+
/// jobs.len() and no unnecessary allocation occurs.
218+
#[test]
219+
fn test_large_deadline() {
220+
let jobs = vec![Job::new("Big", 100, 500), Job::new("Small", 1, 1)];
221+
let result = schedule_jobs(jobs);
222+
// Both jobs should be scheduled.
223+
assert_eq!(result.total_profit, 501);
224+
assert_eq!(result.job_sequence.len(), 2);
225+
// "Small" is in slot 1, "Big" in slot 2 (capped to num_slots = 2).
226+
assert_eq!(result.job_sequence[0], "Small");
227+
}
228+
229+
/// Zero-profit jobs should still be scheduled if a slot is available,
230+
/// because the greedy criterion is profit and zero is a valid profit.
231+
#[test]
232+
fn test_zero_profit_job() {
233+
let jobs = vec![Job::new("Free", 2, 0), Job::new("Paid", 1, 5)];
234+
let result = schedule_jobs(jobs);
235+
assert_eq!(result.total_profit, 5);
236+
// "Free" should still occupy slot 2 (no conflict).
237+
assert_eq!(result.job_sequence.len(), 2);
238+
}
239+
240+
/// Verify that the returned sequence is in ascending slot order.
241+
#[test]
242+
fn test_output_in_slot_order() {
243+
let jobs = vec![
244+
Job::new("Late", 3, 5),
245+
Job::new("Mid", 2, 10),
246+
Job::new("Early", 1, 15),
247+
];
248+
let result = schedule_jobs(jobs);
249+
assert_eq!(result.job_sequence, vec!["Early", "Mid", "Late"]);
250+
assert_eq!(result.total_profit, 30);
251+
}
252+
253+
/// More jobs than slots — ensure that only as many jobs as there are
254+
/// time-slots can be scheduled.
255+
#[test]
256+
fn test_more_jobs_than_slots() {
257+
// 5 jobs, max deadline 2 → at most 2 can be scheduled.
258+
let jobs = vec![
259+
Job::new("A", 1, 40),
260+
Job::new("B", 2, 30),
261+
Job::new("C", 1, 20),
262+
Job::new("D", 2, 15),
263+
Job::new("E", 1, 10),
264+
];
265+
let result = schedule_jobs(jobs);
266+
assert_eq!(result.total_profit, 70); // A (40) + B (30)
267+
assert_eq!(result.job_sequence.len(), 2);
268+
}
269+
}

src/greedy/mod.rs

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

6+
pub use self::job_sequencing::{schedule_jobs, Job, ScheduleResult};
57
pub use self::minimum_coin_change::find_minimum_change;
68
pub use self::smallest_range::smallest_range;
79
pub use self::stable_matching::stable_matching;

0 commit comments

Comments
 (0)