homework.wenqian.dev
< Back to index
Tutorial 102026-10-22

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.

First predict D ends at 11:00; A ends at 12:00. When inserting D, which one should come first?

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.

Step 0 of 5A → D → B → E → C → F
Insertion sort's rule

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.

A12:00
D11:00
B10:00
E11:30
C10:30
F13:00

Start with six unsorted events.

Step 1 of 6

Why merging works

Both halves are already sorted. The smaller front must be the smallest remaining event, so compare only the two fronts.

When finish times tie

Keep the original order: insertion sort shifts only on >; merge sort takes from the left on <=. This is called stable sorting.

MethodWhat repeats?Worst-case time
InsertionGrow a sorted prefixO(n²)
MergeSplit, then merge sorted halvesO(n log n)
Write and check · TODO 1–2

Implement both sorts by finish time. Return a new list; do not change the input.

python3 festival_dp.py sort

TODO 1–2 · Copy the template

python
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.    pass

2 · 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.

Earliest finish

B+C+D+E+F = 130

Better choice

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.

B+20

09:30–10:00

p(1) = 0

C+20

10:00–10:30

p(2) = 1

D+20

10:30–11:00

p(3) = 2

E+20

11:00–11:30

p(4) = 3

A+100

09:00–12:00

p(5) = 0

F+50

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.

The schedule rule

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
python
# 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].
Write and check · TODO 3–5

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 schedule

TODO 3–5 · Copy the template

python
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.    pass

3 · 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.

First predict With 7 kg, jacket C (3 kg/8 points) and power bank D (2 kg/7 points) make 15 points. Souvenir E is 5 kg/13 points. Should we keep C+D, or can E pair with something better? Step through the items to check.

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 · —

Items0 kg1 kg2 kg3 kg4 kg5 kg6 kg7 kg
—00000000
Base case: with zero items, every capacity has value 0. Click Next item to see the answer change.

Example 1 · 7 kg: update or keep

  1. After C and D: C+D uses 5 kg for 15 points.
  2. After E: D+E uses 7 kg for 20 points, replacing C+D.
  3. After A: C+A also gives 20, so keep D+E on this tie.
  4. 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.

The backpack rule

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.

Write and check · TODO 6–8

Fill the 0/1 table, recover A+B (21 points), and reject duplicate or overweight plans.

python3 festival_dp.py bag

TODO 6–8 · Copy the template

python
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.    pass

Copy 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

python
# 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]

?

Base case: dp[0] = 0. No money needs no coins.

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 coins

TODO 9–10 · Copy the template

python
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.    pass

Before you leave

  1. What stays sorted after each insertion pass? Why can merge sort compare only two fronts?
  2. Why can the sorted event order support p(j), and why does earliest-finish greedy miss the highest score?
  3. 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 sort

Grow a sorted prefix, predict shifts, code and check TODO 1.

00:35–01:05

Merge sort

Split, sort, merge; code and check TODO 2, then compare costs.

01:05–01:15

Break

Leave the sorted event order on screen.

01:15–01:35

From sort to schedule

Revisit earliest-finish greedy and change the goal to total points.

01:35–02:15

Weighted schedule

Find p(j), compare take/skip, fill best and reconstruct A,F.

02:15–02:45

0/1 backpack

Transfer take/skip to the two-dimensional capacity table.

02:45–02:55

Run core checks

Check sort, schedule and bag independently, then run all core checks.

02:55–03:00

Exit ticket

Explain 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.