Skip to content

Commit 8433123

Browse files
feat: add job sequencing algorithm
1 parent 3bccfa8 commit 8433123

3 files changed

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

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)