Ski Rental and Rent-or-Buy — Practice Tasks¶
Coding tasks are solved in one language (Python, with a short Go snippet where the port clarifies something). Where marked [coding], implement the online algorithm, the offline OPT, replay the request sequence (including the adversary's worst case), and measure the competitive ratio. [proof] / [analysis] tasks need no code: prove a competitive ratio (deterministic case analysis, adversary argument, or a probabilistic derivation), or analyze a variant, with a clean and complete argument. Model derivations are provided so you can grade yourself.
Ski rental is the seed problem of competitive analysis. You ski for an unknown number of days D. Each day you either rent skis for 1 unit, or buy them once for B units and ski free forever after. You never see D in advance — the adversary stops the trip whenever it likes. The whole rent-or-buy family (spin-then-block locks, the Bahncard / discount-card problem, leasing, snoopy caching) shares this skeleton: a stream of small recurring costs you can end at any time by paying one large one-time cost, decided online.
The discipline for every coding task is the one from competitive analysis: implement the online algorithm, implement the offline OPT = min(D, B), replay sequences, and measure the ratio. The acceptance test is always "the measured competitive ratio respects the proven bound across every tested sequence."
Related practice: - Competitive Analysis tasks — the framework (competitive ratio, adversary models, Yao's principle) these tasks instantiate; its beginner tasks introduce break-even ski rental. - Paging and Caching tasks — the same online/offline/adversary discipline applied to caches, and the randomized H_k lower bound via Yao.
This topic's notes: junior · middle · senior · professional
A note on the constants you will keep meeting: - 2 − 1/B — the optimal deterministic competitive ratio, achieved by break-even (buy on day B), and unbeatable by any deterministic rule. Tends to 2 as B → ∞. - e/(e − 1) ≈ 1.582 — the optimal randomized competitive ratio against an oblivious adversary, achieved by buying on a day drawn from a skewed distribution. - (1 + λ, 1 + 1/λ) — the consistency–robustness frontier of the learning-augmented algorithm (Purohit–Svitkina–Kumar) with trust parameter λ.
Beginner Tasks¶
Task 1 — Break-even ski rental and offline OPT [coding]¶
[easy] Implement the two halves of the canonical instance.
- Offline OPT knows
Din advance: ifD < Brent allDdays (costD); ifD ≥ Bbuy on day 1 (costB). SoOPT(D) = min(D, B). - Break-even online: rent on days
1, …, B − 1; if still skiing on dayB, buy. Cost isDwhenD ≤ B − 1, and(B − 1) + B = 2B − 1whenD ≥ B.
Replay every D from 1 to 3B and measure the worst ratio ALG/OPT. Confirm it never exceeds 2 − 1/B and that the bound is attained exactly at D = B.
Python¶
def opt_ski(D, B):
return min(D, B)
def break_even(D, B):
# Rent days 1..B-1; if still skiing on day B, buy (pay B more).
if D <= B - 1:
return D # trip ended before the buy trigger
return (B - 1) + B # rented B-1 days, then bought
if __name__ == "__main__":
for B in (2, 5, 10, 50, 100):
worst, arg = 0.0, 0
for D in range(1, 3 * B + 1):
ratio = break_even(D, B) / opt_ski(D, B)
if ratio > worst:
worst, arg = ratio, D
bound = 2 - 1.0 / B
print(f"B={B:3d} worst ratio={worst:.4f} at D={arg:3d} "
f"2-1/B={bound:.4f} ok={worst <= bound + 1e-9}")
assert abs(worst - bound) < 1e-9 and arg == B
Go (core)¶
func optSki(D, B int) int {
if D < B {
return D
}
return B
}
func breakEven(D, B int) int {
if D <= B-1 {
return D
}
return (B - 1) + B
}
- Constraints: Costs are integers;
B ≥ 2. The online algorithm only ever learns "am I still skiing?" one day at a time — it may not branch onDbefore dayB, which the break-even rule respects. - Hint: For
D < Bboth payD(ratio 1). AtD = B:ALG = 2B − 1,OPT = B, ratio(2B − 1)/B = 2 − 1/B. ForD > B,OPTstaysBwhileALGstays2B − 1, so the ratio only drops. - Acceptance test: For every
B, the worst measured ratio equals2 − 1/B, attained atD = B, and never exceeds it.
Task 2 — "Buy on day 1" and "rent forever" have unbounded ratio [coding]¶
[easy] Two tempting-but-terrible deterministic strategies. Show each has an unbounded competitive ratio — no constant c works for all inputs.
- Buy on day 1: pay
Bregardless ofD. The adversary picks a 1-day trip:OPT = 1,ALG = B, ratioB— grows without bound asB → ∞. - Rent forever: never buy; pay
D. The adversary picks a very long trip:OPT = B,ALG = D, ratioD/B → ∞.
Python¶
def buy_day1(D, B): return B
def rent_forever(D, B): return D
if __name__ == "__main__":
B = 10
# buy-on-day-1: adversary picks D = 1.
print(f"buy-day1: ratio at D=1 is {buy_day1(1, B)/min(1, B):.1f} = B; "
f"-> infinity as B grows")
# rent-forever: adversary picks D large; ratio = D/B.
print(f"{'D':>7} {'rent-forever ratio':>20}")
for D in (B, 10 * B, 100 * B, 1000 * B):
print(f"{D:>7} {rent_forever(D, B)/min(D, B):>20.1f}")
print("rent-forever ratio = D/B is unbounded as D -> infinity")
- Constraints: Demonstrate the blow-up as a function of the free parameter: buy-on-day-1 is
Θ(B)(unbounded inB); rent-forever isΘ(D/B)(unbounded inD). One fixed instance is not a proof — show the sweep. - Hint: "Unbounded competitive ratio" means: for every constant
cthere is an input withALG > c · OPT. Buy-on-day-1: takeD = 1and letB → ∞. Rent-forever: fixB, takeD = cB + 1, ratio> c. - Acceptance test: Buy-on-day-1's ratio equals
BatD = 1; rent-forever's equalsD/Band scales withD. Neither admits a finite competitive ratio — unlike break-even's2 − 1/B. This is why break-even hedges in the middle rather than committing to an extreme.
Task 3 — Empirical ratio over random and adversarial streams [coding]¶
[easy] Replaying single values of D is the cleanest test, but online analysis stresses an algorithm over batches of inputs. For a fixed B, replay trip lengths drawn three ways and report the maximum ratio in each batch:
- Uniform random
D ∈ [1, 4B], - Adversarial (always
D = B), - Geometric (frequent short trips, rare long ones).
Confirm the maximum ratio in every batch is ≤ 2 − 1/B, and that only the adversarial batch reaches the bound.
Python¶
import random
def opt_ski(D, B): return min(D, B)
def break_even(D, B): return D if D <= B - 1 else (B - 1) + B
def batch_worst(Ds, B):
return max(break_even(D, B) / opt_ski(D, B) for D in Ds)
if __name__ == "__main__":
random.seed(1)
B = 20
bound = 2 - 1 / B
uniform = [random.randint(1, 4 * B) for _ in range(10_000)]
adversarial = [B] * 10_000
geometric = [min(4 * B, 1 + int(random.expovariate(1 / 4))) for _ in range(10_000)]
for name, Ds in (("uniform", uniform), ("adversarial", adversarial),
("geometric", geometric)):
w = batch_worst(Ds, B)
print(f"{name:>12}: worst ratio={w:.4f} bound={bound:.4f} ok={w <= bound + 1e-9}")
assert w <= bound + 1e-9
- Constraints: The same fixed deterministic algorithm runs across all three batches; only the input distribution changes. Report the maximum ratio per batch — competitive ratio is a worst-case quantity, so averages mislead.
- Hint: Random and geometric batches usually report ratios well below the bound because they rarely hit
D = B; the adversarial batch hits it on every sample. This is the empirical face of "competitive ratio is a∀-statement." - Acceptance test: All three batches stay
≤ 2 − 1/B; only the adversarial batch attains it. This previews why randomizing the algorithm helps (Task 6): the adversary can no longer aim at one fixed worstD.
Task 4 — Spin-then-block locks as ski rental [coding]¶
[easy] A thread that wants a contended lock can spin (busy-wait, burning CPU) or block (sleep and let the OS wake it later). Each spin cycle costs a little; blocking costs a fixed, larger amount C (the context-switch + wakeup overhead). The lock is released after an unknown wait W. This is exactly ski rental: a spin cycle is a rental day (cost 1), blocking is buying (cost C), and W is the trip length D.
The classic OS strategy — spin for up to C cycles, then block — is therefore the break-even rule with B = C, and is (2 − 1/C)-competitive. Map the problem, implement the strategy and the offline optimum min(W, C), and measure the ratio over a range of wait times.
Python¶
def opt_lock(W, C):
# Offline: if the lock frees within C cycles, spin the whole time (cost W);
# otherwise block immediately (cost C).
return min(W, C)
def spin_then_block(W, C):
# Spin up to C-1 cycles; if still waiting, block (pay C).
if W <= C - 1:
return W # acquired while spinning
return (C - 1) + C # spun C-1 cycles, then blocked
if __name__ == "__main__":
for C in (4, 16, 64, 256): # block cost in spin-cycle units
worst, arg = 0.0, 0
for W in range(1, 3 * C + 1):
r = spin_then_block(W, C) / opt_lock(W, C)
if r > worst:
worst, arg = r, W
bound = 2 - 1 / C
print(f"C={C:4d} worst ratio={worst:.4f} at W={arg:4d} "
f"2-1/C={bound:.4f} ok={worst <= bound + 1e-9}")
assert worst <= bound + 1e-9
- Constraints: State the mapping explicitly: spin-cost = rental (1), block-cost =
B = C, wait time =D. The strategy may only learn whether the lock is still held, one cycle at a time. - Hint: The worst wait is
W = C: you spinC − 1cycles (wasted), then block (C), total2C − 1, while the optimum would have blocked immediately forC. Real implementations cap the spin budget nearCprecisely because of this2 − 1/Cguarantee. - Acceptance test: The worst ratio equals
2 − 1/CatW = Cand never exceeds it. The same code as Task 1 solves a real systems problem after a variable rename — that is the point of recognizing the rent-or-buy skeleton.
Intermediate Tasks¶
Task 5 — Prove break-even is (2 − 1/B)-competitive and deterministically optimal [proof]¶
[medium] Write a complete proof that break-even is (2 − 1/B)-competitive, and prove the matching lower bound: no deterministic online algorithm beats 2 − 1/B.
No code. Use this as the grading model.
Model proof — upper bound.
Let D be the adversary-chosen number of skiing days; the algorithm learns D only when skiing stops. Break-even rents on days 1, …, B − 1 and buys on day B if still skiing.
Case D ≤ B − 1. The algorithm only rents: ALG = D. The offline optimum also rents (since D < B): OPT = D. Ratio = 1.
Case D ≥ B. The algorithm rents B − 1 days then buys: ALG = (B − 1) + B = 2B − 1. The optimum buys on day 1 (since D ≥ B): OPT = B. Ratio = (2B − 1)/B = 2 − 1/B.
The cases are exhaustive and the second dominates, so for every D,
with no additive slack (α = 0): break-even is strictly (2 − 1/B)-competitive. ∎
Model proof — lower bound (adversary).
Let A be any deterministic online algorithm. Since A is deterministic and sees only "still skiing?", its behavior reduces to a single number: the day b on which it would first buy (b = ∞ if it never buys). The adversary picks D = b.
- If
b = ∞(never buys): pickDarbitrarily large.ALG = D → ∞whileOPT = B, ratio unbounded. - If
1 ≤ b ≤ B: atD = bthe algorithm rentedb − 1days and bought,ALG = (b − 1) + B, whileOPT = min(b, B) = b. Ratio= (b − 1 + B)/b = 1 + (B − 1)/b ≥ 1 + (B − 1)/B = 2 − 1/B, minimized atb = B. - If
b > B: atD = bthe algorithm rentedb − 1 ≥ Bdays then bought,ALG = (b − 1) + B > 2B − 1, whileOPT = B. Ratio> 2 − 1/B.
In every case the ratio is ≥ 2 − 1/B. Hence no deterministic online algorithm beats 2 − 1/B, and break-even is optimal among deterministic algorithms. ∎
- Constraints: Both halves required: (1) the two-case upper bound
ALG ≤ (2 − 1/B)·OPT; (2) the adversary argument that every deterministic algorithm is≥ (2 − 1/B)-competitive. State why a deterministic algorithm reduces to a single buy-dayb. - Acceptance test: The proof exhibits the two upper-bound cases, the buy-day reduction with the
D = badversary, and concludes optimality at exactly2 − 1/B.
Task 6 — Randomized ski rental approaches e/(e − 1) ≈ 1.58 [coding]¶
[medium] Randomization breaks the deterministic 2 − 1/B barrier. The optimal randomized algorithm picks a random buy-day j ∈ {1, …, B} from a skewed distribution and buys on day j if still skiing. Against an oblivious adversary (which fixes D before seeing the coin) its expected competitive ratio tends to e/(e − 1) ≈ 1.582 as B → ∞.
The optimal discrete distribution puts weight
Implement the randomized algorithm and OPT, sweep the oblivious adversary's stopping day D, and Monte-Carlo estimate the worst expected ratio. Confirm it lands near e/(e − 1) and below 2 − 1/B.
Python¶
import random, math
def opt_ski(D, B):
return min(D, B)
def buy_distribution(B):
w = [((B - 1) / B) ** (B - j) for j in range(1, B + 1)]
total = sum(w)
return [x / total for x in w]
def sample_buy_day(probs):
r, acc = random.random(), 0.0
for j, p in enumerate(probs, start=1):
acc += p
if r <= acc:
return j
return len(probs)
def randomized_ski(D, B, buy_day):
if D < buy_day:
return D # trip ended before we bought
return (buy_day - 1) + B # rented buy_day-1 days, then bought
if __name__ == "__main__":
random.seed(7)
print(f"{'B':>5} {'worst E[ratio]':>16} {'e/(e-1)':>10} {'2-1/B':>8}")
for B in (10, 50, 200, 1000):
probs = buy_distribution(B)
trials_per_D = 4000
worst = 0.0
for D in range(1, 2 * B + 1): # oblivious adversary: fix D, then flip
tot = 0.0
for _ in range(trials_per_D):
tot += randomized_ski(D, B, sample_buy_day(probs)) / opt_ski(D, B)
worst = max(worst, tot / trials_per_D)
print(f"{B:>5} {worst:>16.4f} {math.e/(math.e-1):>10.4f} {2 - 1/B:>8.4f}")
assert worst <= 2 - 1/B + 0.02 # strictly below the deterministic bound
- Constraints: The adversary is oblivious — it fixes
Dbefore the coin is flipped, so you sweep candidateDand take the worst expected ratio, never letting the adversary peek atj. Use enough trials that the estimate is stable to two decimals; the distribution must be normalized. - Hint: The skew
((B−1)/B)^{B−j}makes early buy-days rarer and late buy-days commoner — the algorithm leans toward renting but keeps a real chance of buying early, denying the adversary a single worstD. AsB → ∞the discrete optimum converges to the continuous density of Task 8. - Acceptance test: The worst expected ratio over all stopping days is
≈ 1.58for largeB, strictly below2 − 1/B. Randomization buys roughly a0.42reduction in the worst-case multiplier.
Task 7 — The Bahncard / discount-card rent-or-buy problem [coding]¶
[medium] The Bahncard problem generalizes ski rental. A traveler makes trips of varying cost. Without a card, a trip of price t costs t. A discount card costs C up front, lasts forever (the simplified, non-expiring version), and reduces every subsequent trip's price by a factor β ∈ (0, 1) — you pay β·t per trip after buying. Should you buy the card, and when?
This is rent-or-buy with a twist: buying does not make future trips free, only cheaper. Reducing to ski rental: think in units of "money saved." Each trip of price t would save (1 − β)·t if you already held the card, so accumulated potential savings play the role of "rental days," and the card price C plays the role of B. The break-even rule becomes: buy the card once your cumulative would-be savings (1 − β)·Σt reach C.
Implement the online break-even Bahncard, the offline optimum (buy at time 0 iff total discounted spend justifies it), and measure the ratio. Then find the competitive ratio as a function of β.
Python¶
def offline_bahncard(prices, C, beta):
# Offline OPT: either never buy (pay sum prices) or buy at the start
# (pay C + beta*sum prices). Choose the cheaper; full hindsight.
total = sum(prices)
no_card = total
with_card = C + beta * total
return min(no_card, with_card)
def online_bahncard(prices, C, beta):
# Break-even on accumulated would-be savings. Buy the card the first time
# the savings it WOULD have given so far reach C.
cost = 0.0
have_card = False
would_be_savings = 0.0
for t in prices:
if have_card:
cost += beta * t
continue
# Decide before paying for this trip: would buying now have paid off?
would_be_savings += (1 - beta) * t
if would_be_savings >= C:
cost += C # buy the card now ...
have_card = True
cost += beta * t # ... and this trip is discounted
else:
cost += t # still no card: full price
return cost
if __name__ == "__main__":
import random
random.seed(2)
C, beta = 100.0, 0.5
bound = 2 - (1 - beta) # = 1 + beta, the Bahncard break-even ratio
worst = 0.0
for _ in range(20000):
n = random.randint(1, 40)
prices = [random.uniform(1, 30) for _ in range(n)]
on = online_bahncard(prices, C, beta)
off = offline_bahncard(prices, C, beta)
worst = max(worst, on / off)
print(f"C={C} beta={beta} worst ratio={worst:.4f} bound 2-(1-beta)={bound:.4f} "
f"ok={worst <= bound + 1e-9}")
assert worst <= bound + 1e-9
- Constraints: The card never expires (simplified model);
β ∈ (0, 1); the card discounts the current trip too if you buy right before paying for it. Decide whether to buy before committing each trip's payment — the algorithm cannot look ahead. - Hint: Setting
β = 0recovers ordinary ski rental (C = B), and the bound2 − (1 − β) = 1 + βcollapses to2. The break-even threshold is where cumulative savings(1 − β)Σtreach the card priceC; the worst case has the traveler stop the instant the card is bought, paying for it without recouping — exactly theD = Badversary in disguise. - Acceptance test: Over random price sequences the measured worst ratio stays
≤ 1 + β, and tends toward it on adversarial sequences that stop right after the card is purchased. The classic Bahncard result (Fleischer) gives2 − βfor the expiring card; the non-expiring break-even here gives1 + β.
Task 8 — Verify the continuous randomized density p(x) = e^x/(e − 1) [coding + analysis]¶
[medium] In the continuous limit (B → ∞, scale so the buy cost is 1 and the rental rate is 1 per unit "time"), the optimal randomized algorithm buys at a random time x ∈ [0, 1] drawn from the density
Buying at time x means renting for x (cost x) then paying 1 to buy, if the trip lasts beyond x; if the trip ends at time D ≤ x you only paid D. Show by sampling that this density yields expected competitive ratio e/(e − 1) for every adversarial stopping time D ∈ [0, 1] (the ratio is constant in D — that flatness is what makes it optimal).
Python¶
import random, math
def opt_cont(D):
return min(D, 1.0) # buy costs 1, rent rate 1
def sample_buy_time():
# Inverse-CDF sample from p(x) = e^x/(e-1). CDF F(x) = (e^x - 1)/(e - 1).
u = random.random()
return math.log(1 + u * (math.e - 1)) # F^{-1}(u)
def alg_cont(D, x):
# Rent until x, then buy (pay 1) if still skiing; else stop at D.
if D <= x:
return D
return x + 1.0
if __name__ == "__main__":
random.seed(11)
target = math.e / (math.e - 1)
trials = 200_000
print(f"{'D':>5} {'E[ratio]':>10} {'e/(e-1)':>10}")
worst = 0.0
for D in (0.05, 0.25, 0.5, 0.75, 0.999):
tot = 0.0
for _ in range(trials):
tot += alg_cont(D, sample_buy_time()) / opt_cont(D)
est = tot / trials
worst = max(worst, est)
print(f"{D:>5.3f} {est:>10.4f} {target:>10.4f}")
print(f"worst over D = {worst:.4f} (flat ~ e/(e-1) = {target:.4f})")
assert abs(worst - target) < 0.02
- Analysis to write: Compute
E[ALG(D)]for fixedD ∈ [0, 1]directly. With buy-time densityp(x) = e^x/(e − 1):
The first integral covers buy-times before the trip ends (you bought, cost x + 1); the second covers buy-times after (you never bought, cost D). Carrying out the integral gives E[ALG(D)] = (e/(e − 1))·D for all D ∈ [0, 1], while OPT(D) = D, so the ratio is the constant e/(e − 1) — independent of D. A distribution that equalizes the ratio across all adversary choices is optimal, because the adversary has no preferred D to exploit. - Constraints: Sample via the inverse CDF F⁻¹(u) = ln(1 + u(e − 1)), not rejection sampling. Verify the ratio is flat across D, not merely small at one point — flatness is the optimality witness. - Acceptance test: For every tested D ∈ (0, 1) the estimated E[ratio] is ≈ e/(e − 1), and the spread across D is within Monte-Carlo noise. The hand integral confirms the empirical flatness analytically.
Advanced Tasks¶
Task 9 — Learning-augmented ski rental: consistency vs robustness [coding]¶
[hard] A learning-augmented algorithm (Purohit–Svitkina–Kumar, 2018) receives a prediction ŷ of the unknown ski days D plus a trust parameter λ ∈ (0, 1], and aims to be both:
- Consistent: ratio
→ 1as the prediction error→ 0(smallλtrusts the prediction more), and - Robust: ratio bounded even when
ŷis arbitrarily wrong.
The deterministic PSK rule: if the prediction says "long trip" (ŷ ≥ B), buy early — when rental cost reaches λB; if it says "short trip" (ŷ < B), buy late — when rental cost reaches B/λ (capped so the buy-day is a valid day). This is (1 + λ)-consistent and (1 + 1/λ)-robust. Implement it, sweep λ, and empirically plot consistency (ratio under a perfect prediction) against robustness (worst ratio over adversarial ŷ), confirming the two guarantees.
Python¶
def opt_ski(D, B):
return min(D, B)
def la_ski(D, B, y_hat, lam):
# Prediction-augmented buy-day threshold.
if y_hat >= B: # predict long trip -> buy early
buy_day = max(1, round(lam * B))
else: # predict short trip -> buy late
buy_day = max(1, round(B / lam))
if D < buy_day:
return D
return (buy_day - 1) + B
if __name__ == "__main__":
B = 100
print(f"{'lam':>5} {'consistency':>12} {'1+lam':>7} "
f"{'robustness':>11} {'1+1/lam':>8}")
for lam in (0.1, 0.25, 0.5, 0.75, 1.0):
# Consistency: perfect prediction y_hat = D, worst over D.
consistency = max(la_ski(D, B, D, lam) / opt_ski(D, B)
for D in range(1, 3 * B))
# Robustness: adversarial prediction (wildly wrong both directions), worst over D.
robustness = 0.0
for D in range(1, 3 * B):
for y_hat in (1, 10 * B):
robustness = max(robustness,
la_ski(D, B, y_hat, lam) / opt_ski(D, B))
print(f"{lam:>5.2f} {consistency:>12.3f} {1+lam:>7.3f} "
f"{robustness:>11.3f} {1+1/lam:>8.3f}")
assert consistency <= 1 + lam + 0.05
assert robustness <= 1 + 1/lam + 0.05
- Constraints:
λinterpolates trust: smallλtrusts the prediction (low consistency ratio, high robustness ratio), largeλhedges (consistency rises toward 2, robustness falls toward 2). Round buy-days to valid integer days≥ 1. - Hint: No
λmakes the algorithm both1-consistent and2-robust at once: the Pareto frontier(1 + λ, 1 + 1/λ)is the unavoidable price of trusting an unverified prediction. Theλ → 1corner recovers the prediction-free2 − 1/Bof Task 1 — full hedging, no trust. - Acceptance test: Consistency tracks
≈ 1 + λ(best whenλis small), robustness tracks≈ 1 + 1/λ(best whenλis large), and the two move in opposite directions asλsweeps — the empirical consistency–robustness frontier.
Task 10 — Randomized lower bound e/(e − 1) via Yao [analysis]¶
[hard] Task 6 achieved e/(e − 1); now prove no randomized ski-rental algorithm can do better against an oblivious adversary. Use Yao's principle.
No code. Model derivation follows.
Yao's principle. To lower-bound the expected competitive ratio of any randomized online algorithm against an oblivious adversary, it suffices to fix a single input distribution D over stopping days and lower-bound the expected ratio of the best deterministic algorithm on D:
The hard distribution. Work in the continuous model (buy cost 1, rent rate 1, trip length D ∈ [0, 1]). Let the adversary draw D from the density
more precisely the distribution that makes every deterministic buy-time b incur the same expected ratio. A deterministic algorithm is, as always, a single buy-time b ∈ [0, 1] (or "never buy"). Its expected cost under 𝒟:
since for D < b the trip ends before buying (cost D), and for D ≥ b the algorithm rents b then buys (cost b + 1). With OPT(D) = D, choose q so that E[ALG_b] / E[OPT] is constant in b — equal to e/(e − 1). (This q is the dual partner of the algorithm's optimal buy-time density p(x) = e^x/(e − 1) from Task 8; the equalizing input distribution and the equalizing algorithm distribution are a matched saddle pair.)
Applying Yao. Because some input distribution forces every deterministic algorithm — hence the best one — to expected ratio ≥ e/(e − 1), Yao lifts this to randomized algorithms: every randomized ski-rental algorithm has expected competitive ratio ≥ e/(e − 1) against an oblivious adversary. Task 6's algorithm matches it, so e/(e − 1) is exactly the randomized ski-rental ratio. ∎
- Constraints: State Yao's inequality (the min-max swap), exhibit an input distribution that equalizes the expected ratio across all deterministic buy-times
b, and conclude thee/(e − 1)lower bound. Note the duality with the optimal algorithm density of Task 8. - Acceptance test: The answer invokes Yao correctly (single input distribution → best-deterministic expected ratio lower-bounds the randomized worst case), supplies an equalizing distribution over stopping days, derives the constant
e/(e − 1), and identifies it as a matched saddle point with Task 8's algorithm distribution.
Task 11 — Multi-option leasing / parking-permit variant [analysis + coding]¶
[hard] Ski rental has two options (rent, buy). The parking-permit / multi-option leasing problem has many: at each step you may purchase one of several leases, lease i costing c_i and valid for duration d_i, and you must cover every day on which you "park" (use the resource). With K lease types this generalizes rent-or-buy to a menu of commitments. The deterministic competitive ratio is O(K) and there is a matching randomized O(log K) algorithm — directly echoing the deterministic-k-vs-randomized-log k gap of paging.
Take the simplest nontrivial menu: a daily permit (c₁ = 1, d₁ = 1) and a long permit (c₂ = B, d₂ = ∞). Show this is ski rental, so the deterministic ratio is 2 − 1/B. Then add a third, intermediate permit (c₃ = m, covers a block of m consecutive days, 1 < m < B) and analyze how the deterministic competitive ratio changes; simulate to confirm.
Python (simulation harness)¶
def offline_opt(park_days, permits):
# park_days: sorted list of day indices that must be covered.
# permits: list of (cost, duration) with duration None meaning infinite.
# DP over the sorted parking days: cost to cover days[i:].
days = sorted(set(park_days))
n = len(days)
INF = float("inf")
best = [0.0] + [INF] * n # best[i] = min cost to cover days[i:]; index from the end
# process from last day backward
dp = [0.0] * (n + 1)
for i in range(n - 1, -1, -1):
dp[i] = INF
for (cost, dur) in permits:
if dur is None:
dp[i] = min(dp[i], cost) # one infinite permit covers the rest
else:
# this permit bought on day[i] covers [day[i], day[i]+dur)
limit = days[i] + dur
j = i
while j < n and days[j] < limit:
j += 1
dp[i] = min(dp[i], cost + dp[j])
return dp[0]
def online_break_even(park_days, B):
# Two-option ski rental online rule on the parking stream.
rented = 0
cost = 0
bought = False
for _ in park_days:
if bought:
continue
rented += 1
if rented >= B: # break-even: buy the infinite permit
cost += B
bought = True
else:
cost += 1
return cost
if __name__ == "__main__":
import random
random.seed(4)
B = 20
worst = 0.0
permits_two = [(1, 1), (B, None)] # daily + infinite == ski rental
for _ in range(5000):
days = sorted(random.sample(range(0, 3 * B), random.randint(1, 3 * B)))
on = online_break_even(days, B)
off = offline_opt(days, permits_two)
worst = max(worst, on / off)
print(f"two-option (ski rental): worst online/OPT = {worst:.4f} "
f"bound 2-1/B = {2 - 1/B:.4f}")
assert worst <= 2 - 1/B + 1e-9
- Analysis to write:
- Two options reduce to ski rental. The daily permit is "rent," the infinite permit is "buy" with
B = c₂. OPT picksmin(#park-days, B)in the worst (contiguous) case, and break-even is(2 − 1/B)-competitive verbatim. - Adding the
m-day permit. With three options the adversary can no longer be defeated by a single threshold: a good online rule must hedge across both the day→block and block→infinite decisions. Each "level" of the lease hierarchy contributes a constant to the ratio, givingO(number of levels)deterministically — hereO(3)rather thanO(2). The general parking-permit result is deterministicΘ(K)forKlease durations and randomizedΘ(log K)(Meyerson, 2005), structurally identical to the deterministic-kvs randomized-H_kstory for paging. - Why randomization helps again. As with ski rental and paging, the deterministic algorithm commits to thresholds the adversary can target; a randomized algorithm spreads its commitment across lease levels, and the
O(log K)bound falls out of an exponential-weights / harmonic argument over theKlevels. - Constraints: The offline OPT must be exact (the DP over sorted parking days is correct for nested/non-overlapping permit durations). For the two-option menu the simulation must reproduce the
2 − 1/Bski-rental bound exactly. - Acceptance test: The two-option simulation matches
2 − 1/B; the written analysis explains the per-level constant that yields deterministicΘ(K), names the randomizedΘ(log K)(Meyerson) result, and draws the explicit parallel to paging's deterministic-k/ randomized-H_kgap.
Synthesis Task¶
Carry ski rental through the full rent-or-buy arc — online algorithm, offline OPT, adversary, deterministic proof, randomized improvement, and the prediction-augmented frontier — and locate it on the online-algorithms map.
[capstone] Assemble the pieces into one account.
-
Algorithm + OPT [coding]. Break-even (Task 1) and
OPT(D) = min(D, B); replay allD, report worst ratio= 2 − 1/B, attained atD = B. -
Deterministic optimality [proof]. Prove
ALG ≤ (2 − 1/B)·OPTand the matching lower bound (Task 5): every deterministic algorithm reduces to a buy-dayb, andD = bforces ratio≥ 2 − 1/B. -
Randomized improvement [coding + analysis]. Run the randomized algorithm (Task 6), measure the worst expected ratio approaching
e/(e − 1) ≈ 1.58, and explain via Yao (Task 10) why this is the randomized floor — the oblivious adversary must fixDbefore the coin, so it cannot aim at the algorithm's committed buy-day. -
A real rent-or-buy instance [coding]. Reuse the same code for spin-then-block locks (Task 4,
B = C) or the Bahncard (Task 7), demonstrating that the skeleton transfers across domains by a variable rename. -
Learning-augmented frontier [coding]. Sweep
λ(Task 9) and report the(1 + λ, 1 + 1/λ)consistency–robustness trade-off; contrastλ → 1with the prediction-free2 − 1/B.
Reference harness in Python (combines the deterministic and randomized pieces):
import math, random
def opt(D, B): return min(D, B)
def break_even(D, B): return D if D <= B - 1 else (B - 1) + B
def det_worst(B):
return max(break_even(D, B) / opt(D, B) for D in range(1, 3 * B))
def randomized_worst(B, trials_per_D=4000):
w = [((B - 1) / B) ** (B - j) for j in range(1, B + 1)]
tot = sum(w)
probs = [x / tot for x in w]
def sample():
r, acc = random.random(), 0.0
for j, p in enumerate(probs, 1):
acc += p
if r <= acc:
return j
return B
worst = 0.0
for D in range(1, 2 * B + 1):
s = 0.0
for _ in range(trials_per_D):
j = sample()
s += (D if D < j else (j - 1) + B) / opt(D, B)
worst = max(worst, s / trials_per_D)
return worst
if __name__ == "__main__":
random.seed(42)
for B in (10, 100, 1000):
det = det_worst(B)
rnd = randomized_worst(B)
print(f"B={B:5d} deterministic={det:.4f} (=2-1/B={2-1/B:.4f}) "
f"randomized~{rnd:.3f} (floor e/(e-1)={math.e/(math.e-1):.3f})")
assert det <= 2 - 1/B + 1e-9
- Proof answer: Deterministic break-even is strictly
(2 − 1/B)-competitive (two-case upper bound) and optimal (buy-day reduction +D = badversary). Randomization lowers the worst expected ratio toe/(e − 1)because the oblivious adversary must fixDbefore the coin, defeating the "aim at the committed buy-day" attack — the same mechanism that gives paging its randomizedH_kfloor. - Acceptance test: Deterministic worst ratio equals
2 − 1/B, attained atD = B; randomized worst expected ratio sits neare/(e − 1) < 2 − 1/B; the proof closes both deterministic bounds; the write-up places ski rental on the map between deterministic2 − 1/B, randomizede/(e − 1), and the prediction-augmented(1 + λ, 1 + 1/λ)frontier — and shows the same code solving a real systems problem (spin-then-block, Bahncard). This is the whole craft: implement the online algorithm, build the adversary, prove the ratio, measure it against offline OPT, and pin down where randomization and predictions move the floor.
Where to go next¶
- Return to the framework these tasks instantiate — competitive ratio, adversary models, Yao's principle, potential-function proofs — in the competitive-analysis tasks.
- Carry the online/offline/adversary discipline and the randomized
H_k-via-Yao argument into caches in the paging-and-caching tasks. - For the conceptual treatment of break-even, the randomized distribution, the Bahncard, spin-then-block, and learning-augmented consistency/robustness, read this topic's junior, middle, senior, and professional notes.
In this topic
- interview
- tasks