Sort Events, Then Choose
Start with unsorted festival events. Learn two sorting algorithms, use the sorted order to find the highest-scoring schedule, then apply the same take-or-skip idea to a 0/1 backpack.
Core worksheet: TODO 1–8. Coin change is an optional challenge at the end. Every template is on this page with a Copy button. Jump to the full Python file
1 · Sort the events
The events arrive as A,D,B,E,C,F. Before planning, put them in order from earliest finish to latest finish. The order you produce will become the scheduler's input.
SORT LAB · INSERTION SORT
Put festival events in finish-time order
Watch one completed pass at a time. Open the detailed view to see every comparison and shift.
A
09:00–12:00
end 12:00
D
10:30–11:00
end 11:00
B
09:30–10:00
end 10:00
E
11:00–11:30
end 11:30
C
10:00–10:30
end 10:30
F
12:00–13:00
end 13:00
The events arrive out of order. A one-event prefix is already sorted.
Before each pass, the left part is sorted. Move later-finishing events right, then place the new event in the gap. After the pass, the sorted part is one event longer.
The faster method: merge sort
Tutorial 9 introduced divide and conquer. Here we apply it to the same six events: split the list, sort each half, then merge the sorted halves. Review Tutorial 9
SORT LAB · MERGE SORT
Split, sort, merge
The same six events, now sorted through divide and conquer.
Start with six unsorted events.
Step 1 of 6
Both halves are already sorted. The smaller front must be the smallest remaining event, so compare only the two fronts.
Keep the original order: insertion sort shifts only on >; merge sort takes from the left on <=. This is called stable sorting.
| Method | What repeats? | Worst-case time |
|---|---|---|
| Insertion | Grow a sorted prefix | O(n²) |
| Merge | Split, then merge sorted halves | O(n log n) |
Implement both sorts by finish time. Return a new list; do not change the input.
python3 festival_dp.py sortTODO 1–2 · Copy the template
def insertion_sort_events(events): """Return a new list sorted by finish time; preserve ties' input order.""" # TODO 1: copy events. For each key, shift later-ending events right. # Only shift when end > key.end, never when equal. passdef merge_sort_events(events): """Return a new, stable finish-time-sorted list in O(n log n).""" # TODO 2: split, recursively sort both halves, then merge. # On equal finish times, take from the LEFT half first. pass2 · Use the sorted order to plan
Your sort produced B,C,D,E,A,F. Tutorial 9's earliest-finish rule chooses the most compatible events. Now every event has points, and we want the highest total score instead.
B+C+D+E+F = 130
A+F = 150
For each event, compare two complete choices: skip it, or take it with the best compatible earlier schedule. Watch what happens when A arrives.
LEVEL 2 · SYDNEY FESTIVAL
More events is not always more fun
Events are sorted by finish time. Choose a non-overlapping schedule with the most points.
09:30–10:00
p(1) = 0
10:00–10:30
p(2) = 1
10:30–11:00
p(3) = 2
11:00–11:30
p(4) = 3
09:00–12:00
p(5) = 0
12:00–13:00
p(6) = 5
best[0] · selected schedule
0 points · —
Skip current event
—
Take current event
—
Base case: with no events, the best score is 0.
best[j] = max(skip, take)
skip = the best score before event j. take = j's points plus the best score ending before j starts.
To find that earlier score, p(j) names the last event ending by j's start. F starts at 12:00; A ends at 12:00, so they can both be selected. Finish times are sorted, so binary search can find p(j).
To recover the actual plan, start at the last cell: taking F gives 50+100=150, so keep F. Before F, taking A gives 100 instead of the earlier 80, so keep A too.
See the filled table and Python indexes
# finish-time order: B, C, D, E, A, F# previous indexes: -1, 0, 1, 2, -1, 4# best scores: 0, 20, 40, 60, 80, 100, 150# Python event index j is zero-based.# take = events[j][3] + best[previous[j] + 1].Find p(j), fill the best-score table, then walk backward to recover A and F. The checker uses your merge_sort_events result as its input.
python3 festival_dp.py scheduleTODO 3–5 · Copy the template
def previous_compatible(events): """For finish-time-sorted events, return p[j] for every j. p[j] is the last earlier index whose event ends by events[j]'s start. Return -1 when there is no compatible earlier event. """ # TODO 3: binary-search only indexes before j. Equality is compatible. passdef schedule_table(events, previous): """best[j] is the greatest score using the first j events.""" # TODO 4: best[0] = 0. For each event compare skip and take. # take = points + best[previous[j] + 1] when j is zero-based. passdef schedule_plan(events, previous, best): """Return an optimal compatible list of event names in time order.""" # TODO 5: walk backward through best; jump to p(j) after taking j. pass3 · Move from events to a backpack
Scheduling asked 'take or skip this event?' The backpack asks the same question for each item, but adds a limit: 7 kg of capacity. Each item may be taken once.
LEVEL 3 · FESTIVAL BACKPACK
Watch the best plan change
Consider C, D, E, A, B in that order. Each item can be taken once; compare 7 kg and 5 kg backpacks.
C
Jacket
3 kg · 8 pts
D
Power bank
2 kg · 7 pts
E
Souvenir
5 kg · 13 pts
A
Camera
4 kg · 12 pts
B
Snacks
3 kg · 9 pts
dp[0][7] · selected items
0 points · 0/7 kg · —
| Items | 0 kg | 1 kg | 2 kg | 3 kg | 4 kg | 5 kg | 6 kg | 7 kg |
|---|---|---|---|---|---|---|---|---|
| — | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Example 1 · 7 kg: update or keep
- After C and D: C+D uses 5 kg for 15 points.
- After E: D+E uses 7 kg for 20 points, replacing C+D.
- After A: C+A also gives 20, so keep D+E on this tie.
- After B: A+B uses 7 kg for 21 points, becoming the new best plan.
Example 2 · 5 kg: a different answer
Click “Compare 5 kg” above. C+D gives 15. E alone gives 13; A alone gives 12, so neither improves it. When B arrives, D+B fits exactly and gives 7+9=16. The best plan is D+B.
The same items need different decisions at different capacities. That is why the table has a column for every capacity.
The last item B shows the rule: at 7 kg, skip = 20 while take = 9 + K[4][4] = 21; at 5 kg, skip = 15 while take = 9 + K[4][2] = 16. The capacity changes which earlier cell we use.
K[i][w] = max(skip, take)
K[i][w] means: best value using the first i items with capacity w. Skip reads K[i−1][w]. If item i fits, take adds its value to K[i−1][w−weight].
Both choices read the previous row, so item i cannot be used twice.
To recover the items, compare a cell with the cell directly above it. If the value increased, this item was taken; subtract its weight and move to the previous row.
Fill the 0/1 table, recover A+B (21 points), and reject duplicate or overweight plans.
python3 festival_dp.py bagTODO 6–8 · Copy the template
def knapsack_table(items, capacity): """table[i][w] is the greatest value from the first i items in w kg.""" # TODO 6: make (len(items)+1) rows and (capacity+1) columns. # On taking an item, read from the PREVIOUS row: each item is 0/1. passdef knapsack_plan(items, capacity, table): """Recover one optimal list of item names from a completed table.""" # TODO 7: compare table[i][w] with table[i-1][w]. # If item i was taken, reduce w by its weight and move to row i-1. passdef valid_bag(items, chosen_names, capacity): """Return True only when a plan uses each item at most once and fits.""" # TODO 8: check names, duplicates, and total weight. passCopy or download the complete file
The complete file below includes every function template and every check. Click Copy once, paste into festival_dp.py, and complete TODO 1–8 in order. You can also download the same file.
TODO 1–2
python3 festival_dp.py sort
Sort: B,C,D,E,A,F. Passed.
TODO 3–5
python3 festival_dp.py schedule
Festival: 150. Passed.
TODO 6–8
python3 festival_dp.py bag
Backpack: 7 kg = 21; 5 kg = 16. Passed.
When those three pass, run python3 festival_dp.py. The last line should say 'All core checks passed.'
festival_dp.py · complete student template with checks
# festival_dp.py — core TODO 1–8; optional coin challenge TODO 9–10.# An event is (name, start, end, points).# An item is (name, weight, value). Each item may be used ONCE.def insertion_sort_events(events): """Return a new list sorted by finish time; preserve ties' input order.""" # TODO 1: copy events. For each key, shift later-ending events right. # Only shift when end > key.end, never when equal. passdef merge_sort_events(events): """Return a new, stable finish-time-sorted list in O(n log n).""" # TODO 2: split, recursively sort both halves, then merge. # On equal finish times, take from the LEFT half first. passdef previous_compatible(events): """For finish-time-sorted events, return p[j] for every j. p[j] is the last earlier index whose event ends by events[j]'s start. Return -1 when there is no compatible earlier event. """ # TODO 3: binary-search only indexes before j. Equality is compatible. passdef schedule_table(events, previous): """best[j] is the greatest score using the first j events.""" # TODO 4: best[0] = 0. For each event compare skip and take. # take = points + best[previous[j] + 1] when j is zero-based. passdef schedule_plan(events, previous, best): """Return an optimal compatible list of event names in time order.""" # TODO 5: walk backward through best; jump to p(j) after taking j. passdef knapsack_table(items, capacity): """table[i][w] is the greatest value from the first i items in w kg.""" # TODO 6: make (len(items)+1) rows and (capacity+1) columns. # On taking an item, read from the PREVIOUS row: each item is 0/1. passdef knapsack_plan(items, capacity, table): """Recover one optimal list of item names from a completed table.""" # TODO 7: compare table[i][w] with table[i-1][w]. # If item i was taken, reduce w by its weight and move to row i-1. passdef valid_bag(items, chosen_names, capacity): """Return True only when a plan uses each item at most once and fits.""" # TODO 8: check names, duplicates, and total weight. passdef coin_table(amount, coins): """dp[a] is the fewest coins needed for amount a, for 0 <= a <= amount. Assume coin 1 exists, so every amount is reachable. """ # TODO 9: base case dp[0] = 0; try every usable coin for each amount. passdef coin_plan(amount, coins, dp): """Recover one minimum-coin plan from a completed table.""" # TODO 10: find a coin c satisfying dp[amount] == 1 + dp[amount-c]. # Append c, reduce amount, repeat until amount is zero. pass# Checks — keep these below your functions.# Checks: run one stage at a time, for example: python3 festival_dp.py sortimport sysEVENTS = [ ("A", 9.0, 12.0, 100), ("D", 10.5, 11.0, 20), ("B", 9.5, 10.0, 20), ("E", 11.0, 11.5, 20), ("C", 10.0, 10.5, 20), ("F", 12.0, 13.0, 50),]ITEMS = [ ("C", 3, 8), ("D", 2, 7), ("E", 5, 13), ("A", 4, 12), ("B", 3, 9),]def check_coins(): dp = coin_table(6, [1, 3, 4]) assert dp == [0, 1, 2, 1, 1, 2, 2] assert coin_plan(6, [1, 3, 4], dp) == [3, 3] print("Coins: 2. Passed.")def check_sort(): original = EVENTS[:] assert insertion_sort_events([]) == [] assert merge_sort_events([]) == [] ties = [("X", 8, 10, 1), ("Y", 9, 10, 2), ("Z", 8, 9, 3)] assert [e[0] for e in insertion_sort_events(ties)] == ["Z", "X", "Y"] assert [e[0] for e in merge_sort_events(ties)] == ["Z", "X", "Y"] ordered = merge_sort_events(EVENTS) assert insertion_sort_events(EVENTS) == ordered assert [e[0] for e in ordered] == ["B", "C", "D", "E", "A", "F"] assert EVENTS == original print("Sort: B,C,D,E,A,F. Passed.")def check_schedule(): original = EVENTS[:] ordered = merge_sort_events(EVENTS) previous = previous_compatible(ordered) assert previous == [-1, 0, 1, 2, -1, 4] best = schedule_table(ordered, previous) assert best == [0, 20, 40, 60, 80, 100, 150] assert schedule_plan(ordered, previous, best) == ["A", "F"] assert EVENTS == original print("Festival: 150. Passed.")def check_bag(): original = ITEMS[:] table = knapsack_table(ITEMS, 7) assert len(table) == len(ITEMS) + 1 assert all(len(row) == 8 for row in table) assert [row[7] for row in table] == [0, 8, 15, 20, 20, 21] assert table[-1][7] == 21 assert knapsack_plan(ITEMS, 7, table) == ["A", "B"] assert table[-1][5] == 16 assert knapsack_plan(ITEMS, 5, table) == ["D", "B"] assert valid_bag(ITEMS, ["A", "B"], 7) assert not valid_bag(ITEMS, ["A", "A"], 7) assert not valid_bag(ITEMS, ["missing"], 7) assert not valid_bag(ITEMS, ["A", "E"], 7) assert ITEMS == original print("Backpack: 7 kg = 21; 5 kg = 16. Passed.")CHECKS = {"sort": check_sort, "schedule": check_schedule, "bag": check_bag, "coins": check_coins}if __name__ == "__main__": stage = sys.argv[1] if len(sys.argv) > 1 else "all" if stage == "all": for name in ("sort", "schedule", "bag"): CHECKS[name]() print("All core checks passed.") elif stage in CHECKS: CHECKS[stage]() else: print("Choose: sort, schedule, bag, coins, or all.") sys.exit(2)Optional challenge · coin change (TODO 9–10)
With coins 1, 3, 4, taking the largest first makes 6 as 4+1+1. Try every first coin instead; the best answer is 3+3.
LEVEL 1 · CHANGE FOR 6
A quick choice can be wrong
Coins 1, 3, 4 are unlimited. Find the fewest coins needed for every amount from 0 to 6.
Largest coin first
4 + 1 + 1 = 6
3 coins
Best answer
3 + 3 = 6
2 coins
Instead of committing to a coin immediately, solve each smaller remaining amount. For amount a, try every usable coin c and take the smallest value of 1 + dp[a − c].
dp[0]
0
dp[1]
?
dp[2]
?
dp[3]
?
dp[4]
?
dp[5]
?
dp[6]
?
dp[0] = 0; dp[a] = min(1 + dp[a − c]) for every usable coin c. Fill from 0 upward, then recover the chosen coins.
python3 festival_dp.py coinsTODO 9–10 · Copy the template
def coin_table(amount, coins): """dp[a] is the fewest coins needed for amount a, for 0 <= a <= amount. Assume coin 1 exists, so every amount is reachable. """ # TODO 9: base case dp[0] = 0; try every usable coin for each amount. passdef coin_plan(amount, coins, dp): """Recover one minimum-coin plan from a completed table.""" # TODO 10: find a coin c satisfying dp[amount] == 1 + dp[amount-c]. # Append c, reduce amount, repeat until amount is zero. passBefore you leave
- What stays sorted after each insertion pass? Why can merge sort compare only two fronts?
- Why can the sorted event order support p(j), and why does earliest-finish greedy miss the highest score?
- Why does taking an item in 0/1 knapsack read the previous row?
Teacher notes and three-hour plan
00:00–00:10
Why sort?Identify finish time as the key needed by the scheduler.
00:10–00:35
Insertion sortGrow a sorted prefix, predict shifts, code and check TODO 1.
00:35–01:05
Merge sortSplit, sort, merge; code and check TODO 2, then compare costs.
01:05–01:15
BreakLeave the sorted event order on screen.
01:15–01:35
From sort to scheduleRevisit earliest-finish greedy and change the goal to total points.
01:35–02:15
Weighted scheduleFind p(j), compare take/skip, fill best and reconstruct A,F.
02:15–02:45
0/1 backpackTransfer take/skip to the two-dimensional capacity table.
02:45–02:55
Run core checksCheck sort, schedule and bag independently, then run all core checks.
02:55–03:00
Exit ticketExplain the two sorting invariants and the backpack previous-row rule.
Pause before each animation step and ask for a prediction. The core worksheet has eight TODOs; coin change is an optional two-function challenge.