Leetcode Daily Question 20/08/2024 - 1140. Stone Game II

Problem Description:

Alice and Bob continue their games with piles of stones. There are a number of piles arranged in a row, and each pile has a positive integer number of stones piles[i]. The objective of the game is to end with the most stones. Alice and Bob take turns, with Alice starting first. Initially, M = 1. On each player's turn, that player can take all the stones in the first X remaining piles, where 1 <= X <= 2M. Then, we set M = max(M, X). The game continues until all the stones have been taken. Assuming Alice and Bob play optimally, return the maximum number of stones Alice can get.

Solution:

from typing import List
from functools import lru_cache
class Solution:
  def stoneGamesII(self, piles:List[int]) -> int:
    n = len(piles)
    suffix = [0] * (n+1)
    for i in range(n-1,-1,-1):
      suffix[i] = suffix[i+1] + piles[i]
      
    @lru_cache(None)
    def dp(i,m):
      if i + 2*m >= n:
        return suffix[i]
      
      minScore = float('inf')
      
      for x in range(1, 2*m+1):
        minScore = min(minScore, dp(i+x, max(m,x)))
      return suffix[i] - minScore
    
    return dp(0,1)

Explanation:

My first instinct was to use dp to determine the best strategy to play the game. Let's first explore how we can do this then we shall explore a memorisation method to speed up the dp.

First, let's compute the sum of piles from i to the end of the array for convenience. This way we can immediately know the sum of the remaining piles to be taken to determine the optimum strategy.

def stoneGameII(piles): 
  n = len(piles)
  suffix = [0] * (n+1) #we need 1 extra cell to make the traversal work
  for i in range(n-1,-1,-1):
    suffix[i] = suffix[i+1] + piles[i] #previous cumulative suffix sum + current pile

When it comes to doing the dp, we use the dp structure dp[i][m] where m = M in the question of possible moves to make and i represent the index from the end of the array.

Our strategy is to try taking every pile from 1 to 2*m to decide which path is best to take.

Since we know our opponent also always choose the optimum strategy we can predict what they would do.

def stoneGameII(piles): 
  n = len(piles)
  suffix = [0] * (n+1) #we need 1 extra cell to make the traversal work
  for i in range(n-1,-1,-1):
    suffix[i] = suffix[i+1] + piles[i] #previous cumulative suffix sum + current pile
	
  dp = [[0]*(n+1) for _ in range(n+1)]
  for i in range(n-1,-1,-1):
    for m in range(1, n+1):
      max_stones = 0
      for x in range(1, 2*m + 1):
        if i + x <= n:
        	max_stones = max(max_stones, suffix[i] - dp[i+x][max(m,x)])
      dp[i][m] = max_stones
  return dp[0][1]

This gives a time complexity of O(n^3) and space complexity of O(n^2) since we need to go through every possibility and store results in a list of list.

Now we can do the same dp, but with memorisation to speed up the process. We do this using lru_cache from the functools module in python.

We start as we did, calculating the cumulative sum from each i to the end of piles.

def stoneGameII(piles):
  n = len(piles)
  suffix = [0] * (n+1)
  for i in range(n-1,-1,-1):
    suffix[i] = suffix[i+1] + piles[i]
    
  @lru_cache(None)
  def dp(i,m):
    if i + 2*m >= n: #if the maximum moves we can make reach outside the piles, we take the rest of the piles.
      return suffix[i] 
    
   	minScore = float('inf')
    
    for x in range(1, 2*m+1): #try every possible path
      minScore = min(minScore, dp(i+x, max(m,x)))
    return suffix[i] - minScore
  
  return dp(0,1)

This has maximum time complexity of O(n^3). This is because there are O(n^2) possible states (due to i and m), and for each state, we might do up to O(n) work due to the loop over x.

Space complexity of O(n^2).

The reason why caching is so much faster is because we memorise every path and avoid repeated calculations.

The full solution can be found near the top of the page.

Back