2140. Solving Questions With Brainpower
Problem Description
You are given a list of exam questions, where each question has two properties: the points you can earn by solving it, and the "brainpower" cost that determines how many subsequent questions you must skip if you solve it.
The questions are represented as a 2D array questions
, where questions[i] = [points_i, brainpower_i]
:
points_i
is the number of points you earn if you solve questioni
brainpower_i
is the number of questions immediately after questioni
that you cannot solve if you choose to solve questioni
You must process the questions in order (starting from question 0) and for each question, you have two choices:
- Solve it: You earn
points_i
points, but you must skip the nextbrainpower_i
questions - Skip it: You don't earn any points from this question, but you can move on to consider the next question
For example, with questions = [[3, 2], [4, 3], [4, 4], [2, 5]]
:
- If you solve question 0, you earn 3 points but must skip questions 1 and 2 (the next 2 questions due to brainpower = 2)
- If you skip question 0 and solve question 1, you earn 4 points but must skip questions 2 and 3 (the next 3 questions due to brainpower = 3)
The goal is to find the maximum total points you can earn by strategically choosing which questions to solve and which to skip.
Intuition
The key insight is that at each question, we face a decision: solve it or skip it. This binary choice structure naturally leads us to think about dynamic programming or recursion.
Let's think about this problem from any position i
in the questions array. At position i
, we need to make the optimal decision to maximize our total points from this position onwards. We have two options:
-
Solve question
i
: We gainpoints[i]
points immediately, but then we're forced to skip the nextbrainpower[i]
questions. So our next decision point would be at positioni + brainpower[i] + 1
. -
Skip question
i
: We don't gain any points from this question, but we can immediately move to the next question at positioni + 1
.
The maximum points we can get from position i
onwards is the better of these two choices:
points[i] + maxPoints(i + brainpower[i] + 1)
if we solve itmaxPoints(i + 1)
if we skip it
This recursive structure reveals that we're dealing with overlapping subproblems. For instance, different decision paths might lead us to the same question index later on. If we solve question 0 and it forces us to skip to question 3, or if we skip questions 0, 1, and 2 to reach question 3, we'd be calculating the maximum points from question 3 onwards multiple times.
To avoid redundant calculations, we can use memoization to cache the results. This transforms our solution into a dynamic programming approach where dfs(i)
represents the maximum points obtainable starting from question i
, and we work our way backwards through the questions, building up our solution from the base case (when i >= n
, meaning no more questions left, return 0).
Learn more about Dynamic Programming patterns.
Solution Approach
We implement the solution using a top-down dynamic programming approach with memoization. Here's how the implementation works:
1. Define the Recursive Function
We create a function dfs(i)
that calculates the maximum points obtainable starting from the i
-th question. This function represents our state at each position in the questions array.
2. Base Case
When i >= len(questions)
, we've gone beyond the last question, meaning there are no more questions to process. In this case, we return 0
as no more points can be earned.
3. Recursive Case
For each valid question index i
, we extract:
p
: the points for solving this question (questions[i][0]
)b
: the brainpower cost, i.e., number of questions to skip (questions[i][1]
)
Then we calculate:
max(p + dfs(i + b + 1), dfs(i + 1))
This represents choosing the maximum between:
- Solving the current question: gain
p
points and jump to questioni + b + 1
(skippingb
questions after the current one) - Skipping the current question: gain no points but move to the next question
i + 1
4. Memoization with @cache
The @cache
decorator automatically memorizes the results of dfs(i)
for each value of i
. This prevents redundant calculations when the same state is reached through different paths. For example, if multiple decision sequences lead to evaluating question 5, we only calculate dfs(5)
once.
5. Return the Result
We start the recursion with dfs(0)
, which gives us the maximum points starting from the first question (index 0).
The time complexity is O(n)
where n
is the number of questions, as each state is computed at most once due to memoization. The space complexity is also O(n)
for the recursion stack and the cache storage.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with questions = [[3, 2], [4, 3], [4, 4], [2, 5]]
.
We'll trace through the recursive calls starting from dfs(0)
:
Step 1: dfs(0) - Processing question 0 [3, 2]
- Option 1: Solve it → earn 3 points, skip 2 questions, continue from index 3
- This gives us:
3 + dfs(3)
- This gives us:
- Option 2: Skip it → earn 0 points, continue from index 1
- This gives us:
dfs(1)
- This gives us:
- Return:
max(3 + dfs(3), dfs(1))
Step 2: dfs(3) - Processing question 3 [2, 5]
- Option 1: Solve it → earn 2 points, skip 5 questions, continue from index 9
- Since index 9 >= 4 (out of bounds),
dfs(9) = 0
- This gives us:
2 + 0 = 2
- Since index 9 >= 4 (out of bounds),
- Option 2: Skip it → continue from index 4
- Since index 4 >= 4 (out of bounds),
dfs(4) = 0
- This gives us:
0
- Since index 4 >= 4 (out of bounds),
- Return:
max(2, 0) = 2
Step 3: dfs(1) - Processing question 1 [4, 3]
- Option 1: Solve it → earn 4 points, skip 3 questions, continue from index 5
- Since index 5 >= 4 (out of bounds),
dfs(5) = 0
- This gives us:
4 + 0 = 4
- Since index 5 >= 4 (out of bounds),
- Option 2: Skip it → continue from index 2
- This gives us:
dfs(2)
- This gives us:
- Return:
max(4, dfs(2))
Step 4: dfs(2) - Processing question 2 [4, 4]
- Option 1: Solve it → earn 4 points, skip 4 questions, continue from index 7
- Since index 7 >= 4 (out of bounds),
dfs(7) = 0
- This gives us:
4 + 0 = 4
- Since index 7 >= 4 (out of bounds),
- Option 2: Skip it → continue from index 3
- We already computed
dfs(3) = 2
(memoization helps here!) - This gives us:
2
- We already computed
- Return:
max(4, 2) = 4
Backtracking the results:
dfs(2) = 4
dfs(1) = max(4, 4) = 4
dfs(3) = 2
dfs(0) = max(3 + 2, 4) = max(5, 4) = 5
The maximum points we can earn is 5, achieved by solving question 0 (earning 3 points), skipping questions 1 and 2 (due to brainpower cost), and then solving question 3 (earning 2 points).
Solution Implementation
1from typing import List
2from functools import cache
3
4class Solution:
5 def mostPoints(self, questions: List[List[int]]) -> int:
6 """
7 Solve the "Maximum Points From Questions" problem using dynamic programming.
8
9 Each question has [points, brainpower] where:
10 - points: the points gained if we solve this question
11 - brainpower: the number of questions we must skip after solving this one
12
13 Args:
14 questions: List of [points, brainpower] pairs
15
16 Returns:
17 Maximum points that can be obtained
18 """
19
20 @cache
21 def dfs(index: int) -> int:
22 """
23 Recursively calculate maximum points starting from given index.
24
25 Args:
26 index: Current question index to consider
27
28 Returns:
29 Maximum points achievable from this index onwards
30 """
31 # Base case: if we've gone past all questions, return 0
32 if index >= len(questions):
33 return 0
34
35 # Extract points and brainpower for current question
36 points, brainpower = questions[index]
37
38 # Two choices:
39 # 1. Solve current question: gain points, skip brainpower questions
40 solve_current = points + dfs(index + brainpower + 1)
41
42 # 2. Skip current question: move to next question
43 skip_current = dfs(index + 1)
44
45 # Return maximum of both choices
46 return max(solve_current, skip_current)
47
48 # Start solving from the first question (index 0)
49 return dfs(0)
50
1class Solution {
2 // Total number of questions
3 private int totalQuestions;
4
5 // Memoization array to store computed results for each question index
6 private Long[] memo;
7
8 // Reference to the input questions array
9 private int[][] questionsArray;
10
11 /**
12 * Calculates the maximum points that can be earned from solving questions.
13 * Each question has points and a brainpower value that determines how many
14 * questions must be skipped after solving it.
15 *
16 * @param questions 2D array where questions[i] = [points, brainpower]
17 * @return Maximum points that can be earned
18 */
19 public long mostPoints(int[][] questions) {
20 // Initialize instance variables
21 this.totalQuestions = questions.length;
22 this.memo = new Long[totalQuestions];
23 this.questionsArray = questions;
24
25 // Start DFS from the first question (index 0)
26 return calculateMaxPoints(0);
27 }
28
29 /**
30 * Recursive helper method to calculate maximum points starting from index i.
31 * Uses memoization to avoid redundant calculations.
32 *
33 * @param currentIndex Current question index being considered
34 * @return Maximum points achievable starting from currentIndex
35 */
36 private long calculateMaxPoints(int currentIndex) {
37 // Base case: if current index is beyond array bounds, return 0
38 if (currentIndex >= totalQuestions) {
39 return 0;
40 }
41
42 // Check if result for this index is already computed (memoization)
43 if (memo[currentIndex] != null) {
44 return memo[currentIndex];
45 }
46
47 // Extract points and brainpower for current question
48 int points = questionsArray[currentIndex][0];
49 int brainpower = questionsArray[currentIndex][1];
50
51 // Two choices:
52 // 1. Solve current question: gain points and skip next 'brainpower' questions
53 long solveCurrentQuestion = points + calculateMaxPoints(currentIndex + brainpower + 1);
54
55 // 2. Skip current question: move to next question
56 long skipCurrentQuestion = calculateMaxPoints(currentIndex + 1);
57
58 // Store and return the maximum of both choices
59 memo[currentIndex] = Math.max(solveCurrentQuestion, skipCurrentQuestion);
60 return memo[currentIndex];
61 }
62}
63
1class Solution {
2public:
3 long long mostPoints(vector<vector<int>>& questions) {
4 int n = questions.size();
5
6 // dp[i] represents the maximum points obtainable starting from question i
7 vector<long long> dp(n, 0);
8
9 // Define recursive function with memoization using lambda
10 // dfs(i) returns the maximum points achievable starting from index i
11 function<long long(int)> dfs = [&](int i) -> long long {
12 // Base case: if we've gone past all questions, return 0
13 if (i >= n) {
14 return 0;
15 }
16
17 // If already computed, return memoized result
18 if (dp[i] != 0) {
19 return dp[i];
20 }
21
22 // Extract points and brainpower for current question
23 int points = questions[i][0];
24 int brainpower = questions[i][1];
25
26 // We have two choices:
27 // 1. Solve current question and skip the next 'brainpower' questions
28 long long solveCurrentQuestion = points + dfs(i + brainpower + 1);
29
30 // 2. Skip current question and move to the next one
31 long long skipCurrentQuestion = dfs(i + 1);
32
33 // Store and return the maximum of both choices
34 dp[i] = max(solveCurrentQuestion, skipCurrentQuestion);
35 return dp[i];
36 };
37
38 // Start solving from the first question (index 0)
39 return dfs(0);
40 }
41};
42
1/**
2 * Finds the maximum points that can be earned from solving questions
3 * Each question has points and a brainpower value indicating how many questions to skip
4 * @param questions - 2D array where questions[i] = [points, brainpower]
5 * @returns Maximum points that can be earned
6 */
7function mostPoints(questions: number[][]): number {
8 const questionCount: number = questions.length;
9 // Memoization array to store results of subproblems
10 const memo: number[] = Array(questionCount).fill(0);
11
12 /**
13 * Recursive helper function with memoization to calculate maximum points
14 * @param currentIndex - Current question index being considered
15 * @returns Maximum points achievable starting from currentIndex
16 */
17 const calculateMaxPoints = (currentIndex: number): number => {
18 // Base case: if we've gone past all questions
19 if (currentIndex >= questionCount) {
20 return 0;
21 }
22
23 // Return memoized result if already calculated
24 if (memo[currentIndex] > 0) {
25 return memo[currentIndex];
26 }
27
28 // Extract points and brainpower for current question
29 const [points, brainpower] = questions[currentIndex];
30
31 // Two choices:
32 // 1. Solve current question and skip 'brainpower' questions
33 const solveCurrentQuestion: number = points + calculateMaxPoints(currentIndex + brainpower + 1);
34 // 2. Skip current question and move to next
35 const skipCurrentQuestion: number = calculateMaxPoints(currentIndex + 1);
36
37 // Store and return the maximum of both choices
38 memo[currentIndex] = Math.max(solveCurrentQuestion, skipCurrentQuestion);
39 return memo[currentIndex];
40 };
41
42 // Start solving from the first question
43 return calculateMaxPoints(0);
44}
45
Time and Space Complexity
The time complexity is O(n)
, where n
is the number of questions. This is achieved through memoization with the @cache
decorator. Each state i
(representing the index of the current question) is computed at most once. Since there are n
possible states (indices from 0
to n-1
), and each state performs O(1)
operations (comparing two values and making recursive calls), the total time complexity is O(n)
.
The space complexity is O(n)
, where n
is the number of questions. This consists of two components:
- The recursion call stack can go up to depth
O(n)
in the worst case when we skip every question (callingdfs(i+1)
repeatedly). - The memoization cache stores up to
n
different states (one for each possible index value).
Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Index Calculation Error When Solving a Question
The Pitfall: A frequent mistake is incorrectly calculating the next available question index after solving the current one. Developers often write:
dfs(index + brainpower)
instead ofdfs(index + brainpower + 1)
This error stems from misunderstanding what brainpower
represents. If brainpower = 2
, it means we skip the next 2 questions AFTER the current one, not including the current question itself.
Example of the Bug:
# INCORRECT solve_current = points + dfs(index + brainpower) # Wrong!
With questions = [[3, 2], [4, 3], [4, 4], [2, 5]]
:
- Solving question 0 (index 0) with brainpower 2 should skip to question 3 (index 3)
- The correct calculation: next_index = 0 + 2 + 1 = 3
- The incorrect calculation: next_index = 0 + 2 = 2 (would incorrectly allow solving question 2)
Solution:
Always use index + brainpower + 1
to correctly skip past the required number of questions:
# CORRECT solve_current = points + dfs(index + brainpower + 1)
2. Missing or Incorrect Memoization
The Pitfall: Without memoization, the recursive solution has exponential time complexity O(2^n) because the same subproblems are solved multiple times. Some developers might:
- Forget to add memoization entirely
- Try to implement manual memoization but make errors in the dictionary management
- Use memoization incorrectly with mutable default arguments
Example of the Bug:
# INCORRECT - No memoization
def mostPoints(self, questions):
def dfs(index):
if index >= len(questions):
return 0
points, brainpower = questions[index]
return max(points + dfs(index + brainpower + 1), dfs(index + 1))
return dfs(0)
Solution:
Use Python's @cache
decorator for automatic memoization, or implement a proper memoization dictionary:
# Option 1: Using @cache (recommended)
@cache
def dfs(index):
# ... implementation
# Option 2: Manual memoization
def mostPoints(self, questions):
memo = {}
def dfs(index):
if index in memo:
return memo[index]
if index >= len(questions):
return 0
points, brainpower = questions[index]
memo[index] = max(points + dfs(index + brainpower + 1), dfs(index + 1))
return memo[index]
return dfs(0)
3. Incorrect Base Case Handling
The Pitfall:
Using index == len(questions)
instead of index >= len(questions)
in the base case. This causes an index out of bounds error when the brainpower value causes us to skip beyond the array length.
Example of the Bug:
# INCORRECT
if index == len(questions): # This will miss cases where index > len(questions)
return 0
If we have 4 questions and solve question 2 with brainpower 3, we jump to index 2 + 3 + 1 = 6, which is greater than 4. The equality check would fail and try to access questions[6]
, causing an error.
Solution:
Always use >=
to handle all cases where we've moved past the valid question range:
# CORRECT
if index >= len(questions):
return 0
What does the following code do?
1def f(arr1, arr2):
2 i, j = 0, 0
3 new_arr = []
4 while i < len(arr1) and j < len(arr2):
5 if arr1[i] < arr2[j]:
6 new_arr.append(arr1[i])
7 i += 1
8 else:
9 new_arr.append(arr2[j])
10 j += 1
11 new_arr.extend(arr1[i:])
12 new_arr.extend(arr2[j:])
13 return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2 int i = 0, j = 0;
3 List<Integer> newArr = new ArrayList<>();
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.add(arr1[i]);
8 i++;
9 } else {
10 newArr.add(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.add(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.add(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
1function f(arr1, arr2) {
2 let i = 0, j = 0;
3 let newArr = [];
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.push(arr1[i]);
8 i++;
9 } else {
10 newArr.push(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.push(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.push(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!