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$.
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
Trace Moore-Hodgson Regret-Greedy max-heap job eviction, weighted Matroid greedy basis construction, and CDCL SAT unit propagation + 1-UIP conflict clause learning.
Invariant & State
| 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!