Subarrays Distinct Element Sum of Squares II
HardPrompt
You are given a 0-indexed integer array nums.
The distinct count of a subarray of nums is defined as:
- Let
nums[i..j]be a subarray ofnumsconsisting of all the indices fromitojsuch that0 <= i <= j < nums.length. Then the number of distinct values innums[i..j]is called the distinct count ofnums[i..j].
Return the sum of the squares of distinct counts of all subarrays of nums.
Since the answer may be very large, return it modulo 109 + 7.
A subarray is a contiguous non-empty sequence of elements within an array.
Example 1:
Input: nums = [1,2,1]
Output: 15
Explanation: Six possible subarrays are:
[1]: 1 distinct value
[2]: 1 distinct value
[1]: 1 distinct value
[1,2]: 2 distinct values
[2,1]: 2 distinct values
[1,2,1]: 2 distinct values
The sum of the squares of the distinct counts in all subarrays is equal to 12 + 12 + 12 + 22 + 22 + 22 = 15.Example 2:
Input: nums = [2,2]
Output: 3
Explanation: Three possible subarrays are:
[2]: 1 distinct value
[2]: 1 distinct value
[2,2]: 1 distinct value
The sum of the squares of the distinct counts in all subarrays is equal to 12 + 12 + 12 = 3.
Constraints:
1 <= nums.length <= 1051 <= nums[i] <= 105
Approaches
3 approaches with complexity analysis and trade-offs.
This is the most straightforward and naive approach. It involves generating every possible subarray, and for each subarray, counting the number of distinct elements. The square of this count is then added to a running total. This method is easy to conceptualize but highly inefficient.
Algorithm
- Initialize
total_sumto 0. - Iterate through all possible start indices
ifrom0ton-1. - Iterate through all possible end indices
jfromiton-1.- This defines a subarray
nums[i..j]. - Create a new
HashSetto store elements of this subarray. - Iterate from
k = itojand addnums[k]to theHashSet. - Get the size of the
HashSet, which is the distinct countd. - Add
d*dtototal_sum, taking modulo10^9 + 7.
- This defines a subarray
- Return
total_sum.
Walkthrough
The brute-force approach uses three nested loops to solve the problem. The outer two loops, with indices i and j, are used to define the start and end of every possible subarray nums[i..j]. For each of these subarrays, a third loop iterates from i to j. Inside this innermost loop, we use a HashSet to keep track of the unique elements encountered within the current subarray. After the inner loop completes, the size of the HashSet gives us the distinct count, d. We then compute d*d and add it to our total sum. To prevent integer overflow, all additions to the sum are performed modulo 10^9 + 7.
import java.util.HashSet; class Solution { public int sumCounts(int[] nums) { int n = nums.length; long totalSum = 0; int MOD = 1_000_000_007; for (int i = 0; i < n; i++) { for (int j = i; j < n; j++) { HashSet<Integer> distinctElements = new HashSet<>(); for (int k = i; k <= j; k++) { distinctElements.add(nums[k]); } long distinctCount = distinctElements.size(); totalSum = (totalSum + (distinctCount * distinctCount)) % MOD; } } return (int) totalSum; }}Complexity
Time
O(n³) - There are three nested loops. The outer two loops iterate through all O(n²) subarrays, and for each subarray of average length O(n), we iterate through its elements. This results in a cubic time complexity.
Space
O(n) - In the worst-case scenario, a subarray can contain `n` distinct elements, requiring the `HashSet` to store up to `n` items.
Trade-offs
Pros
Simple to understand and implement.
Correct for small input sizes.
Cons
Extremely inefficient due to its cubic time complexity.
Will result in a 'Time Limit Exceeded' error for the given constraints.
This approach is an optimization of the naive brute-force method. Instead of recalculating the distinct elements for each subarray from scratch, we can fix the starting point of a subarray and extend it one element at a time, updating the count of distinct elements incrementally.
Algorithm
- Initialize
total_sumto 0. - Iterate through all possible start indices
ifrom0ton-1.- Create an empty
HashSetfor subarrays starting ati. - Iterate through all possible end indices
jfromiton-1.- Add
nums[j]to theHashSet. - The size of the
HashSetis the distinct countdfornums[i..j]. - Add
d*dtototal_sum, taking modulo10^9 + 7.
- Add
- Create an empty
- Return
total_sum.
Walkthrough
We use two nested loops. The outer loop fixes the starting index i of our subarrays. For each i, we initialize a HashSet. The inner loop then iterates from j = i to n-1, effectively considering all subarrays that start at i (nums[i..i], nums[i..i+1], etc.). As we extend the subarray by including nums[j], we add it to our HashSet. The size of the set at each step j gives the distinct count for the subarray nums[i..j]. We square this count and add it to the total sum. This avoids the third loop of the naive approach, reducing the complexity.
import java.util.HashSet; class Solution { public int sumCounts(int[] nums) { int n = nums.length; long totalSum = 0; int MOD = 1_000_000_007; for (int i = 0; i < n; i++) { HashSet<Integer> distinctElements = new HashSet<>(); for (int j = i; j < n; j++) { distinctElements.add(nums[j]); long distinctCount = distinctElements.size(); totalSum = (totalSum + (distinctCount * distinctCount)) % MOD; } } return (int) totalSum; }}Complexity
Time
O(n²) - Two nested loops run, and the `HashSet` add operation takes, on average, O(1) time.
Space
O(n) - The `HashSet` can store up to `n` distinct elements in the worst case for a given starting index `i`.
Trade-offs
Pros
Improves upon the O(n³) brute force.
Still relatively easy to implement.
Cons
The quadratic time complexity is still too slow for the given constraints (
nup to 10^5).
This efficient approach uses dynamic programming combined with a segment tree. We process the array from left to right. At each index j, we calculate the sum of squares of distinct counts for all subarrays ending at j. A key observation is that when we move from index j-1 to j, the distinct counts of subarrays nums[i..j] change in a predictable way based on the last occurrence of nums[j]. This pattern allows for a range update on the distinct counts, which can be handled efficiently by a segment tree.
Algorithm
- Initialize a segment tree of size
nwith all values 0. Each node will storesum_sq(sum of squares),sum_lin(linear sum), and alazyvalue for range updates. - Initialize
total_sum = 0and aprevmap to store the last seen index of each number. - Loop
jfrom0ton-1:- Get the previous index
pofnums[j]from theprevmap (or -1 if not seen). - Perform a range increment operation on the segment tree for the index range
[p+1, j], adding1to each value. - The sum of squares for all subarrays ending at
j, let's call itdp_j, is now the total sum of squares stored in the root of the segment tree. - Add
dp_jtototal_sum(modulo10^9 + 7). - Update
prev[nums[j]]toj.
- Get the previous index
- Return
total_sum.
Walkthrough
Let d(i, j) be the distinct count of nums[i..j]. At each step j, we are interested in dp[j] = sum_{i=0}^{j} d(i, j)^2. The total answer is sum_{j=0}^{n-1} dp[j].
Let p be the previous index of nums[j]. For any subarray nums[i..j], if i > p, nums[j] is a new element, so d(i, j) = d(i, j-1) + 1. If i <= p, nums[j] was already present (as nums[p]), so d(i, j) = d(i, j-1).
This means that as we process nums[j], the array of distinct counts for subarrays ending at j ([d(0,j), d(1,j), ..., d(j,j)]) can be obtained by taking the array for j-1 and performing a range increment of +1 on the suffix starting at index p+1. We also set the new value for index j to 1.
We can maintain these distinct counts in a segment tree. The segment tree will support range increments and querying the total sum of squares. Each node in the tree needs to store:
sum_sq: The sum of squares of values in its range.sum_lin: The linear sum of values in its range.lazy: A pending value to be added to its range.
When a range is incremented by c, the new sum of squares becomes sum(v+c)^2 = sum(v^2 + 2cv + c^2) = sum(v^2) + 2c*sum(v) + c^2*length. This shows why we need to track both sum_sq and sum_lin.
The main loop iterates from j=0 to n-1, performs a range update on the segment tree for [p+1, j], queries the root for the new total sum of squares, adds it to the answer, and updates the last seen position of nums[j].
import java.util.HashMap; class Solution { private static final int MOD = 1_000_000_007; private long[] sumSq, sumLin, lazy; private int n; public int sumCounts(int[] nums) { this.n = nums.length; sumSq = new long[4 * n]; sumLin = new long[4 * n]; lazy = new long[4 * n]; long totalSum = 0; long currentSumOfSquares = 0; HashMap<Integer, Integer> lastSeen = new HashMap<>(); for (int j = 0; j < n; j++) { int p = lastSeen.getOrDefault(nums[j], -1); update(1, 0, n - 1, p + 1, j, 1); currentSumOfSquares = sumSq[1]; totalSum = (totalSum + currentSumOfSquares) % MOD; lastSeen.put(nums[j], j); } return (int) totalSum; } private void pushDown(int node, int start, int end) { if (lazy[node] == 0) return; long c = lazy[node]; int mid = start + (end - start) / 2; int leftChild = 2 * node; int rightChild = 2 * node + 1; // Update left child long leftLen = mid - start + 1; sumSq[leftChild] = (sumSq[leftChild] + 2 * c * sumLin[leftChild] + (c * c % MOD) * leftLen) % MOD; sumLin[leftChild] = (sumLin[leftChild] + c * leftLen) % MOD; lazy[leftChild] = (lazy[leftChild] + c) % MOD; // Update right child long rightLen = end - mid; sumSq[rightChild] = (sumSq[rightChild] + 2 * c * sumLin[rightChild] + (c * c % MOD) * rightLen) % MOD; sumLin[rightChild] = (sumLin[rightChild] + c * rightLen) % MOD; lazy[rightChild] = (lazy[rightChild] + c) % MOD; lazy[node] = 0; } private void update(int node, int start, int end, int l, int r, int val) { if (start > end || start > r || end < l) { return; } if (l <= start && end <= r) { long c = val; long len = end - start + 1; sumSq[node] = (sumSq[node] + 2 * c * sumLin[node] + (c * c % MOD) * len) % MOD; sumLin[node] = (sumLin[node] + c * len) % MOD; if (start != end) { lazy[node] = (lazy[node] + c) % MOD; } return; } pushDown(node, start, end); int mid = start + (end - start) / 2; update(2 * node, start, mid, l, r, val); update(2 * node + 1, mid + 1, end, l, r, val); sumSq[node] = (sumSq[2 * node] + sumSq[2 * node + 1]) % MOD; sumLin[node] = (sumLin[2 * node] + sumLin[2 * node + 1]) % MOD; }}Complexity
Time
O(n log n) - The main loop runs `n` times. Inside the loop, the segment tree update operation takes O(log n) time.
Space
O(n) - The segment tree requires O(n) space. The `HashMap` for `lastSeen` can store up to O(n) unique elements in the worst case.
Trade-offs
Pros
Highly efficient with O(n log n) time complexity.
Capable of handling large inputs as per the problem constraints.
Cons
Significantly more complex to understand and implement correctly.
Requires a solid understanding of advanced data structures like segment trees with lazy propagation.
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.