Facebook Pixel

3672. Sum of Weighted Modes in Subarrays 🔒

MediumArrayHash TableCountingOrdered SetSliding Window
LeetCode ↗

Problem Description

You are given an integer array nums and an integer k. Your task is to examine every subarray of length k (a contiguous block of k elements) and compute a value called the weight for each of them.

For each window of length k:

  1. Find the mode — the element that appears the most times within that window. If two or more elements are tied for the highest frequency, pick the smallest element among them as the mode.
  2. Compute the weight — multiply the mode by how many times it appears in the window, i.e. weight = mode * frequency(mode).

Finally, return the sum of the weights across all subarrays of length k.

Key terms:

  • A subarray is a contiguous, non-empty sequence of elements taken from the array. For an array of length n, there are exactly n - k + 1 subarrays of length k.
  • The frequency of an element x is the number of times x occurs (here, within the current window).

Example walkthrough:

Suppose nums = [1, 2, 2, 3] and k = 3:

  • Window [1, 2, 2]: the element 2 appears twice (highest frequency), so the mode is 2 and the weight is 2 * 2 = 4.
  • Window [2, 2, 3]: the element 2 appears twice, so the mode is 2 and the weight is 2 * 2 = 4.

The answer would be 4 + 4 = 8.

Tie-breaking example:

For the window [1, 3, 1, 3], both 1 and 3 appear twice. Since there is a tie, the smaller element 1 is chosen as the mode, giving a weight of 1 * 2 = 2.

The challenge of this problem lies in efficiently tracking the mode as the window slides across the array — one element enters the window and one element leaves at each step — without recomputing frequencies from scratch every time.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Sliding Window?

This problem maps to Sliding Window through a short path in the full flowchart.

Subarray/substringproblem?yesMaintain avalidwindow?yesSliding Window

A fixed-size window slides across the array; each step updates a hash map of frequencies and uses a max-heap with lazy deletion to find the mode in amortized logarithmic time.

Open in Flowchart

Intuition

The naive approach is straightforward: for each of the n - k + 1 windows, count the frequency of every element and find the mode. But counting from scratch for each window costs O(k) per window, giving O(n * k) overall — too slow when the array is large.

The key observation is that consecutive windows differ by only two elements: when the window slides one step to the right, exactly one element enters and one element leaves. So instead of recounting everything, we can maintain a hash map cnt of frequencies and update just two entries per slide. This is the classic sliding window idea.

The harder part is: after these two small updates, how do we quickly find the new mode? The frequency of one element went up and another went down, and either change could alter which element is the mode. Scanning the entire hash map after every slide would again be O(k) per window.

This is where a priority queue (max-heap) comes in. We want the element with the highest frequency, breaking ties by the smallest value — exactly what a heap ordered by (-frequency, value) gives us at its top.

But there's a catch: frequencies keep changing as the window slides, and a standard heap doesn't support updating or removing arbitrary entries efficiently. The trick is lazy deletion:

  • Whenever an element's frequency changes, we simply push a new entry (-new_frequency, value) into the heap, leaving the old, outdated entries inside.
  • When we need the mode, we look at the top of the heap and check it against the hash map, which always holds the true current frequency. If the top entry's frequency doesn't match cnt[value], it's stale — we pop it and check the next one. The first entry that matches is guaranteed to be the real mode.

Why does this work? The hash map is the source of truth, and the heap is just a collection of "candidate snapshots." Any valid current state of an element exists in the heap (we pushed it when the frequency last changed), so the correct mode is always in there — we just need to skip past expired snapshots.

The cost stays low because each slide pushes at most two new entries, so the heap holds O(n) entries in total, and each entry is popped at most once across the whole run. This amortizes to O(n log n) overall — a big win over the brute-force O(n * k).

Pattern Learn more about Sliding Window patterns.

Solution Approach

The solution combines four ideas: a hash map for true frequencies, a priority queue for fast mode lookup, a sliding window for incremental updates, and lazy deletion to handle stale heap entries.

Data Structures

  • cnt: a hash map (defaultdict(int)) mapping each element to its current frequency in the window. This is the source of truth.
  • pq: a min-heap storing tuples (-frequency, value). Negating the frequency turns Python's min-heap into a max-heap by frequency; for equal frequencies, the smaller value naturally sorts first — exactly matching the tie-breaking rule.

Step 1: Initialize the First Window

pq = []
cnt = defaultdict(int)
for x in nums[:k]:
    cnt[x] += 1
    heappush(pq, (-cnt[x], x))

For each of the first k elements, we increment its count and push a snapshot (-cnt[x], x) into the heap. Note that an element appearing multiple times produces multiple snapshots (e.g., (-1, x), (-2, x), ...), but only the latest one matches the hash map — the rest are stale and will be lazily discarded.

Step 2: The get_mode() Function (Lazy Deletion)

def get_mode() -> int:
    while -pq[0][0] != cnt[pq[0][1]]:
        heappop(pq)
    freq, val = -pq[0][0], pq[0][1]
    return freq * val

This is the heart of the lazy-deletion technique:

  • Look at the heap top (-freq, val). If -pq[0][0] (the recorded frequency) does not equal cnt[pq[0][1]] (the true frequency), the entry is outdated — pop it and check the next.
  • The first entry whose frequency matches the hash map is guaranteed to be the current mode, because every valid (frequency, value) state was pushed when it occurred, and the heap ordering puts the highest frequency (smallest value on ties) on top.
  • Return the weight freq * val directly.

We peek rather than pop the valid top, so it remains available for future queries.

Step 3: Slide the Window

ans = 0
ans += get_mode()  # weight of the first window

for i in range(k, len(nums)):
    x, y = nums[i], nums[i - k]
    cnt[x] += 1      # element entering the window
    cnt[y] -= 1      # element leaving the window
    heappush(pq, (-cnt[x], x))
    heappush(pq, (-cnt[y], y))
    ans += get_mode()

return ans

For each slide:

  1. x = nums[i] enters the window — increment cnt[x] and push its new snapshot.
  2. y = nums[i - k] leaves the window — decrement cnt[y] and push its new snapshot. Pushing the decreased state is important: the lowered frequency might still make y the mode, so the heap must contain this valid state too.
  3. Call get_mode() to clean up stale entries and add the current window's weight to ans.

Why Correctness Holds

The invariant maintained throughout is: for every element in the window, its current (frequency, value) pair exists somewhere in the heap. We never miss a candidate because every frequency change immediately pushes the new state. Stale entries can only be equal to or higher in priority than reality for the same element, and the hash-map check filters them out precisely.

Complexity Analysis

  • Time: O(n log n). Each of the n slides pushes at most two entries, so the heap receives O(n) entries total. Each entry is popped at most once over the entire execution, and every push/pop costs O(log n). The popping work amortizes across all get_mode() calls.
  • Space: O(n) for the heap (which accumulates snapshots) and O(k) for the hash map.

Example Walkthrough

Let's trace the algorithm on nums = [1, 2, 2, 1, 3] with k = 3. There are 5 - 3 + 1 = 3 windows to process.

Step 1: Build the first window [1, 2, 2]

Process each element, incrementing its count and pushing a snapshot:

Elementcnt after updatePushed to heap
1{1: 1}(-1, 1)
2{1: 1, 2: 1}(-1, 2)
2{1: 1, 2: 2}(-2, 2)

Note that 2 now has two snapshots in the heap: (-1, 2) is already stale, and (-2, 2) is the live one.

get_mode(): the heap top is (-2, 2). Check it against the map: cnt[2] = 2 ✓ matches. Mode is 2, weight = 2 * 2 = 4.

ans = 4

Step 2: Slide to window [2, 2, 1]

  • Entering: x = nums[3] = 1 → cnt[1] becomes 2, push (-2, 1)
  • Leaving: y = nums[0] = 1 → cnt[1] drops back to 1, push (-1, 1)

(Here the same value enters and leaves — the bookkeeping still works because every change pushes a fresh snapshot.)

True state: cnt = {1: 1, 2: 2}. Heap now contains (-2, 1), (-2, 2), (-1, 1), (-1, 1), (-1, 2).

get_mode():

  1. Top is (-2, 1) — it sorts above (-2, 2) because of the smaller value. But cnt[1] = 1 ≠ 2 → stale, pop it. This is lazy deletion in action: that snapshot described a frequency 1 briefly had, which is no longer true.
  2. New top is (-2, 2): cnt[2] = 2 ✓. Mode is 2, weight = 4.

ans = 4 + 4 = 8

Step 3: Slide to window [2, 1, 3]

  • Entering: x = nums[4] = 3 → cnt[3] = 1, push (-1, 3)
  • Leaving: y = nums[1] = 2 → cnt[2] drops to 1, push (-1, 2)

True state: cnt = {1: 1, 2: 1, 3: 1} — a three-way tie, all frequencies equal to 1.

get_mode():

  1. Top is (-2, 2), but cnt[2] = 1 ≠ 2 → stale, pop it.
  2. New top is (-1, 1): cnt[1] = 1 ✓. Among the tied entries (-1, 1), (-1, 2), (-1, 3), the heap ordering automatically surfaces the smallest value first — exactly the tie-breaking rule. Mode is 1, weight = 1 * 1 = 1.

ans = 8 + 1 = 9

Result

The total weight is 9 (4 + 4 + 1).

This short run demonstrates all the moving parts:

  • Sliding window: each step updated only two map entries instead of recounting the window.
  • Lazy deletion: stale snapshots like (-2, 1) and (-2, 2) sat harmlessly in the heap until a query reached them, then were discarded by comparing against cnt.
  • Tie-breaking for free: ordering by (-frequency, value) made the heap return the smallest element among equally frequent candidates without any extra logic.

Solution Implementation

1from typing import List
2from collections import defaultdict
3from heapq import heappush, heappop
4
5
6class Solution:
7    def modeWeight(self, nums: List[int], k: int) -> int:
8        # Max-heap storing (-frequency, value) pairs.
9        # Python's heapq is a min-heap, so frequencies are negated.
10        max_heap = []
11        # Frequency counter for elements inside the current window.
12        freq_map = defaultdict(int)
13
14        # Initialize the first window of size k.
15        for num in nums[:k]:
16            freq_map[num] += 1
17            heappush(max_heap, (-freq_map[num], num))
18
19        def get_mode() -> int:
20            """
21            Return mode * frequency of the current window.
22
23            Lazy deletion: heap entries may be outdated because
24            frequencies change as the window slides. An entry is
25            valid only if its stored frequency matches the current
26            frequency in freq_map; otherwise it is discarded.
27            """
28            while -max_heap[0][0] != freq_map[max_heap[0][1]]:
29                heappop(max_heap)
30            freq, val = -max_heap[0][0], max_heap[0][1]
31            return freq * val
32
33        # Accumulate the answer, starting with the first window.
34        ans = get_mode()
35
36        # Slide the window one position at a time.
37        for i in range(k, len(nums)):
38            new_num, old_num = nums[i], nums[i - k]
39
40            # Add the incoming element and remove the outgoing one.
41            freq_map[new_num] += 1
42            freq_map[old_num] -= 1
43
44            # Push updated states; stale entries are cleaned lazily.
45            heappush(max_heap, (-freq_map[new_num], new_num))
46            heappush(max_heap, (-freq_map[old_num], old_num))
47
48            ans += get_mode()
49
50        return ans
51
1class Solution {
2    public long modeWeight(int[] nums, int k) {
3        // Map each value to its frequency within the current sliding window
4        Map<Integer, Integer> freqMap = new HashMap<>();
5
6        // Max-heap simulated with negated frequencies:
7        // entry = {-frequency, value}
8        // Ordering: higher frequency first; if frequencies tie, smaller value first
9        PriorityQueue<int[]> maxHeap = new PriorityQueue<>(
10            (a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0]) : Integer.compare(a[1], b[1]));
11
12        // Initialize the first window of size k
13        for (int i = 0; i < k; i++) {
14            int value = nums[i];
15            freqMap.merge(value, 1, Integer::sum);
16            // Push a snapshot of the current (frequency, value) pair
17            maxHeap.offer(new int[] {-freqMap.get(value), value});
18        }
19
20        long answer = 0;
21
22        // Lazily retrieve the mode of the current window:
23        // discard stale heap entries whose recorded frequency
24        // no longer matches the actual frequency in freqMap
25        Supplier<Long> getMode = () -> {
26            while (true) {
27                int[] top = maxHeap.peek();
28                int value = top[1];
29                int frequency = -top[0];
30                // Entry is valid only if its snapshot matches the live count
31                if (freqMap.getOrDefault(value, 0) == frequency) {
32                    return 1L * frequency * value;
33                }
34                // Stale entry: remove and keep searching
35                maxHeap.poll();
36            }
37        };
38
39        // Account for the first window
40        answer += getMode.get();
41
42        // Slide the window across the rest of the array
43        for (int i = k; i < nums.length; i++) {
44            int incoming = nums[i];      // element entering the window
45            int outgoing = nums[i - k];  // element leaving the window
46
47            // Add the incoming element and push its updated snapshot
48            freqMap.merge(incoming, 1, Integer::sum);
49            maxHeap.offer(new int[] {-freqMap.get(incoming), incoming});
50
51            // Remove the outgoing element and push its updated snapshot
52            freqMap.merge(outgoing, -1, Integer::sum);
53            maxHeap.offer(new int[] {-freqMap.get(outgoing), outgoing});
54
55            // Accumulate the mode weight of the current window
56            answer += getMode.get();
57        }
58
59        return answer;
60    }
61}
62
1class Solution {
2public:
3    long long modeWeight(vector<int>& nums, int k) {
4        // freq_count[v] = current frequency of value v inside the sliding window
5        unordered_map<int, int> freq_count;
6
7        // Max-heap storing {frequency, -value}.
8        // - Ordered by frequency first (higher frequency on top).
9        // - Negated value as tie-breaker, so the SMALLER value wins
10        //   when two values share the same frequency.
11        // Stale entries are allowed and removed lazily.
12        priority_queue<pair<int, int>> max_heap;
13
14        // ---------- Step 1: Build the first window of size k ----------
15        for (int i = 0; i < k; i++) {
16            int val = nums[i];
17            freq_count[val]++;
18            max_heap.push({freq_count[val], -val});
19        }
20
21        // ---------- Step 2: Helper to fetch the current mode's weight ----------
22        // Lazy deletion: pop entries whose stored frequency no longer matches
23        // the real frequency in freq_count (i.e., outdated snapshots).
24        auto get_mode = [&]() -> long long {
25            while (true) {
26                auto [freq, neg_val] = max_heap.top();
27                int val = -neg_val;
28                if (freq_count[val] == freq) {
29                    // Valid entry: this is the true mode of the window.
30                    // Weight = frequency * value.
31                    return 1LL * freq * val;
32                }
33                // Outdated entry, discard it.
34                max_heap.pop();
35            }
36        };
37
38        long long ans = 0;
39        ans += get_mode(); // mode weight of the initial window
40
41        // ---------- Step 3: Slide the window across the array ----------
42        for (int i = k; i < (int)nums.size(); i++) {
43            int in_val = nums[i];       // element entering the window
44            int out_val = nums[i - k];  // element leaving the window
45
46            // Update frequencies for both elements.
47            freq_count[in_val]++;
48            freq_count[out_val]--;
49
50            // Push fresh snapshots; old snapshots become stale and
51            // will be cleaned up lazily inside get_mode().
52            max_heap.push({freq_count[in_val], -in_val});
53            max_heap.push({freq_count[out_val], -out_val});
54
55            // Accumulate the mode weight of the current window.
56            ans += get_mode();
57        }
58
59        return ans;
60    }
61};
62
1// ---------- Min/Max Heap Implementation ----------
2// A generic binary heap ordered by a custom comparator.
3// comparator(a, b) < 0 means `a` has higher priority than `b`.
4
5type HeapEntry = [number, number]; // [frequency, value]
6
7const heap: HeapEntry[] = [];
8
9// Comparator for the max-heap:
10// - Higher frequency comes first.
11// - On equal frequency, the SMALLER value wins (tie-breaker).
12function compare(a: HeapEntry, b: HeapEntry): number {
13    if (a[0] !== b[0]) return b[0] - a[0]; // larger frequency first
14    return a[1] - b[1];                    // smaller value first
15}
16
17// Insert an entry and restore heap order by sifting it up.
18function heapPush(entry: HeapEntry): void {
19    heap.push(entry);
20    let idx = heap.length - 1;
21    while (idx > 0) {
22        const parent = (idx - 1) >> 1;
23        if (compare(heap[idx], heap[parent]) < 0) {
24            [heap[idx], heap[parent]] = [heap[parent], heap[idx]];
25            idx = parent;
26        } else {
27            break;
28        }
29    }
30}
31
32// Return the highest-priority entry without removing it.
33function heapTop(): HeapEntry {
34    return heap[0];
35}
36
37// Remove the highest-priority entry and restore heap order by sifting down.
38function heapPop(): void {
39    const last = heap.pop()!;
40    if (heap.length === 0) return;
41    heap[0] = last;
42    let idx = 0;
43    const n = heap.length;
44    while (true) {
45        const left = idx * 2 + 1;
46        const right = idx * 2 + 2;
47        let best = idx;
48        if (left < n && compare(heap[left], heap[best]) < 0) best = left;
49        if (right < n && compare(heap[right], heap[best]) < 0) best = right;
50        if (best === idx) break;
51        [heap[idx], heap[best]] = [heap[best], heap[idx]];
52        idx = best;
53    }
54}
55
56// ---------- Main Function ----------
57function modeWeight(nums: number[], k: number): number {
58    // Reset global heap state for repeated calls.
59    heap.length = 0;
60
61    // freqCount.get(v) = current frequency of value v inside the sliding window.
62    const freqCount: Map<number, number> = new Map();
63
64    // ---------- Step 1: Build the first window of size k ----------
65    for (let i = 0; i < k; i++) {
66        const val = nums[i];
67        freqCount.set(val, (freqCount.get(val) ?? 0) + 1);
68        // Push a fresh snapshot of (frequency, value).
69        heapPush([freqCount.get(val)!, val]);
70    }
71
72    // ---------- Step 2: Helper to fetch the current mode's weight ----------
73    // Lazy deletion: pop entries whose stored frequency no longer matches
74    // the real frequency in freqCount (i.e., outdated snapshots).
75    const getMode = (): number => {
76        while (true) {
77            const [freq, val] = heapTop();
78            if (freqCount.get(val) === freq) {
79                // Valid entry: this is the true mode of the window.
80                // Weight = frequency * value.
81                return freq * val;
82            }
83            // Outdated entry, discard it.
84            heapPop();
85        }
86    };
87
88    let ans = 0;
89    ans += getMode(); // mode weight of the initial window
90
91    // ---------- Step 3: Slide the window across the array ----------
92    for (let i = k; i < nums.length; i++) {
93        const inVal = nums[i];      // element entering the window
94        const outVal = nums[i - k]; // element leaving the window
95
96        // Update frequencies for both elements.
97        freqCount.set(inVal, (freqCount.get(inVal) ?? 0) + 1);
98        freqCount.set(outVal, (freqCount.get(outVal) ?? 0) - 1);
99
100        // Push fresh snapshots; old snapshots become stale and
101        // will be cleaned up lazily inside getMode().
102        heapPush([freqCount.get(inVal)!, inVal]);
103        heapPush([freqCount.get(outVal)!, outVal]);
104
105        // Accumulate the mode weight of the current window.
106        ans += getMode();
107    }
108
109    return ans;
110}
111

Time and Space Complexity

Time Complexity: O(n log k)

  • The initialization loop processes the first k elements, performing cnt[x] += 1 and a heappush for each, costing O(k log k).
  • The main loop runs n - k times. In each iteration:
    • Updating the counter cnt for the incoming element x and the outgoing element y takes O(1).
    • Two heappush operations are performed, each costing O(log k) since the number of valid entries in the heap is bounded by the window size k.
    • get_mode() uses lazy deletion: it pops stale entries (where -pq[0][0] != cnt[pq[0][1]]) from the heap top. Since each pushed entry can be popped at most once, the total cost of all pops across the entire run is amortized into the total number of pushes, i.e., O(n log k) overall.
  • Summing up, the total time complexity is O(n log k), where n is the length of nums.

Space Complexity: O(k)

  • The hash table cnt stores at most the distinct elements within the current window of size k, taking O(k) space.
  • The heap pq maintains entries corresponding to the current window; stale entries are continuously discarded by the lazy-deletion step in get_mode(), so the live data it tracks is bounded by the window size, contributing O(k) space.
  • Therefore, the overall space complexity is O(k).

Pattern Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Popping the valid heap top inside get_mode() instead of peeking

A very common mistake is writing the lazy-deletion loop so that it pops every entry it inspects, including the valid one:

# WRONG
def get_mode() -> int:
    while True:
        neg_freq, val = heappop(max_heap)      # destructive!
        if -neg_freq == freq_map[val]:
            return -neg_freq * val

Why it breaks: the popped valid snapshot represents the element's current state. If the window slides and that element's frequency does not change (it neither enters nor leaves), no new snapshot is pushed for it — yet its valid entry is gone from the heap. On the next query the heap may surface a different element with a lower frequency and report it as the mode, or the loop may exhaust the heap entirely and raise IndexError.

Concrete failure: nums = [2, 2, 1, 3], k = 2 is fine, but take nums = [5, 5, 5, 1], k = 3:

  • Window [5, 5, 5]: the destructive version pops (-3, 5) and returns 15. ✔
  • Window [5, 5, 1]: cnt[5] becomes 2, and snapshots (-2, 5), (-1, 5) are pushed by the slide — this happens to survive. Now extend the array so 5's count stays constant across two consecutive windows (e.g., nums = [5, 5, 1, 1, 5], k = 4): the valid (-2, 5) snapshot consumed by the first query no longer exists for the second one, and the reported mode is wrong.

Fix — choose one of two repairs:

  1. Peek, don't pop (the approach used in the reference code). Only stale entries are removed; the valid top stays for future queries:

    def get_mode() -> int:
        while -max_heap[0][0] != freq_map[max_heap[0][1]]:
            heappop(max_heap)                  # remove stale entries only
        freq, val = -max_heap[0][0], max_heap[0][1]
        return freq * val                      # top is left untouched
  2. Pop, then push back the valid entry before returning:

    def get_mode() -> int:
        while True:
            neg_freq, val = heappop(max_heap)
            if -neg_freq == freq_map[val]:
                heappush(max_heap, (neg_freq, val))   # restore the valid state
                return -neg_freq * val

    This works but costs an extra O(log n) per query; peeking is cleaner.


Pitfall 2: Negating the value as well, which inverts the tie-breaking rule

To simulate a max-heap, some people negate both fields:

heappush(max_heap, (-freq_map[num], -num))   # WRONG tie-break

Negating the frequency is required, but negating the value flips the secondary ordering: among equal frequencies, -num smallest means num largest, so the heap returns the largest tied element — the problem demands the smallest.

Concrete failure: window [1, 3, 1, 3] — both 1 and 3 have frequency 2. The correct weight is 1 * 2 = 2, but the double-negated heap yields 3 * 2 = 6.

Fix: store (-frequency, value) with the value un-negated. The min-heap then naturally puts the highest frequency first and, on frequency ties, the smallest value first — exactly the required rule.


Pitfall 3: A sign or lookup error in the staleness check

Two variants of the same trap:

  • Forgetting the negation when comparing: while max_heap[0][0] != freq_map[max_heap[0][1]] compares a negative stored frequency against a positive true count. Every entry looks stale, the loop drains the entire heap, and the program crashes with IndexError on an empty heap.
  • Using a plain dict and deleting zero-count keys (del freq_map[old_num] when the count hits 0). Stale heap entries for that element still exist, and freq_map[max_heap[0][1]] then raises KeyError during the staleness check.

Fix: compare -max_heap[0][0] != freq_map[max_heap[0][1]] (negate the stored frequency back), and keep freq_map as a defaultdict(int) — a vanished element simply reads as 0, which correctly marks all its leftover snapshots as stale.

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

In a binary min heap, the minimum element can be found in:


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More