LeetCode 3768 - Minimum Inversion Count in Subarrays of Fixed Length
We are given an array nums of length n and an integer k. For every contiguous subarray of length k, we must compute its inversion count, then return the smallest inversion count among all such windows.
Difficulty: 🔴 Hard
Topics: Array, Segment Tree, Sliding Window
Solution
LeetCode 3768 - Minimum Inversion Count in Subarrays of Fixed Length
Problem Understanding
We are given an array nums of length n and an integer k. For every contiguous subarray of length k, we must compute its inversion count, then return the smallest inversion count among all such windows.
An inversion is a pair of indices (i, j) such that:
i < jnums[i] > nums[j]
The inversion count of a subarray is simply the number of such pairs that exist entirely inside that subarray.
The task is therefore:
- Consider every contiguous subarray of length
k. - Compute the inversion count of each subarray.
- Return the minimum inversion count observed.
The constraints are the key challenge:
n ≤ 100000nums[i] ≤ 10^9
A quadratic solution is completely impossible. Even an O(nk) solution fails when both n and k are large.
The values themselves can be as large as 10^9, which means we cannot directly use them as indices in a Fenwick Tree or Segment Tree. Coordinate compression is required.
Several important edge cases immediately stand out.
When k = 1, every window contains exactly one element, so the inversion count is always zero.
When k = n, there is only one window, namely the entire array.
Arrays may contain duplicate values. Since an inversion requires nums[i] > nums[j], equal values do not contribute to the inversion count.
The inversion count can be as large as k(k-1)/2, which for k = 100000 is approximately 5 × 10^9, so 64-bit integers are required.
Approaches
Brute Force
The most direct approach is to examine every window of length k.
For each window, we check every pair of indices inside the window and count how many satisfy the inversion condition.
If there are n-k+1 windows and each window requires O(k²) work, the total complexity becomes:
O((n-k+1) · k²)
This is far too slow. For n = k = 100000, the work would be on the order of 10^10 operations.
Even a slightly improved approach that computes each window's inversion count independently using a Fenwick Tree would still require O((n-k+1) · k log n), which is also too expensive.
Key Observation
The windows differ by only one element.
When sliding from:
[l, r]
to
[l+1, r+1]
exactly one element leaves the window and one element enters.
Instead of recomputing the inversion count from scratch, we can update it incrementally.
Suppose:
x = nums[l]leavesy = nums[r+1]enters
The inversion count changes only through pairs involving these two elements.
If we can efficiently determine:
- how many inversion pairs disappear when
xleaves, - how many inversion pairs appear when
yenters,
then we can maintain the inversion count while sliding.
This requires a data structure that supports:
- insert value,
- remove value,
- count elements less than a value,
- count elements greater than a value.
A Fenwick Tree after coordinate compression provides all these operations in O(log n) time.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O((n-k+1)·k²) |
O(1) |
Check every pair in every window |
| Optimal | O(n log n) |
O(n) |
Sliding window inversion maintenance using Fenwick Tree |
Algorithm Walkthrough
Coordinate Compression
The array values can be as large as 10^9.
Fenwick Trees require indices in a compact range, so we sort the distinct values and map each value to its rank.
After compression every number lies in:
[1, m]
where m ≤ n.
Build the First Window Inversion Count
We compute the inversion count of the first window using a Fenwick Tree.
Process elements from left to right.
For each element:
- Count how many previous elements are greater than it.
- Add that count to the inversion total.
- Insert the element into the Fenwick Tree.
After processing the first k elements, we know the inversion count of the initial window.
Maintain a Second Fenwick Tree
After the first window is built, create a Fenwick Tree representing the current window's multiset.
This tree allows us to answer:
- number of elements less than a value,
- number of elements greater than a value,
inside the current window.
Remove the Leftmost Element
Suppose x leaves the window.
Inside the current window, x is always the leftmost position.
Every inversion involving x has the form:
x > other
with the other element appearing later.
The number of such pairs equals:
count(elements < x in current window)
Before removing x, subtract this amount from the inversion count.
Then remove x from the Fenwick Tree.
Add the New Rightmost Element
Suppose y enters from the right.
It becomes the last position of the new window.
Every new inversion involving y has the form:
other > y
where other is already in the window.
The number of such pairs equals:
count(elements > y in current window)
Add this amount to the inversion count.
Then insert y into the Fenwick Tree.
Update the Answer
After every slide:
- Current inversion count is known.
- Update the minimum answer.
Repeat until all windows have been processed.
Why it works
At every step, the maintained inversion count equals the number of inversion pairs inside the current window.
When the leftmost element leaves, every inversion involving it disappears. Since it is the earliest index in the window, all such inversions are exactly the elements smaller than it.
When a new element enters on the right, every new inversion involving it comes from earlier elements larger than it.
No other inversion pairs change during the slide. Therefore the update exactly transforms the old window's inversion count into the new window's inversion count.
By examining every window once, the minimum inversion count is found correctly.
Python Solution
from typing import List
class Fenwick:
def __init__(self, n: int):
self.n = n
self.bit = [0] * (n + 1)
def add(self, idx: int, delta: int) -> None:
while idx <= self.n:
self.bit[idx] += delta
idx += idx & -idx
def query(self, idx: int) -> int:
res = 0
while idx > 0:
res += self.bit[idx]
idx -= idx & -idx
return res
def range_query(self, left: int, right: int) -> int:
if left > right:
return 0
return self.query(right) - self.query(left - 1)
class Solution:
def minInversionCount(self, nums: List[int], k: int) -> int:
n = len(nums)
values = sorted(set(nums))
rank = {v: i + 1 for i, v in enumerate(values)}
compressed = [rank[x] for x in nums]
m = len(values)
build_bit = Fenwick(m)
inversions = 0
for i in range(k):
x = compressed[i]
inversions += i - build_bit.query(x)
build_bit.add(x, 1)
answer = inversions
window_bit = Fenwick(m)
for i in range(k):
window_bit.add(compressed[i], 1)
for left in range(n - k):
outgoing = compressed[left]
removed_pairs = window_bit.query(outgoing - 1)
inversions -= removed_pairs
window_bit.add(outgoing, -1)
incoming = compressed[left + k]
window_size = k - 1
greater_count = (
window_size - window_bit.query(incoming)
)
inversions += greater_count
window_bit.add(incoming, 1)
answer = min(answer, inversions)
return answer
Implementation Explanation
The first section performs coordinate compression so that all values fit into a compact range suitable for Fenwick Tree indexing.
A Fenwick Tree named build_bit computes the inversion count of the initial window. For every element, the number of previous elements greater than it equals:
i - build_bit.query(x)
because build_bit.query(x) counts previous elements less than or equal to the current value.
A second Fenwick Tree named window_bit stores the frequency distribution of the current window.
When sliding:
window_bit.query(outgoing - 1)counts elements smaller than the outgoing value, which are exactly the inversion pairs that disappear.- After removal, the window contains
k-1elements. window_size - window_bit.query(incoming)counts elements strictly greater than the incoming value, which are exactly the new inversion pairs created.
The inversion count is updated in O(log n) time per slide.
Go Solution
func minInversionCount(nums []int, k int) int64 {
n := len(nums)
valsMap := make(map[int]struct{})
for _, v := range nums {
valsMap[v] = struct{}{}
}
vals := make([]int, 0, len(valsMap))
for v := range valsMap {
vals = append(vals, v)
}
sort.Ints(vals)
rank := make(map[int]int)
for i, v := range vals {
rank[v] = i + 1
}
comp := make([]int, n)
for i, v := range nums {
comp[i] = rank[v]
}
m := len(vals)
bit1 := NewFenwick(m)
var inversions int64
for i := 0; i < k; i++ {
x := comp[i]
inversions += int64(i - bit1.Query(x))
bit1.Add(x, 1)
}
answer := inversions
windowBit := NewFenwick(m)
for i := 0; i < k; i++ {
windowBit.Add(comp[i], 1)
}
for left := 0; left < n-k; left++ {
outgoing := comp[left]
removedPairs := int64(windowBit.Query(outgoing - 1))
inversions -= removedPairs
windowBit.Add(outgoing, -1)
incoming := comp[left+k]
greaterCount := int64((k - 1) - windowBit.Query(incoming))
inversions += greaterCount
windowBit.Add(incoming, 1)
if inversions < answer {
answer = inversions
}
}
return answer
}
type Fenwick struct {
n int
bit []int
}
func NewFenwick(n int) *Fenwick {
return &Fenwick{
n: n,
bit: make([]int, n+1),
}
}
func (f *Fenwick) Add(idx int, delta int) {
for idx <= f.n {
f.bit[idx] += delta
idx += idx & -idx
}
}
func (f *Fenwick) Query(idx int) int {
res := 0
for idx > 0 {
res += f.bit[idx]
idx -= idx & -idx
}
return res
}
Go Specific Notes
The inversion count may exceed 32-bit range, so int64 is used for both the running inversion count and the returned result.
The Fenwick Tree stores frequencies, which never exceed 100000, so regular int is sufficient inside the tree.
Go requires explicit coordinate compression and helper structures because there is no built in ordered map.
Worked Examples
Example 1
nums = [3,1,2,5,4]
k = 3
Compressed values:
[3,1,2,5,4]
First window:
[3,1,2]
| Step | Value | New Inversions | Total |
|---|---|---|---|
| 0 | 3 | 0 | 0 |
| 1 | 1 | 1 | 1 |
| 2 | 2 | 1 | 2 |
Current inversion count:
2
Slide to:
[1,2,5]
Outgoing:
3
Smaller elements:
1,2
Removed inversions:
2
New count:
0
Incoming:
5
Greater elements already present:
0
Final count:
0
Minimum becomes:
0
Slide again:
[2,5,4]
Outgoing:
1
Removed pairs:
0
Incoming:
4
Greater elements:
5
Added pairs:
1
Count:
1
Minimum remains:
0
Answer:
0
Example 2
nums = [5,3,2,1]
k = 4
Only one window exists.
All pairs are inversions:
(5,3)
(5,2)
(5,1)
(3,2)
(3,1)
(2,1)
Total:
6
Answer:
6
Example 3
nums = [2,1]
k = 1
Windows:
[2]
[1]
Every window has:
0 inversions
Answer:
0
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n log n) |
Coordinate compression plus one Fenwick update/query sequence per element |
| Space | O(n) |
Compression arrays, mapping, and Fenwick Trees |
The coordinate compression phase requires sorting distinct values, which costs O(n log n). Building the first window and processing every slide each require only a constant number of Fenwick operations, each costing O(log n). Therefore the total complexity remains O(n log n).
Test Cases
sol = Solution()
assert sol.minInversionCount([3, 1, 2, 5, 4], 3) == 0 # example 1
assert sol.minInversionCount([5, 3, 2, 1], 4) == 6 # example 2
assert sol.minInversionCount([2, 1], 1) == 0 # example 3
assert sol.minInversionCount([1], 1) == 0 # smallest input
assert sol.minInversionCount([1, 2, 3, 4], 4) == 0 # fully sorted
assert sol.minInversionCount([4, 3, 2, 1], 4) == 6 # fully reversed
assert sol.minInversionCount([1, 1, 1, 1], 2) == 0 # duplicates only
assert sol.minInversionCount([2, 2, 1], 2) == 0 # duplicate handling
assert sol.minInversionCount([5, 4, 3, 2, 1], 2) == 1 # every window has one inversion
assert sol.minInversionCount([1, 5, 2, 6, 3], 3) == 1 # mixed ordering
assert sol.minInversionCount([1, 2, 3, 4, 5], 1) == 0 # k = 1
assert sol.minInversionCount([5, 4, 3, 2, 1], 1) == 0 # k = 1 reversed
Test Summary
| Test | Why |
|---|---|
[3,1,2,5,4], k=3 |
Official example |
[5,3,2,1], k=4 |
Single window |
[2,1], k=1 |
Minimum window size |
[1], k=1 |
Smallest valid input |
| Sorted array | Zero inversions |
| Reversed array | Maximum inversions |
| All duplicates | Equal values are not inversions |
| Duplicate mixed values | Tests strict greater than rule |
Descending with k=2 |
Small fixed window |
| Mixed ordering | General correctness |
Sorted with k=1 |
Trivial windows |
Reversed with k=1 |
Trivial windows regardless of order |
Edge Cases
Window Size Equals One
When k = 1, every window contains exactly one element. Since an inversion requires two distinct indices, the inversion count is always zero. A buggy implementation might accidentally count self pairs or mishandle sliding updates. The presented solution naturally returns zero because no inversions are added during initialization or sliding.
Window Size Equals Array Length
When k = n, there is only one valid window. Some sliding window implementations incorrectly assume at least one slide will occur. Here, the sliding loop executes zero times, and the answer is simply the inversion count computed for the initial window.
Duplicate Values
An inversion requires a strict inequality, nums[i] > nums[j]. Equal values must not be counted. The Fenwick Tree queries are carefully written so that elements equal to the target value are excluded from both the "smaller than" and "greater than" counts. This guarantees duplicates are handled correctly.
Very Large Inversion Counts
For large windows, the inversion count can exceed two billion. For example, a reversed array of length 100000 contains nearly five billion inversions. Using 32-bit integers would overflow. The solution therefore stores inversion counts in 64-bit variables, specifically int64 in Go and Python's arbitrary precision integers in Python.
Large Value Range
The array values may be as large as 10^9. Direct indexing into a Fenwick Tree would be impossible. Coordinate compression maps values into a dense range [1, m], preserving ordering while keeping the Fenwick Tree size at O(n).