1872. Stone Game VIII
Problem Description
In this game, there are n
stones with different values arranged in a row. Two players, Alice and Bob, take turns playing the game with Alice starting first. The goal for Alice is to maximize her score difference over Bob, and Bob aims to minimize this difference. During each turn, while there is more than one stone remaining, the following steps are taken:
- The player chooses an integer
x > 1
and removes the leftmostx
stones. - The player adds the sum of the removed stones' values to their score.
- A new stone, with value equal to that sum, is placed at the left side of the row.
The game stops when there is only one stone left. The final score difference is calculated as (Alice's score - Bob's score)
. The task is to determine this score difference assuming both players play optimally.
Intuition
The intuition behind solving this problem lies in dynamic programming and the observation that a player will always prefer the option that leads to the maximum difference in their favor at any given point. Since we want to know the outcome when both players act optimally, we can work backward from the end of the game.
The code provided uses 'prefixed sum' (pre_sum
) and formulates the solution based on dynamic programming principles. We begin by calculating the prefix sums of the stones' array, which is used to easily compute the sum of any selected stones during each turn.
As Alice starts first and wants to maximize the difference, she'll choose moves that will leave Bob with the minimum score difference. Meanwhile, Bob will try to do the opposite. However, since Alice starts and makes the first move, she always has an upper hand. This kind of game falls under the umbrella of 'minimax' problems, where players minimize the maximum possible loss.
To find the optimal strategy, we start from the right-most position in the game (which is the second to the last stone since the last stone cannot be chosen due to the x > 1
rule) and move leftward to calculate the optimal score difference at each position considering the current sum and the best outcome of the future state (dynamic programming). The variable f
maintains the maximum possible score difference.
To get the final answer, we loop backwards through the array starting from the second to last stone, and at each point, we consider two possibilities: taking the current prefix sum minus the optimal future score (as if Bob just played), or keeping the previous optimal score (as if Alice is retaining her advantage). The max of these two values gives us Alice's optimal choice at this point, hence the use of max(f, pre_sum[i] - f)
.
After iterating through the stones, the score stored in f
will be the maximum score difference Alice can achieve against Bob when both play optimally.
Learn more about Math, Dynamic Programming and Prefix Sum patterns.
Solution Approach
The solution uses a dynamic programming approach as it computes the optimal score difference at each step by considering the previous steps' outcomes. Dynamic programming is a method for solving complex problems by breaking them down into simpler subproblems, solving each of those subproblems just once, and storing their solutions.
Here's the implementation detail with the algorithms, data structures, and patterns used in the provided Python code:
-
Accumulate Function: The
accumulate
function from Python'sitertools
module is used to create thepre_sum
list, which is a prefix sum array of thestones
. This array helps in calculating the sum of the stones taken in one move quickly.pre_sum = list(accumulate(stones))
-
Dynamic Programming State: The variable
f
is used to maintain the optimal score difference while iterating. It starts with the value of the last prefix sum since this would be the maximum starting score for Alice if she were to take all stones except the first one in her first turn.f = pre_sum[len(stones) - 1]
-
Iterating Backwards: The core of the dynamic programming approach happens when we iterate over the
pre_sum
array in reverse, updating the state off
.for i in range(len(stones) - 2, 0, -1): f = max(f, pre_sum[i] - f)
Here's what happens in the loop:
- We start from the second-to-last stone since one cannot take just the last stone (
x > 1
). - At each step, we're considering two scenarios:
f
: This is the best score difference before this move, considering we're optimizing for Alice.pre_sum[i] - f
: This is the current sum of stones that can be taken subtracted by the best score difference from all future moves (working backwards means we're considering as if Bob takes his turn now, and Alice tries to counter in the future).
- The
max
function is used to select the better option to update the score difference in Alice's favor. This simulates Alice's optimal play as she tries to maximize her score difference from this point onward.
- We start from the second-to-last stone since one cannot take just the last stone (
Finally, f
will represent the optimal score difference Alice can obtain when both players play optimally, and we return this as the final result.
This algorithm is particularly efficient since only a single pass through the game states is necessary after the accumulation, resulting in a time complexity of O(n), where n is the length of the stones
array. This strategy effectively relies on a bottom-up approach to dynamic programming, considering the optimal future states while computing the current state.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution approach with an example where the game starts with a row of stones with values stones = [2, 7, 3, 4]
. Here's how we would apply the algorithm described in the solution approach:
-
Initial Preparations: Compute the prefix sums of the
stones
array to facilitate quick sums.stones: [2, 7, 3, 4] pre_sum: [2, 9, 12, 16] // Prefix sums using the accumulate function
-
Dynamic Programming Initialization: Initialize the variable
f
with the value of the last prefix sum minus the first stone's value, as this is the best score Alice can secure if she takes all stones on her first move.f: 16 - 2 = 14
-
Iterating Backwards: Iterate through the
pre_sum
list from right to left, starting from the second-to-last position.- Iteration 1 (i = 2): Evaluate the score if taking first three stones.
- Current
f
: 14 (stored best score difference so far) - New option:
pre_sum[2] - f = 12 - 14 = -2
(if Bob took a turn, considering optimal future moves) - Alice decides the max value for
f
:max(14, -2) = 14
(Alice will select 14 to maximize her score difference)
- Current
- Iteration 2 (i = 1): Evaluate the score if taking the first two stones.
- Current
f
: 14 (stored best score difference so far) - New option:
pre_sum[1] - f = 9 - 14 = -5
(if Bob took his turn here) - Alice decides the max value for
f
:max(14, -5) = 14
(Alice still selects 14)
- Current
After this iteration, Alice cannot make more decisions since there needs to be more than one stone to play the game.
- Iteration 1 (i = 2): Evaluate the score if taking first three stones.
By the end of the iterations, f
is 14. Since Alice can choose to remove the last three stones in her first turn, leaving the first stone for Bob, this leads to a final score difference of final score = Alice's Score - Bob's Score = 14 - 2 = 12
.
Therefore, using the solution approach described earlier, we determined that the optimal final score difference is 12 in Alice's favor when both Alice and Bob play optimally.
Solution Implementation
1from typing import List
2from itertools import accumulate
3
4class Solution:
5 def stoneGameVIII(self, stones: List[int]) -> int:
6 # Calculate the prefix sum array of the stones list
7 prefix_sum = list(accumulate(stones))
8
9 # Initialize the 'score' variable with the last value of the prefix sum,
10 # which corresponds to the sum of all stones.
11 score = prefix_sum[-1]
12
13 # Reverse iterate over the prefix_sum array starting from the second last element
14 # The loop goes till the second element as the first move can't use only one stone
15 for i in range(len(stones) - 2, 0, -1):
16 # Update the 'score' to be the maximum value between the current 'score'
17 # and the difference between the current prefix_sum and the 'score'
18 # This represents choosing the optimal score after each player's turn
19 score = max(score, prefix_sum[i] - score)
20
21 # Return the final score after both players have played optimally
22 return score
23
1class Solution {
2 public int stoneGameVIII(int[] stones) {
3 // n represents the total number of stones
4 int n = stones.length;
5 // preSum array is used to store the prefix sum of the stones
6 int[] prefixSum = new int[n];
7 // The first element of preSum is the first stone itself
8 prefixSum[0] = stones[0];
9 // Calculate the prefix sum for the entire array of stones
10 for (int i = 1; i < n; ++i) {
11 prefixSum[i] = prefixSum[i - 1] + stones[i];
12 }
13 // f represents the maximum score that can be achieved
14 // Start by assuming the last element of prefixSum is the maximum score
15 int maxScore = prefixSum[n - 1];
16 // Traverse backwards through the stones, updating maxScore
17 for (int i = n - 2; i > 0; --i) {
18 // The maxScore is updated to be the maximum of current maxScore
19 // and the difference of the current prefix sum and maxScore
20 maxScore = Math.max(maxScore, prefixSum[i] - maxScore);
21 }
22 // Return the final calculated maxScore
23 return maxScore;
24 }
25}
26
1#include <vector>
2#include <algorithm>
3
4class Solution {
5public:
6 int stoneGameVIII(std::vector<int>& stones) {
7 // n represents the total number of stones
8 int n = stones.size();
9 // 'prefixSum' vector is used to store the prefix sum of stones
10 std::vector<int> prefixSum(n);
11 // The first element of 'prefixSum' is the first stone itself
12 prefixSum[0] = stones[0];
13 // Calculate the prefix sum for the entire vector of stones
14 for (int i = 1; i < n; ++i) {
15 prefixSum[i] = prefixSum[i - 1] + stones[i];
16 }
17 // 'maxScore' represents the maximum score that can be achieved
18 // Start by assuming the last element of 'prefixSum' is the maximum score
19 int maxScore = prefixSum[n - 1];
20 // Traverse backwards through the stones, updating 'maxScore'
21 for (int i = n - 2; i > 0; --i) {
22 // The 'maxScore' is updated to be the maximum of current 'maxScore'
23 // and the difference of the current prefix sum and 'maxScore'
24 maxScore = std::max(maxScore, prefixSum[i] - maxScore);
25 }
26 // Return the final calculated 'maxScore'
27 return maxScore;
28 }
29};
30
1function stoneGameVIII(stones: number[]): number {
2 // 'n' is the total number of stones.
3 let n: number = stones.length;
4
5 // 'prefixSums' array is used to store the running sum of the stones.
6 let prefixSums: number[] = new Array(n);
7
8 // The first element of 'prefixSums' is the first stone itself.
9 prefixSums[0] = stones[0];
10
11 // Compute the running sum for the entire array of stones.
12 for (let i = 1; i < n; i++) {
13 prefixSums[i] = prefixSums[i - 1] + stones[i];
14 }
15
16 // 'maxScore' represents the maximum score achievable.
17 // Initialize it with the sum of all stones, as if you took all.
18 let maxScore: number = prefixSums[n - 1];
19
20 // Iterate backwards through the stones to update 'maxScore'.
21 for (let i = n - 2; i > 0; i--) {
22 // Update 'maxScore' to be the maximum between the current 'maxScore'
23 // and the sum of stones up to 'i', minus the subsequent 'maxScore'.
24 maxScore = Math.max(maxScore, prefixSums[i] - maxScore);
25 }
26
27 // Return the final calculated 'maxScore'.
28 return maxScore;
29}
30
Time and Space Complexity
Time Complexity
The time complexity of the code is primarily determined by two operations: the calculation of the prefix sums and the iterative process to compute the maximum score that Alice can achieve.
-
Calculating the prefix sums uses
accumulate
from Python's itertools, which iterates through thestones
list once. This operation has a time complexity ofO(n)
, wheren
is the length of thestones
list. -
The for-loop iterates from the second-to-last element to the first non-inclusive, meaning it executes
n - 2
times. Each iteration performs a constant time operation, which is checking and updating the value off
. Therefore, the time complexity of this loop is alsoO(n)
.
Combining these two operations, since they are sequential and not nested, the overall time complexity of the code remains O(n)
.
Space Complexity
The space complexity is influenced by the additional space required for storing the prefix sums and the space for the variable f
.
-
The prefix sums are stored in
pre_sum
, which is a list of the same length asstones
, requiringO(n)
space. -
The variable
f
is a single integer, which takesO(1)
space.
Hence, the total space complexity of the code is O(n)
due to the space needed for the pre_sum
list. The space needed for the integer f
is negligible compared to the size of pre_sum
.
Learn more about how to find time and space complexity quickly using problem constraints.
Which of the following shows the order of node visit in a Breadth-first Search?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
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
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
Want a Structured Path to Master System Design Too? Don’t Miss This!