3672. Sum of Weighted Modes in Subarrays 🔒
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:
- 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.
- 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 exactlyn - k + 1subarrays of lengthk. - The frequency of an element
xis the number of timesxoccurs (here, within the current window).
Example walkthrough:
Suppose nums = [1, 2, 2, 3] and k = 3:
- Window
[1, 2, 2]: the element2appears twice (highest frequency), so the mode is2and the weight is2 * 2 = 4. - Window
[2, 2, 3]: the element2appears twice, so the mode is2and the weight is2 * 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.
How We Pick the Algorithm
Why Sliding Window?
This problem maps to Sliding Window through a short path in the full flowchart.
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 FlowchartIntuition
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 smallervaluenaturally 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 equalcnt[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 * valdirectly.
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:
x = nums[i]enters the window — incrementcnt[x]and push its new snapshot.y = nums[i - k]leaves the window — decrementcnt[y]and push its new snapshot. Pushing the decreased state is important: the lowered frequency might still makeythe mode, so the heap must contain this valid state too.- Call
get_mode()to clean up stale entries and add the current window's weight toans.
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 thenslides pushes at most two entries, so the heap receivesO(n)entries total. Each entry is popped at most once over the entire execution, and every push/pop costsO(log n). The popping work amortizes across allget_mode()calls. - Space:
O(n)for the heap (which accumulates snapshots) andO(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:
| Element | cnt after update | Pushed 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]becomes2, push(-2, 1) - Leaving:
y = nums[0] = 1→cnt[1]drops back to1, 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():
- Top is
(-2, 1)— it sorts above(-2, 2)because of the smaller value. Butcnt[1] = 1 ≠2→ stale, pop it. This is lazy deletion in action: that snapshot described a frequency1briefly had, which is no longer true. - New top is
(-2, 2):cnt[2] = 2✓. Mode is2, 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 to1, push(-1, 2)
True state: cnt = {1: 1, 2: 1, 3: 1} — a three-way tie, all frequencies equal to 1.
get_mode():
- Top is
(-2, 2), butcnt[2] = 1 ≠2→ stale, pop it. - 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 is1, 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 againstcnt. - 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
511class 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}
621class 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};
621// ---------- 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}
111Time and Space Complexity
Time Complexity: O(n log k)
- The initialization loop processes the first
kelements, performingcnt[x] += 1and aheappushfor each, costingO(k log k). - The main loop runs
n - ktimes. In each iteration:- Updating the counter
cntfor the incoming elementxand the outgoing elementytakesO(1). - Two
heappushoperations are performed, each costingO(log k)since the number of valid entries in the heap is bounded by the window sizek. 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.
- Updating the counter
- Summing up, the total time complexity is
O(n log k), wherenis the length ofnums.
Space Complexity: O(k)
- The hash table
cntstores at most the distinct elements within the current window of sizek, takingO(k)space. - The heap
pqmaintains entries corresponding to the current window; stale entries are continuously discarded by the lazy-deletion step inget_mode(), so the live data it tracks is bounded by the window size, contributingO(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 returns15. ✔ - Window
[5, 5, 1]:cnt[5]becomes2, and snapshots(-2, 5),(-1, 5)are pushed by the slide — this happens to survive. Now extend the array so5'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:
-
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 -
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 * valThis 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 withIndexErroron an empty heap. - Using a plain
dictand deleting zero-count keys (del freq_map[old_num]when the count hits0). Stale heap entries for that element still exist, andfreq_map[max_heap[0][1]]then raisesKeyErrorduring 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 RoadmapIn a binary min heap, the minimum element can be found in:
Recommended Readings
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Want a Structured Path to Master System Design Too? Don’t Miss This!