We need to pick exactly k coins from the tops of various piles to maximize total value. For each pile, we can take zero or more coins from the top, but once we move to the next pile, we cannot return. This naturally suggests a recursive approach: at each pile, try all possible numbers of coins to take (from zero up to the pile size or remaining coins), then recurse on the next pile with fewer coins left to pick. The answer is the maximum across all choices.
dfs(i, coins) that returns the maximum value starting from pile i with coins remaining.i == n, return 0 (no more piles).res = dfs(i + 1, coins).j from 0 to min(coins, len(piles[i])) - 1:j + 1 coins from pile i.res with curPile + dfs(i + 1, coins - (j + 1)).res.dfs(0, k) as the final answer.class Solution:
def maxValueOfCoins(self, piles: List[List[int]], k: int) -> int:
n = len(piles)
def dfs(i, coins):
if i == n:
return 0
res = dfs(i + 1, coins) # skip current pile
curPile = 0
for j in range(min(coins, len(piles[i]))):
curPile += piles[i][j]
res = max(res, curPile + dfs(i + 1, coins - (j + 1)))
return res
return dfs(0, k)Where is the number of piles and is the number of coins to choose.
The plain recursive solution recomputes the same subproblems many times. We can add memoization by storing results for each state (pile index, remaining coins). Once a state is computed, we return the cached result instead of recomputing. This reduces exponential time complexity to polynomial.
dp of size n x (k + 1), initialized to -1.dfs(i, coins) as before, but before computing, check if dp[i][coins] is already computed. If so, return it.dp[i][coins] before returning.dfs(0, k) as the final answer.class Solution:
def maxValueOfCoins(self, piles: List[List[int]], k: int) -> int:
n = len(piles)
dp = [[-1] * (k + 1) for _ in range(n)]
def dfs(i, coins):
if i == n:
return 0
if dp[i][coins] != -1:
return dp[i][coins]
dp[i][coins] = dfs(i + 1, coins) # skip current pile
curPile = 0
for j in range(min(coins, len(piles[i]))):
curPile += piles[i][j]
dp[i][coins] = max(dp[i][coins], curPile + dfs(i + 1, coins - (j + 1)))
return dp[i][coins]
return dfs(0, k)Where is the number of piles, is the number of coins to choose, and is the total number of coins among all the piles.
Instead of recursion with memoization, we can fill the DP table iteratively. We process piles from the last to the first. For each pile and each possible number of remaining coins, we compute the best value by considering all options: skip the pile or take some coins from it. The final answer is stored at dp[0][k].
dp of size (n + 1) x (k + 1), initialized to 0.i from n - 1 down to 0:coins from 0 to k:dp[i][coins] = dp[i + 1][coins] (skip pile i).j from 0 to min(coins, len(piles[i])) - 1:j + 1 coins' value.dp[i][coins] with curPile + dp[i + 1][coins - (j + 1)] if larger.dp[0][k].class Solution:
def maxValueOfCoins(self, piles: List[List[int]], k: int) -> int:
n = len(piles)
dp = [[0] * (k + 1) for _ in range(n + 1)]
for i in range(n - 1, -1, -1):
for coins in range(k + 1):
dp[i][coins] = dp[i + 1][coins]
curPile = 0
for j in range(min(coins, len(piles[i]))):
curPile += piles[i][j]
dp[i][coins] = max(
dp[i][coins],
curPile + dp[i + 1][coins - (j + 1)]
)
return dp[0][k]Where is the number of piles, is the number of coins to choose, and is the total number of coins among all the piles.
In the bottom-up approach, each pile only depends on the results from the next pile. We can reduce space by using a single 1D array of size k + 1. Processing coins in reverse order ensures we do not overwrite values we still need for the current pile's computation.
dp of size k + 1, initialized to 0.piles:coins from k down to 1:j from 0 to min(coins, len(pile)) - 1:j + 1 coins' value.dp[coins] with dp[coins - (j + 1)] + curPile if larger.dp[k].class Solution:
def maxValueOfCoins(self, piles: List[List[int]], k: int) -> int:
dp = [0] * (k + 1)
for pile in piles:
for coins in range(k, 0, -1):
curPile = 0
for j in range(min(coins, len(pile))):
curPile += pile[j]
dp[coins] = max(dp[coins], dp[coins - (j + 1)] + curPile)
return dp[k]Where is the number of piles, is the number of coins to choose, and is the total number of coins among all the piles.
When iterating through coins to take from a pile, it is easy to miscount. Taking j + 1 coins means indices 0 through j, so loops should run from 0 to min(coins, pile.size()) - 1. Mixing up whether j represents the count or the last index causes incorrect state transitions.
The option to take zero coins from a pile (skip it entirely) must be explicitly handled. In the DP transition, initialize the result with dp[i + 1][coins] before iterating over how many coins to take. Missing this case means you cannot skip piles, leading to suboptimal or incorrect answers.
In the space-optimized version, iterating coins from 0 to k instead of from k down to 1 causes you to overwrite values you still need for the current pile. Always iterate in reverse when using a 1D DP array to avoid using updated values prematurely.
When computing the value of taking the top j + 1 coins, you must accumulate the sum incrementally: curPile += pile[j]. Recomputing the sum from scratch each iteration is inefficient, and forgetting to accumulate leads to only counting the single coin at index j rather than all coins from 0 to j.