A matrix points describes merit points for n training days and three available activities. Each row represents one day, while columns 0, 1, and 2 represent running, fighting practice, and learning new moves.
Select exactly one activity per day. Consecutive days cannot use the same activity. Return the maximum total merit points across the complete schedule.
Example 1
Input: points = [[1, 2, 5], [3, 1, 1], [3, 3, 3]]
Output: 11
Explanation: Choose points[0][2] = 5 on day 0, points[1][0] = 3 on day 1, and points[2][1] = 3 on day 2. The chosen activity indices are 2, 0, and 1, so no two consecutive days use the same activity. The maximum total is 5 + 3 + 3 = 11.
Example 2
Input: points = [[8, 3, 5]]
Output: 8
Explanation: A single training day has no neighboring-day restriction. Activity index 0 gives the largest available score, 8.
Recursion
A valid schedule makes one small choice per day: select an activity different from the neighboring day's activity. Starting from the final day keeps the first choice open because no later activity exists. Every selected activity leaves the same scheduling problem for the remaining earlier days.
The state solve(day, last) returns the best score from day 0 through day, with activity last forbidden on day. Sentinel value 3 represents no forbidden activity for the initial call. Direct recursion follows every valid branch and reveals the repeated decision clearly.
Algorithm
Define
solve(day, last)so the state records the active day and the activity blocked by the next day, preserving the consecutive-day rule during backward movement.Begin with
solve(n - 1, 3)because sentinel value3leaves all three activities available on the final day.Inspect every activity on day
0exceptlast, then return the largest allowed score because no earlier day remains.Try all three activities on every later day and skip
lastbecause matching the neighboring activity would create an invalid schedule.Add
points[day][activity]tosolve(day - 1, activity)so the chosen activity becomes the restriction for the preceding day.Keep the largest candidate across all valid activities because the goal requires the highest complete schedule score.
Return the largest candidate to the preceding recursive state, allowing every earlier choice to compare complete valid totals.
Dry Run
Diagram 1
Solution
#include <bits/stdc++.h>using namespace std;class Solution {private: // Finds the best score for one recursive state. int solve(int day, int last, vector<vector<int>>& points) { // Stop at day zero because no earlier day remains. if (day == 0) { int best = 0; // Check every activity to find the best valid start. for (int activity = 0; activity < 3; activity++) { // Skip the blocked activity to prevent repetition. if (activity != last) { // Keep the strongest allowed first-day score. best = max(best, points[0][activity]); } } // Return the best valid score for the first day. return best; } int best = 0; // Explore every activity because each choice can win. for (int activity = 0; activity < 3; activity++) { // Skip the blocked activity to preserve validity. if (activity != last) { // Add the chosen score to the earlier schedule. int candidate = points[day][activity] + solve(day - 1, activity, points); // Keep the largest complete valid total. best = max(best, candidate); } } // Return the strongest schedule for the state. return best; }public: // Returns the maximum merit points for all days. int ninjaTraining(vector<vector<int>>& points) { int n = points.size(); // Sentinel three leaves the final day unrestricted. return solve(n - 1, 3, points); }};// Driver codeint main() { vector<vector<int>> points = {{1, 2, 5}, {3, 1, 1}, {3, 3, 3}}; Solution obj; cout << obj.ninjaTraining(points) << '\n'; return 0;}Complexity Analysis
Time Complexity: O(2N), where N is the number of training days, because each non-base state can branch into two valid activity choices after the first activity is selected.
Space Complexity: O(N), because the recursion stack can contain at most one active call for each training day.
Memoization
Direct recursion reaches identical (day, last) states through different activity sequences. Every repeated state produces the same answer, so recalculating a complete subtree adds work without adding information.
A two-dimensional array named dp stores one answer for every day and forbidden activity. A saved value turns an entire repeated subtree into a constant-time lookup. The recursive choice and state meaning remain unchanged.
Algorithm
Keep the recursive state
solve(day, last)unchanged so memoization optimizes repeated work without altering the scheduling decision.Create
dpwithnrows and four columns, filled with-1, so every untouched entry clearly marks an uncalculated state.Start from
solve(n - 1, 3)because sentinel value3leaves the final day free from an activity restriction.Return
dp[day][last]immediately after finding a stored value, avoiding another traversal of the same recursive subtree.Evaluate day
0by selecting the largest activity score different fromlast, then store the base answer so later calls can reuse the completed state.Explore every allowed activity on later days and combine the current score with
solve(day - 1, activity), preserving the original recurrence.Store and return the largest candidate in
dp[day][last], making the completed state reusable across later branches.
Dry Run
Diagram 1
Solution
#include <bits/stdc++.h>using namespace std;class Solution {private: // Finds and stores the best score for one state. int solve( int day, int last, vector<vector<int>>& points, vector<vector<int>>& dp ) { // Reuse a saved answer to avoid repeated work. if (dp[day][last] != -1) { return dp[day][last]; } // Stop at day zero because no earlier day remains. if (day == 0) { int best = 0; // Check every activity to find the best valid start. for (int activity = 0; activity < 3; activity++) { // Skip the blocked activity to prevent repetition. if (activity != last) { // Keep the strongest allowed first-day score. best = max(best, points[0][activity]); } } // Store the base answer for later cache hits. dp[day][last] = best; return dp[day][last]; } int best = 0; // Explore every activity because each choice can win. for (int activity = 0; activity < 3; activity++) { // Skip the blocked activity to preserve validity. if (activity != last) { // Add the chosen score to the cached subproblem. int candidate = points[day][activity] + solve(day - 1, activity, points, dp); // Keep the largest complete valid total. best = max(best, candidate); } } // Store the completed state before returning. dp[day][last] = best; return dp[day][last]; }public: // Returns the maximum merit points for all days. int ninjaTraining(vector<vector<int>>& points) { int n = points.size(); // Minus one marks every state as uncalculated. vector<vector<int>> dp(n, vector<int>(4, -1)); // Sentinel three leaves the final day unrestricted. return solve(n - 1, 3, points, dp); }};// Driver codeint main() { vector<vector<int>> points = {{1, 2, 5}, {3, 1, 1}, {3, 3, 3}}; Solution obj; cout << obj.ninjaTraining(points) << '\n'; return 0;}Complexity Analysis
Time Complexity: O(N), where N is the number of training days, because each day has at most four states and each state checks only three activities.
Space Complexity: O(N), because the N × 4 DP cache and the recursion stack of depth at most N both require linear space.
Tabulation
Memoization requests states from the final day and solves earlier states on demand. Tabulation reverses the flow: day 0 is filled first, followed by each later day. Every required earlier answer is already available before a new row begins.
The table dp[day][last] keeps the same meaning as the recursive state. Four values are stored per day because three values describe a forbidden activity and value 3 describes no restriction. Bottom-up order removes recursive calls while preserving the same choices.
Algorithm
Create a table
dpwithnrows and four columns so every recursive(day, last)state has one iterative position.Fill the four states for day
0with the best score outside each forbidden activity so every later transition begins from a complete base row.Move from day
1through dayn - 1because every current state depends only on values from the preceding row.Examine all four
lastvalues for each day so restricted states and the unrestricted sentinel state remain available.Try each activity different from
lastbecause equal values would repeat an activity across neighboring days.Add the current activity score to
dp[day - 1][activity]because the selected activity becomes the preceding row's restriction.Store the largest candidate in
dp[day][last]so every state keeps the best total, then returndp[n - 1][3]because sentinel value3removes the outer restriction.
Dry Run
Ninjas Training Tabulation
Solution
#include <bits/stdc++.h>using namespace std;class Solution {public: // Returns the maximum merit points with tabulation. int ninjaTraining(vector<vector<int>>& points) { int n = points.size(); // Four columns preserve every forbidden choice. vector<vector<int>> dp(n, vector<int>(4, 0)); // Build the base row from valid first-day choices. dp[0][0] = max(points[0][1], points[0][2]); dp[0][1] = max(points[0][0], points[0][2]); dp[0][2] = max(points[0][0], points[0][1]); dp[0][3] = max(points[0][0], max(points[0][1], points[0][2])); // Move forward because every earlier row is ready. for (int day = 1; day < n; day++) { // Calculate all four forbidden-activity states. for (int last = 0; last < 4; last++) { // Try every activity because any choice can win. for (int activity = 0; activity < 3; activity++) { // Skip repetition to keep the schedule valid. if (activity != last) { // Add the score to the matching prior state. int candidate = points[day][activity] + dp[day - 1][activity]; // Keep the largest total for the state. dp[day][last] = max( dp[day][last], candidate ); } } } } // Sentinel three represents no final restriction. return dp[n - 1][3]; }};// Driver codeint main() { vector<vector<int>> points = {{1, 2, 5}, {3, 1, 1}, {3, 3, 3}}; Solution obj; cout << obj.ninjaTraining(points) << '\n'; return 0;}Complexity Analysis
Time Complexity: O(N), where N is the number of training days, because each day computes four states and each state checks three activities, which is constant work.
Space Complexity: O(N), because the DP table stores four values for each of the N days, while the iterative approach uses no recursion stack.
Space Optimization
Every tabulation transition reads only the preceding row. Rows older than one day never participate in a later calculation, so a full n-row table stores unnecessary history.
An array named previous holds the completed row, while current collects the next row. Every current value must be calculated before the row shift. Afterward, current becomes previous, preserving exactly the data needed by the next day.
Algorithm
Store the four day
0states inpreviousbecause every later calculation needs only the immediately preceding row.Move from day
1through dayn - 1because every state depends only on the completed preceding row.Create a fresh four-value
currentrow for each day so unfinished values cannot overwrite required values inprevious.Examine every
lastvalue and every activity different fromlast, preserving all four state meanings and the consecutive-day restriction.Add
points[day][activity]toprevious[activity]because the chosen activity identifies the required state from the preceding day.Keep the largest candidate in
current[last]so no unfinished state replaces required values fromprevious.Shift
currentintopreviousonly after the full row is ready because every next-day transition needs a complete row, then returnprevious[3]as the unrestricted answer.
Dry Run
Ninjas Training Space Optimization
Solution
#include <bits/stdc++.h>using namespace std;class Solution {public: // Returns the maximum score with two small rows. int ninjaTraining(vector<vector<int>>& points) { int n = points.size(); // Previous stores every first-day state. vector<int> previous(4, 0); previous[0] = max(points[0][1], points[0][2]); previous[1] = max(points[0][0], points[0][2]); previous[2] = max(points[0][0], points[0][1]); previous[3] = max(points[0][0], max(points[0][1], points[0][2])); // Move forward because only the prior row is needed. for (int day = 1; day < n; day++) { vector<int> current(4, 0); // Calculate every current state before the shift. for (int last = 0; last < 4; last++) { // Try every activity because any choice can win. for (int activity = 0; activity < 3; activity++) { // Skip repetition to keep the schedule valid. if (activity != last) { // Add the score to the matching prior state. int candidate = points[day][activity] + previous[activity]; // Keep the largest total for the state. current[last] = max( current[last], candidate ); } } } // Shift only after every current state is ready. previous = current; } // Sentinel three represents no final restriction. return previous[3]; }};// Driver codeint main() { vector<vector<int>> points = {{1, 2, 5}, {3, 1, 1}, {3, 3, 3}}; Solution obj; cout << obj.ninjaTraining(points) << '\n'; return 0;}Complexity Analysis
Time Complexity: O(N), where N is the number of days, because each day computes four states and each state checks three activities, which is constant work.
Space Complexity: O(1), because only two arrays of four values are maintained, independent of N.
Interview follow-up Questions
No. State value 3 acts only as a sentinel for no forbidden activity. Real activity indices remain 0, 1, and 2.
Be the first to add a comment.