What L5 looks like
- States and justifies that an optimal window ends on a sprint's last day.
- Builds prefix sums over sprints and uses the tail-sum formula for the
partial first sprint, gettingO(n log n)orO(n). - Tests the edge cases:
k = 1,k = T, one sprint, and windows that wrap.
Approach
Flatten the schedule into a sequence of daily points: a sprint of length d
contributes 1, 2, ..., d. The schedule repeats, so the sequence is circular,
and we want the circular window of exactly k consecutive days with the
largest sum.
Brute force. Expand every sprint into its days (T = sum(days) values)
and slide a window of length k around the circle with a running sum. That
is O(T), but T can be huge when sprints are long (lengths up to 10^9 are a
common constraint for this kind of problem), so expanding days is not acceptable. We need something
that is linear in the number of sprints, n.
Key observation: an optimal window ends on the last day of some sprint.
Take any window whose last day is day j of a sprint of length d, with
j < d. Shifting the window one day to the right gains j + 1 and loses
the value x of its first day.
- While neither end crosses a sprint boundary, both the gained and the lost
value grow by exactly 1 per shift, so the change per shift is constant. The
total is linear in the shift, and the best position in that stretch is at
one of its ends. - One end of the stretch is "right edge reaches a sprint end" (what we want).
The other is "left edge sits on day 1 of a sprint". From there, a right
shift loses 1 and gainsj + 1 >= 2, which strictly helps, so we keep
moving right.
So we only need to test n candidate windows: one ending at the end of each
sprint.
Evaluating one candidate in O(log n). Unroll the circle once
(days + days) and build two prefix sums over it: days so far, and points
so far (a full sprint of length d is worth d(d+1)/2). For a window ending
at the end of sprint e (with e in the second copy, so the window never
runs off the left side when k <= T):
-
start = end - kis the day just before the window. - Binary-search the sprint
scontaining daystart + 1. - Sprints
s+1 .. eare fully inside: read their points from the prefix
sum. - Sprint
scontributes only its lastrdays, worth
(d-r+1) + ... + d = r(2d - r + 1)/2. Taking the tail matters: the
window starts mid-sprint, so it gets that sprint's high-value days.
If k exceeds T, every full loop is forced and contributes the same total,
so take k mod T for the window and add (k div T) times the points of one
loop.
Invariants: day_pre[i] is the number of days in the first i sprints of
the unrolled array; 1 <= r <= doubled[s]; the window covers days
(start, end] exactly.
Code
from bisect import bisect_left
from itertools import accumulate
def tri(m: int) -> int:
"""Points for a full sprint of m days: 1 + 2 + ... + m."""
return m * (m + 1) // 2
def tail_points(d: int, r: int) -> int:
"""Points for the last r days of a d-day sprint: (d-r+1) + ... + d."""
return r * (2 * d - r + 1) // 2
def max_sprint_points(days: list[int], k: int) -> int:
n = len(days)
total_days = sum(days)
if n == 0 or k <= 0 or total_days == 0:
return 0
cycles, k = divmod(k, total_days) # whole loops are forced
base = cycles * sum(tri(d) for d in days)
if k == 0:
return base
doubled = days + days # unroll the cycle once
day_pre = [0] + list(accumulate(doubled)) # day_pre[i] = days in doubled[:i]
pts_pre = [0] + list(accumulate(tri(d) for d in doubled))
best = 0
# Window ends at the last day of doubled[e], for e in n .. 2n-1.
for e in range(n, 2 * n):
end = day_pre[e + 1]
start = end - k # window covers days (start, end]
# first sprint s whose end lies after `start`
s = bisect_left(day_pre, start + 1) - 1
full = pts_pre[e + 1] - pts_pre[s + 1] # sprints s+1 .. e, whole
r = day_pre[s + 1] - start # days taken from sprint s (its tail)
best = max(best, full + tail_points(doubled[s], r))
return base + best
if __name__ == "__main__":
assert max_sprint_points([3, 1, 4], 5) == 13 # 3 4 | 1 2 3 wraps around
assert max_sprint_points([5], 3) == 12 # 3 + 4 + 5
assert max_sprint_points([4, 1], 5) == 11
assert max_sprint_points([1, 1, 1], 2) == 2
assert max_sprint_points([3, 1, 4], 10) == 24 # one full loop (14) + best 2 days (10)
print("ok")
Complexity
- Time:
O(n log n)fornsprints:O(n)to build the prefix sums over
the unrolled array, thenncandidates with one binary search each. A two
pointer sweep (the start sprint only moves forward asegrows) makes it
O(n). - Space:
O(n)for the unrolled array and the two prefix sums. - Independent of the number of days, so sprint lengths up to 10^9 are fine.
Python integers do not overflow; in Java or C++ use 64-bit sums.
Test cases
-
days = [3, 1, 4], k = 5->13(the circular run3, 4 | 1, 2, 3: the
last two days of sprint 2, then all of sprint 0 in the next loop). -
days = [5], k = 3->12(last three days3 + 4 + 5). -
days = [4, 1], k = 5->11(4 + 1 + 1 + 2 + 3, wrapping). -
days = [1, 1, 1], k = 2->2(every day is worth 1). -
days = [2, 5], k = 3->12(3 + 4 + 5, entirely inside the long sprint). -
days = [4, 2, 3], k = 6->15. -
days = [7], k = 7->28(the whole loop,k == T). -
days = [3, 1, 4], k = 10->24(k > T = 8: one full loop worth 14, plus
the best 2-day window3 + 4, worth 10). - Edge:
k = 1returnsmax(days);k = 0returns0. - Randomized check against the day-by-day brute force for small inputs.
Follow-ups
-
Linear time. Replace the binary search with a pointer
sthat only
advances aseadvances (the window start is monotone). Same prefix sums,
O(n)total. -
Return the window, not just the sum. Track the best
eands; report
the starting sprint and day (doubled[s] - r + 1, modn) and the end
sprint. -
Different scoring per day. If day
jof sprintiis worth an
arbitraryf(i, j)that is still increasing within each sprint, the
alignment argument still points to ends of sprints, but you need a
per-sprint suffix-sum formula (or precomputed suffix sums when sprints are
short). If points are not monotone within a sprint, the argument breaks and
you fall back to a sliding window over expanded days. -
Many queries for different
k. Precompute the prefix sums once; each
query is thenO(n)(orO(n log n)). For many queries, note the answer
per candidateeis a piecewise quadratic ink, which supports offline
tricks, but stating theO(n)per query is usually enough. -
Non-cyclic schedule. Drop the unrolling; only candidates with
day_pre[e+1] >= kare valid, plus the window that starts at day 1 of the
first sprint ifkdoes not fit after a sprint end.
Interviewer expectations
Mid-level (L4)
- Models days as a circular array and gets a correct sliding-window solution
over expanded days. - Recognizes that expanding days is too slow when sprint lengths are large.
- Handles the wrap-around correctly (array doubling or modulo indexing).
Senior (L5)
- States and justifies that an optimal window ends on a sprint's last day.
- Builds prefix sums over sprints and uses the tail-sum formula for the
partial first sprint, gettingO(n log n)orO(n). - Tests the edge cases:
k = 1,k = T, one sprint, and windows that wrap.
Staff (L6+)
- Gives a crisp exchange argument (the sum is linear between boundaries, so
the maximum is at a boundary) and knows when it fails (non-monotone points). - Generalizes cleanly to
k > T, many queries, or other scoring functions. - Calls out overflow in fixed-width languages and writes the code so the
arithmetic is visibly correct.
Make it yours
Write each chapter yourself, check it against this answer, then let the coach grill you.
