Greedy Algorithms for Interval Scheduling and Partitioning
Greedy Algorithms
Choose the most attractive choice at each step, and hope that this will lead to the optimal solution. Proofs of correctness are particularly important for greedy algorithms.
Interval Scheduling
Job $j$ starts at $s(j)$ and finishes at $f(j)$. Two jobs are compatible if they don't overlap. The goal is to schedule as many jobs as possible without overlapping.
Start by sorting the jobs with $f(j)$, and iterate over the jobs in order and choose as many jobs as you can.
def interval_scheduling(jobs):
jobs.sort(key=lambda x: x[1])
last = 0
S = []
for job in jobs:
if job[0] >= last:
S.append(job)
last = job[1]
return S
Greedy Stays Ahead Proof
Suppose the above algorithm has chosen jobs $f(i_1) \le f(i_2) \le \ldots \le f(i_k)$, and suppose $f(j_1) \le f(j_2) \le \ldots \le f(j_m)$.
Goal: $m \le k$
Lemma: $\forall r$, $f(i_r) \le f(j_r)$
Proof: Induction, $P(r) := f(i_r) \le f(j_r)$
Base Case: $P(1)$. $i_1$ has the smallest finishing time.
IH: Assume $P(r - 1)$
IS: Goal $P(r)$
Applying $P(r - 1)$, and using the fact that both sets of jobs chosen are non-overlapping within themselves, we have...
$$ f(i_{r - 1}) \le f(j_{r - 1}) \le s(j_r) $$
So $j_r$ is a candidate for $i_r$. However, greedy chose $i_r$, which implies $f(i_r) \le $f(j_r)$. (induction over)
Now, we want to show that $m \le k$. Suppose for contradiction that $m > k$.
We know $f(i_k) \le f(j_k) \le s(j_{k + 1})$, so greedy could have executed $j_{k + 1}$ after $i_k$, which is a contradiction.
Exchange Argument
Make the optimal solution similar to greedy without changing its value.
This would look like removing $j_1$ from the optimal solution, adding $i_1$ instead. Then this optimal solution still has the same number of jobs, and is thus still optimal, but also shares $i_1$ in common with the greedy solution. Continue this into the general case where the first $k$ jobs are in common, and we can use the previous lemma to show that we can continue this exchange, until the greedy solution becomes the optimal.
Interval Partitioning
Given a set of intervals $I$, partition them into the minimum number of sets $S_1, S_2, \ldots, S_k$ such that each $S_i$ contains no overlapping intervals.
def partition_intervals(I: list[tuple[int, int]]):
# sort by start time
I.sort(key=lambda x: x[0])
d = 0
S = []
for itvl in I:
# if some partition works, add itvl to it
for i, S_i in enumerate(S):
if itvl[0] >= S_i[-1][1]:
S[i].append(itvl)
break
# otherwise, allocate new partition with itvl
else:
S[d] = [itvl]
d += 1
return S
Proof of Correctness
Observation: The algorithm never schedules two incompatible lectures in the same classroom.
Lemma: The algorithm is optimal.
Proof: using structural property
Let $d$ be the number of classrooms the greedy algorithm uses. Classroom $d$ is then allocated because we needed to schedule a job, $j$, that is incompatible with all $d - 1$ previously allocated classrooms.
Since we sorted by start time, all these incompatible jobs must have started before $s(j)$, and thus we have $d$ lectures overlapping at time $s(j) + \epsilon$, so our maximum depth is $\ge d$.
Since we have that the optimal solution must schedule at least depth number of classrooms, we have that the greedy algorithm is optimal.