Skip to content
Home DSA Job Sequencing — First-Free-Slot Bug Costs $50k

Job Sequencing — First-Free-Slot Bug Costs $50k

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Greedy & Backtracking → Topic 10 of 13
Assigning jobs to earliest free slot can cause high-profit jobs to miss deadlines, in a $50k penalty.
⚙️ Intermediate — basic DSA knowledge assumed
In this tutorial, you'll learn
Assigning jobs to earliest free slot can cause high-profit jobs to miss deadlines, in a $50k penalty.
  • Sort by profit descending — greedily assign highest-profit jobs first.
  • Assign each job to the latest available slot ≤ its deadline.
  • Naive O(n²); Union-Find optimisation achieves O(n log n).
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • Sort jobs by profit descending: highest profit first.
  • For each job, find the latest free slot ≤ its deadline.
  • Assign job to that slot — keeping earlier slots open for other jobs.
  • Naive O(n²) slot search; Union-Find reduces to near O(n log n).
  • Proven optimal via exchange argument: any optimal schedule can be transformed to this greedy result without reducing profit.
🚨 START HERE

Quick Job Sequencing Troubleshooter

Cheat-sheet commands and checks for debugging job scheduling algorithms.
🟡

Profit too low

Immediate ActionPrint the sorted job list and confirm profits are descending.
Commands
print(sorted(jobs, key=lambda x: x[2], reverse=True))
Check slot-assignment loop: for s in range(deadline, 0, -1):
Fix NowIf you used range(1, deadline+1) instead, reverse it.
🟡

Union-Find returns 0 jobs

Immediate ActionPrint parent array after init — it should be list(range(max_d + 2)).
Commands
print(parent[:10])
Check find() path compression: parent[i] = find(parent, parent[i])
Fix NowSet parent = list(range(max_d + 2)) not max_d + 1.
Production Incident

Missed Deadline in a Cloud Batch Scheduler Cost $50k in Penalties

A batch processing platform scheduled jobs using earliest available slot instead of latest, causing high-value jobs to be skipped despite having deadlines later in the day.
SymptomCritical reports consistently missed their deadlines by hours, while lower-priority batch jobs ran on time. The scheduler never flagged an error.
AssumptionThe team assumed scheduling jobs as early as possible would leave room for urgent jobs later. They didn't account that later deadlines could be ignored if earlier slots were filled.
Root causeThe scheduler assigned each job to the first free slot at or after its deadline — not the latest free slot before its deadline. High-profit jobs with later deadlines were pushed to early slots, blocking themselves and leaving no space for earlier-deadline jobs.
FixReplaced the earliest-available-slot logic with the classic greedy approach: sort by profit descending and assign each job to the latest available slot ≤ its deadline. Added a validation test that checks total profit against a known optimal solution for small input sizes.
Key Lesson
For deadline-based scheduling, always assign jobs to the latest possible slot — not the earliest.Greedy is optimal here, but only when the choice rule and slot selection are both correct.Unit test edge cases: all jobs same deadline, all profits equal, large profit disparity.
Production Debug Guide

Diagnose why your greedy scheduler is producing suboptimal profit or skipping jobs that should fit.

Total profit is lower than expected from manual calculation.Verify sorting: ensure profit is descending. Then check that you're scanning slots from deadline down to 1, not from 1 up to deadline.
All jobs fit but some high-profit ones are missing from output.Check if you're using 1-indexed or 0-indexed time slots. A common bug: using slots[0] as day 1 but forgetting to offset max deadline.
Union-Find optimisation returns fewer jobs than naive version.The parent array must size up to max deadline + 2 to avoid off-by-one. Also confirm find() compresses properly; missing path compression leads to O(n²) anyway.
Scheduler runs very slow for >10k jobs.Upgrade from naive O(n²) to Union-Find O(n log n). Also confirm sort step is not O(n²) — use sorted() or sort() with key, not custom comparison.

Job scheduling with deadlines is a classic operations research problem that appears in OS scheduling, manufacturing planning, and project management. The greedy insight — schedule highest-paying jobs as late as possible within their deadlines — is counterintuitive: you might expect to schedule early, but scheduling late preserves earlier slots for potentially higher-value jobs that might arrive.

The Union-Find optimisation that reduces O(n^2) to O(n log n) is an elegant application of the disjoint set data structure in a non-graph context — a useful demonstration that Union-Find is about more than just connected components.

Greedy Algorithm

Sort jobs by profit descending. For each job, find the latest available slot before its deadline and assign it there.

This is the core greedy choice: take the highest-profit job and defer it as late as possible. Why late? Because early slots are scarce — a job with deadline 1 can only go in slot 1. If you take a low-profit job for slot 1, you block every job with deadline 1. By scheduling high-profit jobs late, you keep earlier slots flexible for jobs that have no choice but to go early.

job_scheduling.py · PYTHON
12345678910111213141516171819202122
def job_scheduling(jobs: list[tuple]) -> tuple[int, list]:
    """
    jobs: list of (job_id, deadline, profit)
    Returns (total_profit, scheduled_job_ids)
    """
    jobs = sorted(jobs, key=lambda x: x[2], reverse=True)
    max_deadline = max(d for _, d, _ in jobs)
    slots = [None] * (max_deadline + 1)  # slots[1..max_deadline]
    total = 0
    scheduled = []
    for job_id, deadline, profit in jobs:
        # Find latest free slot at or before deadline
        for slot in range(deadline, 0, -1):
            if slots[slot] is None:
                slots[slot] = job_id
                total += profit
                scheduled.append(job_id)
                break
    return total, scheduled

jobs = [('a',2,100), ('b',1,19), ('c',2,27), ('d',1,25), ('e',3,15)]
print(job_scheduling(jobs))  # (142, ['a', 'c', 'e'])
▶ Output
(142, ['a', 'c', 'e'])
📊 Production Insight
The naive scanning is O(n) per job, fine for <1000 jobs.
At 10k jobs, this becomes 100M iterations — noticeable latency.
If your scheduler runs every minute, that's unsustainable.
Rule: use naive only for small batches or prototypes.
🎯 Key Takeaway
Greedy choice = highest profit first.
Slot assignment = latest available slot ≤ deadline.
This combination yields maximum total profit.
Proven optimal by exchange argument.

Union-Find Optimisation — O(n log n)

The naive slot-finding is O(n) per job — O(n²) total. Using Union-Find (disjoint set), we can find the latest free slot in near-O(1) amortised time, giving O(n log n) overall.

How it works: treat each time slot as a node in a Union-Find structure. When a slot is used, union it with the slot before it (slot → slot-1). The find operation returns the largest free slot ≤ given deadline. It's essentially a 'next available lower slot' finder, with path compression making almost constant time.

job_scheduling_uf.py · PYTHON
123456789101112131415161718192021
def find(parent, i):
    if parent[i] != i:
        parent[i] = find(parent, parent[i])
    return parent[i]

def job_scheduling_uf(jobs):
    jobs = sorted(jobs, key=lambda x: x[2], reverse=True)
    max_d = max(d for _, d, _ in jobs)
    parent = list(range(max_d + 2))  # extra sentinel for slot 0
    total, scheduled = 0, []
    for job_id, deadline, profit in jobs:
        slot = find(parent, deadline)
        if slot > 0:
            parent[slot] = slot - 1  # union with previous slot
            total += profit
            scheduled.append(job_id)
    return total, scheduled

# Same example yields same result
jobs = [('a',2,100), ('b',1,19), ('c',2,27), ('d',1,25), ('e',3,15)]
print(job_scheduling_uf(jobs))  # (142, ['a', 'c', 'e'])
▶ Output
(142, ['a', 'c', 'e'])
📊 Production Insight
Union-Find may over-allocate memory: parent array of size max_d+2.
If deadlines are sparse (max_d=1,000,000 but only 100 jobs), wasteful.
Alternative: use hash-based disjoint set to track only used slots.
Rule: choose Union-Find when deadlines are dense relative to job count.
🎯 Key Takeaway
Union-Find turns slot search from O(n) to O(α(n)).
α(n) ≤ 4 for any practical input — effectively constant.
Total time dominated by sort: O(n log n).
But only use it when n > ~5000; otherwise overhead negates benefit.

Complexity

Naive: O(n log n) sort + O(n²) slot finding = O(n²) Union-Find: O(n log n) sort + O(n α(n)) slot finding ≈ O(n log n)

For most interview purposes the naive O(n²) implementation is sufficient. But you should understand both — the interviewer may ask how to optimise when n reaches 10⁵.

📊 Production Insight
O(n²) at n=10⁵ is 10¹⁰ operations — minutes of runtime.
O(n log n) at same size takes <0.1 seconds.
If your scheduler runs hourly, O(n²) might be acceptable for n<5000.
Always benchmark your actual n before optimising.
Rule: optimise only when you've measured the bottleneck.
🎯 Key Takeaway
Naive: O(n²) — fine for small n.
Union-Find: O(n log n) — essential for large n.
Sort is the unavoidable O(n log n) step.
Choose based on input size, not theory alone.

Proof of Optimality via Exchange Argument

We need to prove that the greedy algorithm (highest profit first, latest slot) produces maximum total profit. The exchange argument works like this:

  1. Let G be the set of jobs selected by greedy. Let O be an optimal set with maximum total profit.
  2. Compare G and O from highest profit to lowest. At the first point they differ, greedy chose job j (higher profit) while optimal chose job k (lower profit or none).
  3. Since j has higher profit and is scheduled at or after k's slot, we can swap k with j in O without reducing profit and without violating deadlines.
  4. After a finite number of swaps, O becomes identical to G, proving G is optimal.

This argument holds only because the profit-sorted order is monotonic — the greedy never picks a lower-profit job when a higher-profit one could be placed. The 'latest slot' rule ensures we don't block future jobs unnecessarily.

Mental Model
Why 'Latest Slot' Works
Think of deadlines as constraints that tighten toward the past. Scheduling late keeps early slots — the most constrained resource — open for jobs that cannot move.
  • Slot 1 can only fit jobs with deadline ≥ 1 — every job qualifies.
  • Slot 10 can only fit jobs with deadline ≥ 10 — fewer jobs qualify.
  • So slot 1 is the scarcest: fill it last, with a job that absolutely must go there.
  • By scanning from deadline down to 1, you effectively 'push' each job as far right as possible.
📊 Production Insight
Exchange argument is the standard correctness proof for greedy.
Interviewers expect you to outline it when asked 'why is this optimal?'
Real-world scheduler may violate the 'no two jobs same slot' assumption.
If jobs can run concurrently (e.g., multi-core), greedy fails entirely.
Rule: know the assumptions behind the proof — they tell you when not to use it.
🎯 Key Takeaway
Exchange argument shows greedy is optimal.
Proof structure: take first difference → swap → profit doesn't decrease.
Valid only when each job requires exactly one time unit.
If jobs have variable duration, the problem becomes NP-hard.

Variations and Real-World Extensions

The classic job sequencing problem assumes each job takes exactly one time unit. In practice, jobs have varying durations and may depend on each other. Here are common variations:

Weighted Interval Scheduling: Jobs have start/end times and durations. Greedy fails; DP required. Job Sequencing with Release Times: Each job has a release time (earliest start). Greedy still works with slight modification — always pick the job with highest profit among available ones. Multi-Machine Scheduling: Multiple workers/CPUs. NP-hard in general; heuristic approaches used. Minimum Lateness Scheduling: Instead of maximising profit, minimise maximum lateness. Solved by EDD (Earliest Due Date) — a different greedy.

Understanding the classic version provides the foundation for these extensions.

📊 Production Insight
Classic job sequencing rarely appears in raw form in production.
But the 'greedy + union-find' trick shows up in resource allocation problems.
For example, assigning IP addresses from a pool: each allocation has an expiry (deadline) and a 'profit' (priority).
Rule: learn the pattern, not just the problem. It transfers to scheduling slots, memory blocks, time slots.
🎯 Key Takeaway
Classic problem is a special case — unit jobs, single machine.
Real schedulers are usually more complex.
But the greedy + union-find pattern generalises to many 'slot assignment' problems.
Master this, then explore variations.
Which Scheduling Algorithm to Use?
IfAll jobs have unit duration, single machine, maximise profit with deadlines
UseGreedy (this article) — optimal and fast.
IfJobs have start/end times, overlap not allowed, maximise profit
UseWeighted Interval Scheduling — DP (O(n log n)).
IfJobs have processing times and due dates, minimise max lateness
UseEarliest Due Date (EDD) — greedy.
IfMultiple identical machines, jobs have durations, minimise makespan
UseList Scheduling (greedy heuristic) — near optimal; exact is NP-hard.
🗂 Greedy vs Union-Find Optimisation
Trade-offs between the two implementations of Job Sequencing.
CriterionNaive O(n²)Union-Find O(n log n)
Time complexity (after sort)O(n * max_deadline) → O(n²) worstO(n α(n)) ≈ O(n)
Space complexityO(max_deadline)O(max_deadline)
Code complexitySimple loop, no extra DSRequires Union-Find with path compression
Best for n< 5000≥ 10000
Off-by-one riskLow: slots[1..D] easyMedium: parent array size + sentinel
Memorisation difficultyStraightforwardModerate; need to recall parent[] = slot-1

🎯 Key Takeaways

  • Sort by profit descending — greedily assign highest-profit jobs first.
  • Assign each job to the latest available slot ≤ its deadline.
  • Naive O(n²); Union-Find optimisation achieves O(n log n).
  • Exchange argument proves optimality — a core greedy proof technique.
  • Unit time per job is a critical assumption; drop it and the problem changes entirely.
  • This pattern transfers to any slot-based resource allocation under deadlines.

⚠ Common Mistakes to Avoid

    Assigning jobs to earliest available slot instead of latest
    Symptom

    Suboptimal total profit — high-profit jobs with later deadlines get blocked by lower-profit jobs that fit earlier.

    Fix

    Change loop to scan from deadline down to 1: for slot in range(deadline, 0, -1):

    Off-by-one in slot array indexing (0 vs 1 indexed)
    Symptom

    Jobs with deadline = 1 are never scheduled, or IndexOutOfBounds.

    Fix

    Allocate slots of size max_deadline + 1 and ignore index 0, or allocate max_deadline and shift all accesses by -1.

    Using Union-Find but forgetting path compression in find()
    Symptom

    Slot search degrades to O(n) per job, giving O(n²) runtime despite using Union-Find.

    Fix

    Ensure find() sets parent[i] = find(parent, parent[i]) recursively, not just returns parent[i].

    Sorting by deadline instead of profit
    Symptom

    Total profit is far from optimal — often low-profit jobs occupy slots while high-profit ones are skipped.

    Fix

    Sort by profit descending: jobs.sort(key=lambda x: x[2], reverse=True)

    Assuming jobs can be split or run part-time
    Symptom

    Algorithm schedules partial jobs or miscalculates profit.

    Fix

    Explicitly clarify that each job occupies exactly one time slot, indivisible. For fractional jobs, use Fractional Knapsack instead.

Interview Questions on This Topic

  • QWhy do we assign jobs to the latest possible slot rather than earliest?Mid-levelReveal
    Because earlier slots are more constrained: slot 1 can only fit jobs with deadline ≥ 1, but slot 10 can fit any job with deadline ≥ 10. By scheduling a high-profit job as late as possible, we keep the scarce early slots free for jobs that have no later option. This maximises the number of jobs we can fit and hence the total profit. The exchange argument proves optimality.
  • QWhat data structure optimises slot-finding to O(log n)?SeniorReveal
    There's no O(log n) per operation structure that fits this exact problem. The best is Union-Find with path compression and union by rank, which gives amortised O(α(n)) per slot search — essentially constant time. Some candidates mention a balanced BST (like TreeSet in Java) storing free slots; you can do floor(deadline) in O(log n), but the total remains O(n log n) with a higher constant. Union-Find is simpler and faster in practice.
  • QProve that sorting by profit and taking the latest available slot is optimal.SeniorReveal
    Use an exchange argument. Let G be the greedy solution and O an optimal solution with maximum profit. If G ≠ O, consider the highest-profit job where they differ. Greedy picks job j (higher profit) at its latest feasible slot; optimal has job k (lower profit) or none at the same slot or an earlier one. Since j has higher profit, swapping j into O's schedule (removing k if needed) does not reduce total profit and maintains feasibility because j's slot in O is no later than its slot in G. Repeat for all differences — eventually O becomes G. Thus greedy is optimal.
  • QHow would you handle a variant where each job takes X time units instead of 1?SeniorReveal
    That becomes the weighted interval scheduling problem (each job has start, end, profit), which cannot be solved greedily — it requires dynamic programming. The greedy approach fails because choosing a high-profit long job may block multiple short jobs that together yield more profit. The classic job sequencing assumption of unit duration is critical. If you drop it, the problem becomes NP-hard in the general case (bin packing style).
  • QWhat if jobs have release times where they cannot be started before a certain time?SeniorReveal
    Then you need to consider both release time and deadline. The greedy algorithm still works if you sort jobs by profit descending and schedule each job at the latest slot that is ≥ release time and ≤ deadline. But you must also ensure you don't schedule a job before its release time. The Union-Find approach can be adapted by initialising parent only for slots that are ≥ earliest release. Alternatively, you can use a priority queue (max-heap) to process available jobs at each time slot.

Frequently Asked Questions

What if no slot is available for a job?

Skip it — this is the greedy choice. Taking a lower-profit job that fits would only reduce total profit. The algorithm already considered jobs in descending order of profit, so if a high-profit job can't fit, no later job will improve the total by replacing it.

Why does the Union-Find parent array need to be size max_deadline + 2?

Because find() is called with deadline values, and we also need a sentinel at index 0. When find(deadline) returns 0, it means no free slot ≤ deadline exists. So the array must have indices 0..max_deadline inclusive, which is max_deadline + 1 elements. Adding one more (+2 total) is a safety measure; some implementations use find(deadline) and then union with deadline-1, so accessing parent[deadline-1] requires index up to max_deadline-1 only. But the extra element prevents off-by-one errors.

Can this algorithm handle duplicate profits?

Yes. Sorting by profit descending is stable in Python (and most languages) — ties are broken in original order. Since all jobs with equal profit are equivalent, any tie-breaking is fine as long as you apply the 'latest slot' rule. The total profit will be the same regardless.

What is the time complexity if we sort using insertion sort?

Insertion sort is O(n²) worst-case, making the whole algorithm O(n²). Never use insertion sort for the sort step — use the built-in sorted() which is O(n log n). The sort dominates the overall complexity, so it's critical to use an efficient sort.

How does this relate to Earliest Deadline First (EDF) scheduling?

EDF schedules the job with the nearest deadline at each time slot, aiming to meet all deadlines. It minimises maximum lateness but does not maximise profit. The greedy in this article maximises profit, possibly dropping some low-profit jobs even if they could meet their deadlines. They solve different objectives.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousFractional Knapsack Problem — Greedy ApproachNext →Huffman Encoding — Greedy Data Compression
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged