1. When Pure Greedy Fails: Retroactive Replacement via Heaps
Consider the Variable-Length Course Scheduling Problem (Moore–Hodgson Algorithm, 1968 / Course Schedule III): Given $N$ courses, each with duration $t_i$ and deadline $d_i$, maximize the number of courses completed. Unlike Unit-Time Scheduling ($t_i = 1$), variable durations $t_i$ do not form a matroid! If we sort by deadline $d_1 \le d_2 \le \dots \le d_n$ and greedily take course $1$ ($t_1 = 100, d_1 = 100$), we might block courses $2$ and $3$ ($t_2 = 30, d_2 = 110$ and $t_3 = 30, d_3 = 120$).
Core Invariant — Moore–Hodgson Regret Greedy Invariant
Process jobs in increasing order of deadline $d_1 \le d_2 \le \dots \le d_n$, maintaining a max-heap $H$ of durations of currently selected jobs and their total time $T = \sum_{j \in H} t_j$. For each job $i$, optimistically insert $t_i$ into $H$ and set $T \leftarrow T + t_i$. If $T > d_i$ (the schedule overflows $i$'s deadline), immediately evict the longest-duration job $t_{\max} = H.\text{pop()}$ and set $T \leftarrow T - t_{\max}$.
Why this is optimal: After processing prefix $1..i$, $H$ achieves the maximum possible job count $k$ AND simultaneously achieves the minimum possible total completion time $T$ among all valid $k$-job subsets of $1..i$.
2. Virtual Cancellation (Slope Trick / Stock Trading with Unlimited Transactions)
A second, deeper form of Regret Greedy is Virtual Undo (Equivalent to a 1-capacity Successive Shortest Path augmenting path or Slope Trick): Suppose on Day $i$ the stock price is $p_i$, and you want to pair Buy/Sell days to maximize profit (or match tasks to servers). If you buy at $p_1 = 10$, sell at $p_2 = 15$ (profit $+5$), and then see $p_3 = 22$, you regret selling at $15$ instead of $22$.
Observe the telescope identity:
$$(p_2 - p_1) + (p_3 - p_2) = p_3 - p_1$$When we match $p_2$ with $p_1$ to book profit $p_2 - p_1$, if we push $p_2$ back into the min-heap as a "virtual buy option", then later buying that virtual $p_2$ and selling at $p_3$ adds $p_3 - p_2$, which algebraically cancels the intermediate sale at $p_2$ and converts the trade into $(p_1, p_3)$!
Profit += (15 - 10) = +5"| H2["Push 15 twice (1 real, 1 virtual undo)
Min-Heap: [15, 15]"] end subgraph Step2["Day 3: p_3=22"] H2 -->|"Pop virtual 15:
Profit += (22 - 15) = +7"| H3["Total Profit = (15-10) + (22-15)
= 22 - 10 = +12!"] end
Hardware Reality — $O(N \log N)$ Flat Binary Heap vs. $O(N \cdot D)$ Pseudo-Polynomial DP
Solving Variable-Length Deadline Scheduling via Dynamic Programming requires an $O(N \cdot \max d_i)$ table. When deadlines reach $d_i = 10^9$, the DP table requires $8 \text{ GB}$ per row and is completely intractable. Moore-Hodgson's Regret Greedy uses a single std::priority_queue<int> backed by a contiguous std::vector<int> of size $\le N$, solving $N = 10^6, d_i = 10^{18}$ in 45 milliseconds and 4 MB of L3 cache.
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
| Problem Variant | Dynamic Programming | Regret Greedy (Heap) | Heap Space | Mechanism |
|---|---|---|---|---|
| Moore-Hodgson ($1 | d_j | \sum U_j$) | O(N * max D) | O(N log N) | O(N) | Evict max $t_j$ when $\sum t_j > d_i$ |
| Farthest Building / Shielded Jumps ($K$ Ladders) | O(N * K) | O(N log K) | O(K) | Min-heap of $K$ largest climbs |
| Minimum Refueling Stops | O(N^2) | O(N log N) | O(N) | Max-heap of passed fuel stations |
| Buy/Sell Stock Multi-Hold (Slope Trick) | O(N^2) | O(N log N) | O(N) | Telescoping virtual-buy re-insertion |
4. Reference C++20 Implementation
#include <algorithm>
#include <cstdint>
#include <functional>
#include <queue>
#include <vector>
struct Course {
long long duration;
long long deadline;
};
// 1. Moore-Hodgson Algorithm (Course Schedule III): Max courses completed before deadline
int max_courses_moore_hodgson(std::vector<Course> courses) {
std::sort(courses.begin(), courses.end(), [](const Course& a, const Course& b) {
return a.deadline < b.deadline;
});
std::priority_queue<long long> max_durations; // Max-heap of chosen course durations
long long total_time = 0;
for (const auto& c : courses) {
total_time += c.duration;
max_durations.push(c.duration);
if (total_time > c.deadline) {
// Regret Step: Evict the single longest course taken so far (might be c itself!)
total_time -= max_durations.top();
max_durations.pop();
}
}
return static_cast<int>(max_durations.size());
}
// 2. Virtual Undo (Telescoping Heap): Buy/Sell at most 1 share per day, hold unlimited shares
long long max_profit_multi_share_trading(const std::vector<long long>& prices) {
// Min-heap of available buy prices (both unused days and virtual undo options)
std::priority_queue<long long, std::vector<long long>, std::greater<long long>> min_heap;
long long total_profit = 0;
for (long long p : prices) {
if (!min_heap.empty() && min_heap.top() < p) {
total_profit += (p - min_heap.top());
min_heap.pop();
// Push p TWICE:
// 1st copy: allows cancelling the sale at p if a higher price p' > p appears later:
// (p - buy) + (p' - p) = p' - buy
// 2nd copy: allows buying a brand new share on this day at price p
min_heap.push(p);
}
min_heap.push(p);
}
return total_profit;
}
Production Pitfall — Assuming Regret Greedy Works When Both Weights AND Profits Vary
In Moore-Hodgson's algorithm, every job has unit profit ($1$ course = $+1$ count) and variable duration $t_i$, so evicting the maximum $t_j$ strictly dominates any other single-job eviction. If jobs have both arbitrary durations $t_i$ and arbitrary profits $v_i$, a single eviction in a priority queue is no longer sufficient because trading one large high-profit job for two small medium-profit jobs is the 0/1 Knapsack problem (NP-hard)!