{x}
blog image

Largest Sum of Averages

You are given an integer array nums and an integer k. You can partition the array into at most k non-empty adjacent subarrays. The score of a partition is the sum of the averages of each subarray.

Note that the partition must use every integer in nums, and that the score is not necessarily an integer.

Return the maximum score you can achieve of all the possible partitions. Answers within 10-6 of the actual answer will be accepted.

 

Example 1:

Input: nums = [9,1,2,3,9], k = 3
Output: 20.00000
Explanation: 
The best choice is to partition nums into [9], [1, 2, 3], [9]. The answer is 9 + (1 + 2 + 3) / 3 + 9 = 20.
We could have also partitioned nums into [9, 1], [2], [3, 9], for example.
That partition would lead to a score of 5 + 2 + 6 = 13, which is worse.

Example 2:

Input: nums = [1,2,3,4,5,6,7], k = 4
Output: 20.50000

 

Constraints:

  • 1 <= nums.length <= 100
  • 1 <= nums[i] <= 104
  • 1 <= k <= nums.length

Solution Explanation for LeetCode 813: Largest Sum of Averages

This problem asks to find the maximum score achievable by partitioning an array into at most k non-empty adjacent subarrays. The score is the sum of the averages of each subarray.

Two solutions are presented: Memoized Search and Dynamic Programming. Both leverage prefix sums for efficient subarray sum calculations.

This approach uses a recursive function dfs(i, k) representing the maximum average sum achievable starting from index i with at most k partitions remaining.

  • Base Cases:

    • i == n (end of array): return 0 (no more partitions possible).
    • k == 1 (only one partition left): return the average of the remaining subarray.
  • Recursive Step:

    • Iterate through possible split points j from i + 1 to n.
    • Calculate the average of the subarray from i to j - 1.
    • Recursively call dfs(j, k - 1) to find the maximum average sum for the remaining partitions.
    • Update the maximum score by adding the current subarray average and the result of the recursive call.
  • Memoization: A cache (@cache in Python, f[][] in other languages) is used to store the results of subproblems, avoiding redundant calculations. This significantly improves efficiency.

Time Complexity: O(n²k). The nested loops iterate through all possible partition points. Memoization reduces the complexity from exponential to polynomial.

Space Complexity: O(nk). The space is dominated by the memoization table.

2. Dynamic Programming

This solution transforms the memoized search into a bottom-up dynamic programming approach.

  • State: f[i][j] represents the maximum average sum achievable using the first i elements and at most j partitions.

  • Base Case: f[i][1] (one partition) is simply the average of the first i elements.

  • Transition: For f[i][j] (j > 1), iterate through possible split points h (0 ≤ h < i). The maximum score is the maximum of f[h][j-1] + (average of elements from h+1 to i).

Time Complexity: O(n²k). Similar to the memoized approach, the complexity arises from the nested loops.

Space Complexity: O(nk). The space is again dominated by the DP table f[i][j].

Code Examples (Python)

Memoized Search:

from functools import cache, lru_cache
 
class Solution:
    def largestSumOfAverages(self, nums: List[int], k: int) -> float:
        n = len(nums)
        s = list(accumulate(nums, initial=0)) #prefix sum
 
        @cache
        def dfs(i: int, k: int) -> float:
            if i == n:
                return 0
            if k == 1:
                return (s[n] - s[i]) / (n - i)
            ans = 0
            for j in range(i + 1, n + 1):
                ans = max(ans, (s[j] - s[i]) / (j - i) + dfs(j, k - 1))
            return ans
 
        return dfs(0, k)
 

Dynamic Programming:

from itertools import accumulate
 
class Solution:
    def largestSumOfAverages(self, nums: List[int], k: int) -> float:
        n = len(nums)
        f = [[0.0] * (k + 1) for _ in range(n + 1)]
        s = list(accumulate(nums, initial=0))
 
        for i in range(1, n + 1):
            f[i][1] = s[i] / i
            for j in range(2, min(i + 1, k + 1)):
                for h in range(i):
                    f[i][j] = max(f[i][j], f[h][j - 1] + (s[i] - s[h]) / (i - h))
        return f[n][k]
 

The code in other languages (Java, C++, Go, TypeScript) follows similar logic, with adjustments for syntax and data structures. The core algorithms remain the same. Choose whichever method (memoized search or dynamic programming) best suits your preference and the specific requirements of your programming environment.