Problem
From a coding assessment (2026-10-13), where it was called “Max points with jumps of k”. Same problem as LeetCode #3147 Taking Maximum Energy From the Mystic Dungeon.
A board of spaces, each worth positive or negative points. Start on any space and jump exactly
kspaces to the right each time, collecting every space you land on. The game ends when you jump past the last space. Return the maximum total.
Example 1
Input: game_val = [2, -3, 4, 6, 1], k = 2
Output: 7 # land on 2, 4, 1 (start at index 0)
Example 2 (LeetCode #3147)
Input: energy = [5, 2, -10, -5, 1], k = 3
Output: 3 # start at index 1: 2 + 1
Constraints
1 <= n <= 10^6-10^3 <= game_val[i] <= 10^31 <= k <= n
Thinking
- Brute force idea: try every start and walk
i, i+k, i+2k, …to the end: , which is fork = 1. Too slow at . - Key insight: once you pick a start the path is fixed; the only choice is the start. Paths
share tails: the path from 0 is
0, 2, 4and the path from 2 is2, 4. So Fill right to left sodp[i + k]is ready, and returnmax(dp). That is a suffix sum along each chainr, r+k, r+2k, …. - Why starting later can win: every landing is mandatory, so you can’t skip a negative in the middle of a path; starting later drops a bad prefix. You can never drop a bad suffix.
- Edge cases to remember: all negative (best is the single best value among spaces that end
a chain, i.e. the last
kspaces),k = 1(best suffix sum),k = n(no jumps: max element),n = 1.
Solution
Approach A — my version: DP right to left ✅ best practice
Complexity: Time · Space for the copy.
dp = game_val[:] makes a copy (dp = game_val would alias the same list and change the
caller’s input) and also starts every dp[i] at game_val[i], which is already correct for the
last k spaces.
def max_points(game_val: list[int], k: int) -> int:
n = len(game_val)
dp = game_val[:] # dp[i] = total points if you start at i
for i in range(n - 1, -1, -1):
if i + k < n:
dp[i] += dp[i + k]
return max(dp)
Approach B — brute force: simulate every start
Complexity: Time · Space .
sum(game_val[i::k]) is exactly the path from i. Recomputes every shared tail.
def max_points_brute(game_val: list[int], k: int) -> int:
return max(sum(game_val[i::k]) for i in range(len(game_val)))
Approach C — same DP, in place, starting at n - k - 1
Complexity: Time · Space extra, but it overwrites the input.
Spaces n-k … n-1 need no update, so start the loop at n - k - 1 and drop the if. Only do
this when the caller doesn’t need game_val afterwards (or in an interview, say so).
def max_points_inplace(game_val: list[int], k: int) -> int:
for i in range(len(game_val) - k - 1, -1, -1):
game_val[i] += game_val[i + k] # mutates the input
return max(game_val)
Approach D — suffix sums per chain with accumulate (Pythonic)
Complexity: Time · Space per chain.
The board splits into k independent chains game_val[r::k]. The answer for a chain is its
best suffix sum: reverse it and take the best running sum. Slicing and accumulate run in C.
from itertools import accumulate
def max_points_chains(game_val: list[int], k: int) -> int:
return max(max(accumulate(reversed(game_val[r::k]))) for r in range(k))
Scratch / Experiments
Max size with k = 1 (one long chain) and k = 1000 (many short chains). The brute
force is , so it only runs at , k = 1. Approach C mutates, so its timing
includes making a copy (fair: you’d copy too if you needed the input later).
Typical results:
| Approach | , | , | , |
|---|---|---|---|
| A DP ✅ | ~0.34 ms | ~80 ms | ~80 ms |
| B brute force | ~54 ms | (hours) | (seconds–minutes) |
| C in place (+copy) | ~0.26 ms | ~65 ms | ~62 ms |
D accumulate chains | ~0.08 ms | ~20 ms | ~35 ms |
All but B are . D is several times faster because the slicing, the running sum and the
max all happen in C (a bit less so with many short chains, k = 1000); A and C run one Python-level loop iteration per space. C beats A only by
skipping the if.
import random
random.seed(0)
tiny = [random.randint(-1000, 1000) for _ in range(5000)]
big = [random.randint(-1000, 1000) for _ in range(1_000_000)]
def run(fn, vals, k):
return fn(list(vals), k) if fn is max_points_inplace else fn(vals, k)
for label, vals, k, fns in [
("n=5000 k=1 ", tiny, 1, APPROACHES),
("n=1e6 k=1 ", big, 1, [f for f in APPROACHES if f is not max_points_brute]),
("n=1e6 k=1000", big, 1000, [f for f in APPROACHES if f is not max_points_brute]),
]:
for fn in fns:
print(f"{label} {fn.__name__:20s}", end=" ")
%timeit run(fn, vals, k)
Takeaways
- Pattern: “the path from
iis fixed and continues from a later index” → DP from the right:dp[i] = val[i] + dp[next(i)], answer = bestdp[i]. With a fixed jumpkthe board splits intokindependent chains, and the DP is just a suffix sum on each chain. Mandatory landings mean you can only trim the start, never the middle. - Mistakes to avoid:
dp = game_valaliases the input;[:],list(...)or.copy()make a real copy.- Re-walking the path from every start () instead of reusing the shared tail.
- Off-by-one between 1-based problem pictures (“start at index 1”) and 0-based Python.
- Initialising the answer to
0: with all-negative values you still have to play, so the answer can be negative.
- Related problems: Jump Game VI (#1696, jump up to
k→ DP + monotonic deque), Maximum Subarray (#53, same “drop a bad prefix” idea, Kadane), House Robber (#198, DP over a line), Min Cost Climbing Stairs (#746, DP from fixed jumps).