Leetcode: 1563. Stone Game V

Check out the problem statement here.

Slow Dynamic Programming

Be \(s(i,j)\) the sum of stone values from \(i\) to \(j\), and be \(f(i, j)\) the max score Alice can obtain using stones \(i..j\). When \(i=j\), the game has finished and the score is 0. Otherwise, \(i

With \(n\) being the number of stones, we have that

  • Time complexity: \(O(n^3)\)
  • Space complexity: \(O(n^2)\)
from functools import cache
from itertools import accumulate
from typing import List


def solve(a):
    ps = list(accumulate(a))

    def sum(s, e):
        return ps[e] - ps[s - 1] if s > 0 else ps[e]

    @cache
    def rec(s, e):
        if s == e:
            return 0

        ans = 0
        for m in range(s + 1, e + 1):
            l = sum(s, m - 1)
            r = sum(m, e)
            if l > r:
                ans = max(ans, r + rec(m, e))
            elif r > l:
                ans = max(ans, l + rec(s, m - 1))
            else:
                ans = max(
                    ans,
                    r + rec(m, e),
                    l + rec(s, m - 1)
                )
        return ans

    return rec(0, len(ps) - 1)

assert solve([6,2,3,4,5,5]) == 18
assert solve([7,7,7,7,7,7,7]) == 28
assert solve([4]) == 0


class Solution:
    def stoneGameV(self, stoneValue: List[int]) -> int:
        return solve(stoneValue)


assert Solution().stoneGameV([6,2,3,4,5,5]) == 18

Dynamic Programming on multiple optimizations

To speed up the solution above, we have to avoid checking all possible intervals when computing \(f(i,j)\). But, how could we do that? Is the cubic solution using all constraints of the problem? The answer is "No! The cubic solution is not using the fact that stone values are integers greater than 0". How does this constraint help us when deciding where to split an interval? Let's take \([1, 2, 1, 1, 1, 1]\) as example and \(i=0\) (first stone) and \(j=5\) (last stone). These are all ways to split on an index \(k\):

k Left interval sum Right Interval sum
  \(s(i,k-1)\) \(s(k, j)\)
1 1 6
2 3 4
3 4 3
4 5 2
5 6 1
6 7 0

Note that for \(k<3\), the left side sum is always smaller than the right side sum. For \(k>=3\), the left side sum is always greater than the right side sum. Such \(k\) will always exist due the constraint of the problem and can be found using a Binary Search. Be \(l(i, j)=\max(s(i,j) + f(i, j), l(i, j - 1))\) for \(i

  • Time complexity: \(O(n^2\times\log(n))\)
    • \(l\) and \(r\) has time complexity of \(O(n^2)\), and
    • finding \(k\) is \(O(\log(n))\).
  • Space complexity: \(O(n^2)\).
from functools import cache
from itertools import accumulate
from typing import List

def sum(ps, s, e):
    if s > e:
        return 0
    assert s <= e
    assert s >= 0
    return ps[e] - ps[s - 1] if s > 0 else ps[e]


def search(ps, a, b):
    s, e = a, b
    while s <= e:
        m = s + (e - s) // 2
        l = sum(ps, a, m)
        r = sum(ps, m + 1, b)
        assert l + r == sum(ps, a, b), (l + r, sum(ps, a, b))
        if l >= r:
            e = m - 1
        else:
            s = m + 1
    return s

assert search(list(accumulate([1, 1, 1, 1])), 0, 3) == 1
assert search(list(accumulate([1, 1, 3, 1])), 0, 3) == 2
assert search(list(accumulate([4, 1, 1, 1])), 0, 3) == 0

def solve(a):
    ps = list(accumulate(a))

    @cache
    def left(s, e):
        if s > e:
            return 0
        assert s >= 0
        assert e < len(a)
        return max(sum(ps, s, e) + rec(s, e), left(s, e - 1))

    @cache
    def right(s, e):
        if s > e:
            return 0
        assert s >= 0
        assert e < len(a)
        return max(sum(ps, s, e) + rec(s, e), right(s + 1, e))

    @cache
    def rec(s, e):
        if s == e:
            return 0

        m = search(ps, s, e)
        l = sum(ps, s, m)
        r = sum(ps, m + 1, e)
        ans = 0
        if l == r:
            return max(left(s, m), right(m + 1, e))
        else:
            return max(left(s, m - 1), right(m + 1, e))
        return ans

    return rec(0, len(ps) - 1)

assert solve([6,2,3,4,5,5]) == 18, solve([6,2,3,4,5,5])
assert solve([7,7,7,7,7,7,7]) == 28
assert solve([4]) == 0


class Solution:
    def stoneGameV(self, stoneValue: List[int]) -> int:
        return solve(stoneValue)


assert Solution().stoneGameV([6,2,3,4,5,5]) == 18

Cited by 1