|
| 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 | +} |
0 commit comments