Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Greedy, Backtracking & Dynamic Programming  ›  Capstone Project

Capstone — Festival Planner

50 minRecognise which technique a problem needs, and compose them
You're building a piece ofTunebox — festival planner
This piece — plan_festival(): Plans a festival lineup using every technique in the section.
Scenario Tunebox Festival has one night, two stages and a curfew. The planner fills the main stage for the biggest crowd, gives the side stage to as many of the remaining acts as fit, and lists every encore mix that runs exactly to curfew.
Your task
Build plan_festival(acts, crate, curfew). Acts are [name, start, end, fans] with unique names, in minutes after doors open; crate tracks are [title, minutes]. Return {"main_stage", "fans", "side_stage", "encore", "encore_mixes"}. main_stage: the non-overlapping acts with the most total fans, in play order (sort by end time with ties in input order; walking back from the last act, an act plays only if it strictly beats the best lineup without it). fans: their total. side_stage: from the acts NOT on the main stage, the most non-overlapping acts by earliest finish. encore: curfew minus the end of the main stage's last act (or curfew if the main stage is empty), never below 0. encore_mixes: every combination of crate tracks filling the encore exactly, tracks in crate order and mixes ordered by position; [] when encore is 0. Back-to-back acts don't overlap. Example: plan_festival([["Nova", 0, 60, 300], ["Kade", 30, 120, 800], ["Luma", 60, 150, 400], ["Rex", 120, 180, 350], ["Zed", 150, 200, 500]], [["Aurora", 4], ["Blink", 3], ["Cobalt", 2], ["Drift", 5], ["Echo", 1]], 207) → {"main_stage": ["Kade", "Zed"], "fans": 1300, "side_stage": ["Nova", "Luma"], "encore": 7, "encore_mixes": [["Aurora", "Blink"], ["Aurora", "Cobalt", "Echo"], ["Cobalt", "Drift"]]}.

Capstone — Festival Planner

Tunebox Festival is one night with a main stage, a side stage and a hard curfew. Planning it means three decisions, and each one is a different shape of problem. The skill this capstone tests isn't any single algorithm. It's recognising which technique a problem is asking for, then wiring the pieces together so each one's output feeds the next.

Three questions, three shapes

Every act applies with [name, start, end, fans], in minutes after the doors open. The house DJ's crate holds [title, minutes] tracks.

Question Shape Technique
Which acts go on the main stage to draw the most fans? non-overlapping intervals, maximise a weight dynamic programming
Which leftover acts go on the side stage, as many as possible? non-overlapping intervals, maximise the count greedy, earliest finish
Which crate tracks fill the encore gap to curfew exactly? list every exact combination backtracking

The first two rows look almost identical, and that's the trap.

Why greedy breaks once acts have weights

Earliest-finish greedy books the most acts. It says nothing about fans:

python

Three acts, 300 + 400 + 500 = 1200 fans. But Kade and Zed alone draw 1300. The exchange argument that proved earliest-finish swaps one act for another. That keeps the count the same, not the fans, so the proof says nothing about weights. Maximising weight means weighing every act against everything it excludes, and that's a DP.

Weighted interval scheduling

Sort the acts by end time. Let best[i] be the most fans from the first i acts. Act i either sits out, giving best[i − 1], or it plays, giving fans + best[p], where p is how many acts end by the time act i starts. Those are exactly the acts that can play before it.

Because the end times are sorted, p is a binary search:

What does this print?
from bisect import bisect_right

ends = [60, 120, 150]
print(bisect_right(ends, 120))

Now use the recurrence on one cell. The first two acts are done: best[1] = 300 (Nova) and best[2] = 800 (Kade). What about Luma?

What does this print?
best = [0, 300, 800, 0]
p = 1                                  # only Nova ends by 60, when Luma starts
best[3] = max(best[2], 400 + best[p])  # sit out, or play Luma's 400 after the best of the first p
print(best[3])

Now step through the whole table for those three acts:

step through it
1from bisect import bisect_right
2order = [["Nova", 0, 60, 300], ["Kade", 30, 120, 800], ["Luma", 60, 150, 400]]
3ends = [a[2] for a in order]
4best = [0, 0, 0, 0]
5for i in range(1, 4):
6 name, start, end, fans = order[i - 1]
7 p = bisect_right(ends, start)
8 best[i] = max(best[i - 1], fans + best[p])
9print(best)

Reading the lineup back

As with the playlist packer, the table holds totals, so you walk back from the end. Act i plays only if fans + best[p] beats best[i − 1]. If it does, record the act and jump to p, since everything between p and i overlaps it. If it doesn't, step to i − 1. On a tie the act sits out, so the later-finishing act is the one dropped. It's the same tie rule as the packer.

broken — fix it

The main stage should be Kade and Zed. It prints four acts, and some of them overlap.

Composing the planner

Each stage of the pipeline consumes the one before it:

def plan_festival(acts, crate, curfew):
    main, fans = main_stage(acts)                 # DP: most fans, no overlaps
    leftovers = [a for a in acts if a not in main]
    side = side_stage(leftovers)                  # greedy: most acts
    encore = curfew - (main[-1][2] if main else 0)
    mixes = exact_mixes(crate, encore)            # backtracking: every exact fill
    ...

Build each helper on its own, check it against the examples, and only then connect them. When the planner's output is wrong, you'll know which stage to blame.

❓ Cross-question: "Couldn't backtracking do all three?" It could, slowly. Trying every subset of acts is 2ⁿ. Forty applicants is about a trillion lineups, while the DP needs 40 table cells. Use backtracking where you need every answer, like the encore mixes. There the output itself can be exponential, so nothing faster exists.

❓ Cross-question: "Then why not DP for the encore?" DP can count the mixes in O(c · slot), like count_fills. It can't list them any faster than backtracking, because listing costs at least as much as the list is long.

Stage Technique Time
Main stage sort + DP + binary search O(n log n)
Side stage sort + one greedy pass O(n log n)
Encore backtracking over c crate tracks O(c · 2ᶜ) worst case
Whole planner O(n log n + c · 2ᶜ)

Idioms & real-world patterns

Recognising the shape

  • "Most things that fit" and a swap argument works → greedy.
  • "Best total", where choices interact through a budget or overlaps → DP. Define the state, write the recurrence, fill or memoize.
  • "All of them", or a yes/no with heavy constraints → backtracking with pruning.
  • Not sure? Write the brute force first. It becomes the test oracle for whatever you write next.

⚡ Advanced: the same DP, top-down

The recurrence doesn't care about direction. With @cache, the state is just i:

python

⚡ Advanced: when the problem grows

Add a second main stage and the state has to track two stages' free times, so the simple DP no longer applies. Add fees and a budget and the state gains a budget dimension, just like knapsack. That's this lesson's boss exercise. Most hard DP problems are an easy one with one more thing in the state.


🎯 Your turn

Write plan_festival(acts, crate, curfew). Acts are [name, start, end, fans] with unique names. The crate is [title, minutes]. Return:

{
    "main_stage":   [...],  # most total fans, no overlaps, in play order
    "fans":         ...,    # their total fans
    "side_stage":   [...],  # from acts NOT on the main stage: most acts, earliest finish
    "encore":       ...,    # curfew minus the main stage's last end (the whole curfew if it's empty); never below 0
    "encore_mixes": [...],  # every crate combination filling the encore exactly; [] if encore is 0
}

Rules. Back-to-back acts don't overlap. Sort by end time, keeping input order on ties. On the main stage, an act plays only if it strictly beats the best lineup without it (walk back from the last act). Encore mixes follow the mix-finder order: tracks in crate order, mixes by position.

  • Acts Nova 0–60 (300), Kade 30–120 (800), Luma 60–150 (400), Rex 120–180 (350), Zed 150–200 (500); crate Aurora 4, Blink 3, Cobalt 2, Drift 5, Echo 1; curfew 207 → main ["Kade", "Zed"], fans 1300, side ["Nova", "Luma"], encore 7, mixes [["Aurora", "Blink"], ["Aurora", "Cobalt", "Echo"], ["Cobalt", "Drift"]]

Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green means this piece of the app is built. ✅

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upWho can play first?

Write acts_before(ends, start). ends is a sorted list of end times. Return how many of them are at or before start, meaning how many acts could finish before an act starting at start. Use bisect. acts_before([60, 120, 150], 120)2.

bisectbinary-search
DrillBiggest crowd

Write max_fans(acts). Each act is [name, start, end, fans], and back-to-back acts don't overlap. Return the largest total fans from a set of acts with no overlaps. max_fans([["Nova", 0, 60, 300], ["Kade", 30, 120, 800], ["Luma", 60, 150, 400], ["Rex", 120, 180, 350], ["Zed", 150, 200, 500]])1300.

dpbisecttabulation
BuildAudit a greedy promoter

A promoter books acts greedily by popularity: sort by fans, most first (ties keep input order), and book each act that doesn't overlap one already booked. Write audit_greedy(acts) returning [greedy_fans, best_fans]: the promoter's total, and the true best total, found by backtracking over every compatible set of acts. Acts are [name, start, end, fans]; back-to-back doesn't overlap.

greedybacktrackingsubsets
BossBiggest crowd on a budget

Now every act has a fee: [name, start, end, fans, fee]. Write fans_within_budget(acts, budget) returning the most total fans from acts that don't overlap (back-to-back is fine) and whose fees add up to at most budget.

dpknapsackbisect
CapstoneThe smoothest closing set

The closing DJ plays every track exactly once and always opens with track 0. cost[i][j] (a non-negative whole number) is how jarring it is to go from track i straight into track j, and it may differ from cost[j][i]. Write smoothest_order(cost) returning the smallest possible total cost of the transitions in the set. There's at least one track.

backtrackingpruningpermutations
plan_festival.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
plan_festival(acts, crate, curfew) → dictThe night's plan: main stage, fans, side stage, encore length and every encore mix.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.
Capstone — Festival Planner — Pebells