DSA DSA In Depth
0%
lesson greedyexchange-argumentproofs

Proving Greedy Correctness: Greedy Stays Ahead & Exchange Arguments (Interval Scheduling, Minimizing Lateness)

7 min read Module 6.1: Recursion, Backtracking, Pruning & Greedy Exchange Arguments Updated Oct 23, 2026
📄 Cheat Sheet
Greedy Method — Introduction, Choice Property & Exchange Arguments
Abdul Bari Greedy Method — Introduction, Choice Property & Exchange Arguments Watch on YouTube ↗

Rigorous derivation of the Earliest Finish Time (Greedy Stays Ahead) and Earliest Deadline First (Exchange Argument) correctness proofs.

0:00 / --:--

1. Proof Paradigm I: "Greedy Stays Ahead" (Unweighted Interval Scheduling)

Given $n$ intervals $[s_i, f_i)$, select a maximum-size subset of mutually compatible intervals ($f_i \le s_j$ or $f_j \le s_i$). Plausible heuristics like Earliest Start Time, Shortest Duration ($f_i - s_i$), or Fewest Conflicts all fail on simple counterexamples. The unique simple rule that works is Earliest Finish Time (EFT): sort intervals by $f_1 \le f_2 \le \dots \le f_n$ and greedily pick the next compatible interval.

Core Invariant — Greedy Stays Ahead Lemma

Let $G = (i_1, i_2, \dots, i_k)$ be the intervals chosen by EFT, and let $O = (j_1, j_2, \dots, j_m)$ be any mutually compatible set of intervals ordered chronologically. Then for every index $r \le \min(k, m)$:

$$f(i_r) \le f(j_r)$$

Proof by Induction: For $r = 1$, $i_1$ has the minimum finish time among all intervals, so $f(i_1) \le f(j_1)$. Assuming $f(i_{r-1}) \le f(j_{r-1})$, since $O$ is compatible we know $f(j_{r-1}) \le s(j_r)$, which implies $f(i_{r-1}) \le s(j_r)$. Thus $j_r$ is in the candidate pool of compatible intervals available when EFT chooses $i_r$. Since EFT picks the candidate with minimum finish time, $f(i_r) \le f(j_r)$. If $m > k$, then $f(i_k) \le f(j_k) \le s(j_{k+1})$, meaning EFT would have also picked $j_{k+1}$—contradiction! Hence $k = m$.

2. Proof Paradigm II: Adjacent Exchange Argument (Minimizing Maximum Lateness & Smith's Rule)

When every item must be scheduled (a permutation problem), we use a Local Exchange Argument:

  • Minimizing Maximum Lateness ($L_{\max} = \max_i \max(0, C_i - d_i)$): Each job $i$ has processing time $t_i > 0$ and deadline $d_i$. Earliest Deadline First (EDF) sorts jobs by $d_1 \le d_2 \le \dots \le d_n$. Suppose an optimal schedule $O^*$ has an inversion ($d_i > d_j$, yet $i$ is scheduled immediately before $j$). Swapping adjacent jobs $i$ and $j$ leaves all other jobs unchanged, moves $j$ earlier (reducing $L_j$), and finishes $i$ at $j$'s old completion time $C'_i = C_j$. Since $d_i > d_j$, the new lateness $C_j - d_i < C_j - d_j \le L_{\max}(O^*)$. Thus swapping adjacent inversions never increases $L_{\max}$!
  • Weighted Completion Time ($\sum_i w_i C_i$, Smith's Rule): Sort jobs by $w_i / t_i$ descending (i.e., $w_i t_j > w_j t_i$). Swapping an adjacent pair $(i, j)$ changes the total objective by $\Delta = w_i t_j - w_j t_i$, so an optimal schedule cannot contain any adjacent pair with $w_i / t_i < w_j / t_j$.
flowchart LR subgraph Before["Before Swap (Inversion: d_i > d_j)"] B1["Job i (t_i, d_i)
Finishes at t_0 + t_i"] --> B2["Job j (t_j, d_j)
Finishes at C_j = t_0 + t_i + t_j"] end subgraph After["After Adjacent Swap (Inversion Removed)"] A1["Job j (t_j, d_j)
Finishes earlier at t_0 + t_j"] --> A2["Job i (t_i, d_i)
Finishes at C'_i = C_j (L'_i = C_j - d_i < C_j - d_j)"] end Before ==>|"Swap (i, j): max lateness cannot increase"| After

Hardware Reality — Cross-Multiplication vs. Floating-Point Division in Comparators

When sorting by ratio $w_i / t_i$ (Smith's Rule) or $a_i / b_i$ in C++, writing (double)a.w / a.t > (double)b.w / b.t suffers from two bugs: hardware floating-point division takes 10–14 cycles (vs. 3 cycles for 64-bit integer IMUL), and for 64-bit integers $> 2^{53}$, IEEE-754 double loses precision, which can violate strict weak ordering ($a < b$ and $b < c$ but $a \not< c$) and cause std::sort to segfault out-of-bounds! Always compare via 64-bit or 128-bit integer cross-multiplication: 1LL * a.w * b.t > 1LL * b.w * a.t.

3. Complexity Matrix

Regret-Greedy Heap Rollback, Matroid Basis & CDCL SAT Implication Graph
O(N log N) Regret / 1-UIP CDCL

Trace Moore-Hodgson Regret-Greedy max-heap job eviction, weighted Matroid greedy basis construction, and CDCL SAT unit propagation + 1-UIP conflict clause learning.

Step 1 / 8
Custom Input:
Timeline

Invariant & State

Moore-Hodgson Regret Invariant: Accepting J2 pushes total time to 4+3=7 > d2=6. Pop max-duration job J1 (p=4) from heap to restore feasibility!
🌳 Live 2D Hierarchical Tree Topology — Regret-Greedy Heap Rollback, Matroid Basis & CDCL SAT Implication Graph Visited/Active Pivot/Root Resolved
Max p=4 J1 (p… [0] Total ti J2 (p… [1] Next J3 (p… [2]
[0]J1 (p=4, d=4)Max p=4 in Heap
[1]J2 (p=3, d=6)Total time=7 > d=6!
[2]J3 (p=2, d=7)Next
Before Eviction
Time = 7 > Deadline 6
Max-Heap Eviction
Drop J1 (p=4)
After Eviction
Time = 3 ≤ 6 (Feasible)
[Step 1] Deadline overflow at J2; max-heap evicts longest accepted job J1 (p=4), reducing total time from 7 to 3.
Greedy Scheduling Problem Greedy Sorting Criterion Proof Technique Time Complexity Space Complexity
Maximum Disjoint Intervals Earliest Finish Time ($f_i \uparrow$) Greedy Stays Ahead O(N log N) O(1)
Interval Partitioning (Min Rooms) Earliest Start Time ($s_i \uparrow$) Structural Lower Bound (Depth) O(N log N) O(N)
Minimizing Maximum Lateness Earliest Deadline First ($d_i \uparrow$) Adjacent Exchange Argument O(N log N) O(1)
Weighted Completion Time ($\sum w_i C_i$) Smith's Ratio ($w_i / t_i \downarrow$) Adjacent Exchange Cost $\Delta$ O(N log N) O(1)

4. Reference C++20 Implementation

#include <algorithm>
#include <cstdint>
#include <vector>

struct Interval {
    long long start;
    long long finish;
};

// 1. Maximum Disjoint Intervals via Earliest Finish Time (Greedy Stays Ahead)
int max_disjoint_intervals(std::vector<Interval> intervals) {
    if (intervals.empty()) return 0;
    std::sort(intervals.begin(), intervals.end(), [](const Interval& a, const Interval& b) {
        return a.finish < b.finish;
    });

    int count = 1;
    long long last_finish = intervals[0].finish;
    for (std::size_t i = 1; i < intervals.size(); ++i) {
        if (intervals[i].start >= last_finish) {
            ++count;
            last_finish = intervals[i].finish;
        }
    }
    return count;
}

struct Job {
    long long processing_time;
    long long deadline;
    long long weight;
};

// 2. Minimize Maximum Lateness via Earliest Deadline First (Exchange Argument)
long long minimize_max_lateness(std::vector<Job> jobs) {
    std::sort(jobs.begin(), jobs.end(), [](const Job& a, const Job& b) {
        return a.deadline < b.deadline;
    });
    long long current_time = 0;
    long long max_lateness = 0;
    for (const auto& job : jobs) {
        current_time += job.processing_time;
        max_lateness = std::max(max_lateness, current_time - job.deadline);
    }
    return max_lateness;
}

// 3. Minimize Weighted Sum of Completion Times via Smith's Rule (w_i / t_i descending)
long long minimize_weighted_completion_time(std::vector<Job> jobs) {
    std::sort(jobs.begin(), jobs.end(), [](const Job& a, const Job& b) {
        return static_cast<__int128>(a.weight) * b.processing_time >
               static_cast<__int128>(b.weight) * a.processing_time;
    });
    long long current_time = 0;
    long long total_weighted_cost = 0;
    for (const auto& job : jobs) {
        current_time += job.processing_time;
        total_weighted_cost += job.weight * current_time;
    }
    return total_weighted_cost;
}

Production Pitfall — Non-Transitive Custom Comparators & Weighted Intervals

When devising an exchange comparator between two items $i$ and $j$ (such as monsters with damage/healing or tower bricks with weight/strength), always verify that your pairwise preference relation $i \prec j$ is transitive! If $i \prec j$ and $j \prec k$ does not imply $i \prec k$, std::sort exhibits undefined behavior. Also remember: the moment intervals carry weights (Weighted Interval Scheduling) or jobs have release times $r_i$ in $\min L_{\max}$, greedy fails completely—Weighted Interval Scheduling requires $O(N \log N)$ Dynamic Programming, and $1 | r_j | L_{\max}$ is strongly NP-hard!

Check yourself Question 1 of 4
Concept Check Interval Scheduling Greedy Criterion recall

Which greedy sorting criterion is guaranteed to find a maximum-cardinality subset of mutually non-overlapping intervals $[s_i, f_i)$?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

In C++, why can passing a comparator `[](const Job& a, const Job& b) { return a.w * b.t >= b.w * a.t; }` (using `>=` instead of `>`) to `std::sort` cause out-of-bounds memory access and segmentation faults?

Key Takeaways

Always test greedy intuition by searching for a 3-element counterexample or proving optimality via Greedy Stays Ahead or an Exchange Argument.
Earliest Finish Time (EFT) solves unweighted interval scheduling in O(N log N) because the k-th greedy interval finishes no later than the k-th interval of any valid schedule.
Adjacent exchange arguments prove permutation schedules optimal by showing that removing an inversion between adjacent items i and j never degrades the objective.
Use 64-bit or 128-bit integer cross-multiplication instead of floating-point division inside std::sort comparators to preserve strict weak ordering.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue