The lantern wire
Switch off a row of festival lanterns in the order that earns the most tips, by deciding which lantern in each stretch goes dark last.
A wire of paper lanterns goes dark one lantern at a time, and each switch-off pays according to the two lanterns still lit beside it. What to remove first is the wrong question.
The problem
A wire across the market square carries n paper lanterns, lantern i burning
at brightness glow[i]. At closing time the crew switches them off one at a
time, in any order, until the wire is dark.
A photographer pays for every switch-off: the tip is the brightness of the
nearest lantern still lit to the left, times the brightness of the lantern going
dark, times the brightness of the nearest one still lit to the right. Where no
lit lantern remains on a side, the wall bracket at that end stands in with a
brightness of 1; brackets are never switched off. A dark lantern leaves the
wire for good, so its neighbours become neighbours of each other and the next
tip is read off the shortened wire.
Return the largest total tip the crew can collect.
Input. glow — a list of integers, the brightness of each lantern from left
to right.
Output. An integer, the maximum total tip.
Example.
glow = [2, 4, 3, 5] -> 115
Take the 3 first, between 4 and 5: 4·3·5 = 60. Then the 4, now between
2 and 5: 40. Then the 2, with only the bracket on its left: 10. The 5
last, alone: 5. Total 115. Always taking the dimmest first pays 93.
A second example, where a lantern worth nothing is worth removing early:
glow = [5, 0, 4] -> 25
Switching off the 0 earns nothing, but it makes 5 and 4 neighbours: the
4 then pays 20 and the 5 pays 5. Leaving it for last caps the run at 24.
Constraints.
0 <= len(glow) <= 3000 <= glow[i] <= 100- The wire may be empty; the answer is then
0.
Hints
Hint 1
There are n! orders. Ask instead what a stretch of wire is worth on its own,
and how stretches combine.
Hint 2
"Which lantern goes off first" is a bad split: afterwards the two sides are not independent, since a later removal on the left can see a neighbour on the right. Ask which lantern goes off last instead.
Hint 3
Pad the wire with a brightness-1 bracket at each end and value the stretch
(i, j): everything strictly between is dark, i and j still lit.
Approach
Brute force
Try every order: n! sequences, each O(n) to score. At n = 12 that is 5.7
billion steps, and the limit is 300.
The insight
Split a stretch on the lantern switched off last, not first — when the last one goes, its lit neighbours are exactly the two lanterns bounding the stretch, whatever happened inside it.
That pins the final tip at glow[i] * glow[k] * glow[j], fixed by the stretch
alone. The lanterns left of k and those right of k are then closed
sub-problems of the same shape, because k stays lit while both are cleared, so
neither side can see across it. Splitting on the first removal gives no such
guarantee, which is why it fails.
Algorithm
- Build
wire = [1] + glow + [1], so0andn+1are the brackets. - Let
best[i][j]be the most tips from clearing everything strictly betweeniandj, both still lit; a gap under 2 is worth0. - Fill by increasing gap
j - i, from 2 up ton + 1. - For each
(i, j)try everykbetween them as the last one off:best[i][k] + wire[i]*wire[k]*wire[j] + best[k][j], keeping the maximum. - The answer is
best[0][n + 1].
Complexity
Time O(n³) — about n²/2 stretches, each trying up to n split points, so
roughly 4.5 million updates at n = 300. Space O(n²) for the table.
Solution
"""Lantern wire — interval DP on the lantern that is switched off last."""
def solve(glow):
# Pad both ends with a brightness of 1: the wall brackets are never switched
# off, so they can stand in for "no lit lantern on that side".
wire = [1] + list(glow) + [1]
n = len(wire)
# best[i][j]: most tips earned by switching off every lantern strictly
# between positions i and j, while i and j are still lit.
best = [[0] * n for _ in range(n)]
for span in range(2, n): # widen the gap: shorter runs are already solved
for i in range(n - span):
j = i + span
top = 0
for k in range(i + 1, j):
# k is the LAST lantern off inside (i, j), so at that moment its
# lit neighbours are exactly i and j, whatever happened before.
tips = best[i][k] + wire[i] * wire[k] * wire[j] + best[k][j]
if tips > top:
top = tips
best[i][j] = top
return best[0][n - 1]The cases that ran
TESTS = [
(([2, 4, 3, 5],), 115),
(([5, 0, 4],), 25),
(([6, 2],), 18),
(([7],), 7),
(([3, 3, 3],), 39),
(([1, 9, 2, 8, 1],), 243),
(([],), 0),
]Pitfalls
- Splitting on the first lantern switched off. It looks symmetric and is not: the two sides can still become neighbours, so the sub-problems overlap and the recurrence charges tips the wire never paid.
- Filling the table in plain row order.
best[i][j]readsbest[i][k]andbest[k][j], both shorter stretches, so the loop has to widen the gap; afor i: for j:sweep reads zeros that are not filled yet and under-reports. - Treating a zero-brightness lantern as skippable. It earns nothing itself, but removing it early joins two bright neighbours, which is worth real money.
Variants
- Cutting the parcel spur — also asks where to split, but on a tree, where one sweep suffices because the two pieces never interact.
- The fly cue sheet — a grid DP whose state is a row layout rather than an interval.