Thoughts on Subarray Sum

A Brief Discussion on the Thought Process for Implementing Subarray Sum

Posted by Byolio on December 15, 2024
🌐 中文 English

Let’s step into the world of subarray sum Test links for this article:

  1. https://leetcode.cn/problems/maximum-subarray/
  2. https://leetcode.cn/problems/house-robber/
  3. https://leetcode.cn/problems/maximum-sum-circular-subarray/
  4. https://leetcode.cn/problems/maximum-sum-of-3-non-overlapping-subarrays/

What is Subarray Sum

Subarray Sum, its core idea is to decompose the original problem into a series of overlapping subproblems, solve these subproblems step by step and store their results to avoid repeated calculations and improve algorithm efficiency. It is essentially an algorithmic idea of trading space for time. In solving problems, it also utilizes the idea of Dynamic Programming (DP for short), often employing techniques like Prefix Sum and Suffix Sum for efficient computation.

Implementing Ordinary Subarray Sum

It is mainly implemented using the DP idea, with two main implementation methods (Test link: https://leetcode.cn/problems/maximum-subarray/):

  1. Ordinary DP implementation
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    class Solution {
    public:
     vector<int> dp;
     int maxSubArray(vector<int>& nums) {
         int n = nums.size();
         dp = vector<int>(n, 0);     //1 <= nums.length <= 105
         dp[0] = nums[0];
         int ans = dp[0];
         for (int i = 1; i < n; ++i)
         {
             dp[i] = max(nums[i], dp[i - 1] + nums[i]);
             ans = max(ans, dp[i]);
         }
         return ans;
     }
    };
    

    Explanation:

    • Since nums.length >= 1, corresponding boundary checks can be omitted.
    • dp[0] = nums[0] is initialized, so starting from i = 1, dp[i - 1] will not go out of bounds.
    • During the DP process, the maximum value is recorded simultaneously to prevent having to traverse the dp array again at the end.
    • ans is initialized to dp[0] to handle cases where nums has only one element or the maximum case includes nums[0].

Comment: The time complexity has been optimized to O(n) through DP, but the space complexity is O(n), which can be further optimized.

  1. Space-optimized DP implementation
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    // Space compression
    class Solution {
    public:
     int pre;   // 1 <= nums.length <= 105
     int maxSubArray(vector<int>& nums) {
         int n = nums.size();
         pre = nums[0];
         int ans = nums[0];
         for (int i = 1; i < n; ++i)
         {
             pre = max(nums[i], pre + nums[i]);
             ans = max(ans, pre);
         }
         return ans;
     }
    };
    

    Explanation:

    • Using pre to replace the dp array for recording dp[i - 1], space complexity is optimized to O(1).

Comment: Its space complexity has been optimized to O(1), and its time complexity is also optimized to O(n). This implementation approach is optimal.

Implementing Non-Circular Subarray Sum

It is also mainly implemented using the DP idea: (Test link: https://leetcode.cn/problems/house-robber/):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<algorithm>
class Solution {
public:
    vector<int> dp;
    int rob(vector<int>& nums) {
        int n = nums.size();
        // Handle special cases separately
        if (n == 1)
        {
            return nums[0];
        }
        if (n == 2)
        {
            return max(nums[0], nums[1]);
        }
        // dp initialization
        dp = vector<int>(n, 0);
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);

        // Main logic
        for (int i = 2; i < n; ++i)
        {
            dp[i] = max(dp[i - 1], max(dp[i], dp[i - 2] + nums[i]));
        }
        return dp[n - 1];
    }
};

Explanation:

  • The separate judgment dp[1] = max(nums[0], nums[1]) handles the cases of robbing house 1 and house 2.
  • Using dp[i] = max(dp[i - 1], max(dp[i], dp[i - 2] + nums[i])) determines the maximum between robbing house i and not robbing house i, taking the maximum of these as the current maximum.

Implementing Circular Subarray Sum

Test link: https://leetcode.cn/problems/maximum-sum-circular-subarray/ This problem has two scenarios:

  1. The maximum subarray is in the middle. Simply use the ordinary subarray sum to find the maximum.
  2. The maximum subarray includes both the head and tail of the array. Then, the total sum minus the minimum subarray sum in the middle gives the maximum.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    #include<climits>
    #include<algorithm>
    class Solution {
    public:
     int maxSubarraySumCircular(vector<int>& nums) {
         int n = nums.size();
         int all = nums[0];
         int maxSum = nums[0];
         int minSum = nums[0];
         for (int i = 1, maxPre = nums[0], minPre = nums[0]; i < n; ++i)
         {
             all += nums[i];
             maxPre = max(nums[i], maxPre + nums[i]);   // When selecting the maximum
             maxSum = max(maxSum, maxPre);
    
             minPre = min(nums[i], minPre + nums[i]);   // When selecting the minimum
             minSum = min(minSum, minPre);   
         }
         return all == minSum ? maxSum : max(maxSum, all - minSum);  
         // all == minSum : All numbers are negative, but the return cannot be an empty array, so only maxSum can be returned.
     }
    };
    

    Explanation: * all - minSum is for the case where the array head and tail need to be taken, maxSum is for the case where they don’t. * all == minSum is the case where all numbers are negative, but the return value cannot be an empty array, so only maxSum is returned.

Implementing Subarray Sum Using Prefix and Suffix Sums

This part I find relatively difficult to understand. It is mainly used for problems requiring the maximum sum of segmented subarrays. Test link: https://leetcode.cn/problems/maximum-sum-of-3-non-overlapping-subarrays/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class Solution {
public:
    vector<int> sums;                       //  The cumulative sum starting from position i for the next k elements
    vector<int> prefix;
    vector<int> suffix;
    vector<int> maxSumOfThreeSubarrays(vector<int>& nums, int k) {
        int n = nums.size();
        sums = vector<int>(n);
        for (int l = 0, r = 0, sum = 0; r < n; ++r)
        {
            sum += nums[r];
            if (r - l + 1 == k)
            {
                sums[l] = sum;
                sum -= nums[l++];
            }
        }
        // prefix : The index of the maximum subarray sum from 0 ~ i
        prefix = vector<int>(n, 0);
        prefix[0] = 0;
        for (int l = 1, r = k; r < n; ++l, ++r)
        {
            if (sums[l] > sums[prefix[r - 1]])  // If the value at the current position's subarray sum array is greater than the previous position's
            {
                prefix[r] = l;                  
            }else{
                prefix[r] = prefix[r - 1];      
            }
        }
        // suffix : The index of the maximum subarray sum from i ~ n - 1, the index value is the ending index
        suffix = vector<int>(n);
        suffix[n - k] = n - k;
        for (int l = n - 1 - k; l >= 0; --l)
        {
            if (sums[l] >= sums[suffix[l + 1]])  // Note it's >= here, because if there are multiple results, return the lexicographically smallest one.
            {
                suffix[l] = l;
            }else{
                suffix[l] = suffix[l + 1];
            }
        }
        int a, b, c, maxVal = 0;
        for (int p, s, i = k, j = 2 * k - 1, sum; j < n - k; ++i, ++j)
        {
            p = prefix[i - 1];
            s = suffix[j + 1];
            sum = sums[p] + sums[i] + sums[s];
            if (sum > maxVal)
            {
                a = p;
                b = i;
                c = s;
                maxVal = sum;
            }
        }
        return {a, b, c};
    }
};

Explanation:

  • It uses the DP idea of recording starting indices with prefix and suffix sums. When arriving at position i, it needs to find the index of the maximum subarray sum from 0 ~ i - 1, and the index of the maximum subarray sum from i + k ~ n - 1. In this scenario, using sum = sums[p] + sums[i] + sums[s]; finds the maximum sum of three subarrays.
  • Note the use of >= in the judgment here, because if there are multiple results, the lexicographically smallest one should be returned.

Comment: The time complexity is O(n), which can be considered a very good implementation.

Summary

  1. The core idea of subarray sum is to decompose the original problem into a series of overlapping subproblems, solve these subproblems step by step and store their results to avoid repeated calculations and improve algorithm efficiency. It is essentially an algorithmic idea of trading space for time.
  2. In solving problems, it also utilizes the idea of Dynamic Programming (DP for short), often employing techniques like Prefix Sum and Suffix Sum for efficient computation.
  3. Implementing subarray sum using prefix and suffix sums to store indices is a relatively difficult-to-understand implementation method. It is mainly used for problems requiring the maximum sum of segmented subarrays.