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