Count Inversions in Array
Count Inversions in Array
Pattern: merge function of merge sort variation
Idea:
Variations :
๐ป Code
def count_inversions(arr):
"""Main wrapper function"""
def danq(arr, low, high):
if low >= high:
return 0
mid = (low + high) // 2
count = 0
# Count inversions in left half
count += danq(arr, low, mid)
# Count inversions in right half
count += danq(arr, mid + 1, high)
# Count split inversions (merge step)
count += merge_and_count(arr, low, mid, high)
return count
n = len(arr)
if n == 0:
return 0
return danq(arr, 0, n - 1)
def merge_and_count(arr, low, mid, high):
"""
Merge two sorted subarrays and count inversions.
Key insight: When left[i] > right[j], all elements from i to mid
in left subarray are also > right[j].
"""
left = arr[low:mid+1]
right = arr[mid+1:high+1]
i = j = 0
k = low
inversion_count = 0
# Two-pointer merge
while i < len(left) and j < len(right):
if left[i] < right[j]:
arr[k] = left[i]
i += 1
elif left[i] > right[j]:
# โญ CRITICAL: All remaining elements in left (from i onwards)
# form inversions with right[j]
arr[k] = right[j]
j += 1
inversion_count += len(left) - i # NOT (mid - i + 1)
else: # left[i] == right[j]
arr[k] = left[i]
i += 1
j += 1
k += 1
# Exhaust remaining elements
while i < len(left):
arr[k] = left[i]
i += 1
k += 1
while j < len(right):
arr[k] = right[j]
j += 1
k += 1
return inversion_count
# Test
arr = [2, 4, 1, 12, 9, 3]
print(count_inversions(arr)) # Output: 6
Time complexity - O(n log n )
Aux. Space complexity - O(n)
Count Inversions in Array
#dsa #merge-sort #interview-patterns #arrays
๐ Problem Statement
Given an array of integers, count the number of inversions. An inversion is a pair of indices (i, j) where i < j but arr[i] > arr[j].
Example:
arr = [2, 4, 1, 12, 9, 3]
Inversions: (2,1), (4,1), (4,3), (12,9), (12,3), (9,3) = 6 inversions
๐ฏ Core Insight
This is fundamentally a modified merge sort problem. The inversion count represents how โunsortedโ an array is โ a fully sorted array has 0 inversions, while a reverse-sorted array has the maximum: n*(n-1)/2.
Key Realization: When merging two sorted subarrays, if an element from the right subarray is smaller than an element from the left subarray, ALL remaining elements in the left subarray form inversions with this right element.
Left: [2, 4, 1]
Right: [12, 9, 3]
When we pick 3 from right and see 1 > 3? No.
But 4 > 3? Yes โ all elements from position of 4 onwards in left (which is [4, 1]) form inversions.
That's len(left) - i = 2 inversions.
๐ป Solution: Merge Sort Approach
Algorithm Overview
- Divide the array into two halves
- Recursively count inversions in left half
- Recursively count inversions in right half
- Merge while counting split inversions (elements from left half > elements from right half)
Code
def count_inversions(arr):
"""Main wrapper function"""
def danq(arr, low, high):
if low >= high:
return 0
mid = (low + high) // 2
count = 0
# Count inversions in left half
count += danq(arr, low, mid)
# Count inversions in right half
count += danq(arr, mid + 1, high)
# Count split inversions (merge step)
count += merge_and_count(arr, low, mid, high)
return count
n = len(arr)
if n == 0:
return 0
return danq(arr, 0, n - 1)
def merge_and_count(arr, low, mid, high):
"""
Merge two sorted subarrays and count inversions.
Key insight: When left[i] > right[j], all elements from i to mid
in left subarray are also > right[j].
"""
left = arr[low:mid+1]
right = arr[mid+1:high+1]
i = j = 0
k = low
inversion_count = 0
# Two-pointer merge
while i < len(left) and j < len(right):
if left[i] < right[j]:
arr[k] = left[i]
i += 1
elif left[i] > right[j]:
# โญ CRITICAL: All remaining elements in left (from i onwards)
# form inversions with right[j]
arr[k] = right[j]
j += 1
inversion_count += len(left) - i # NOT (mid - i + 1)
else: # left[i] == right[j]
arr[k] = left[i]
i += 1
j += 1
k += 1
# Exhaust remaining elements
while i < len(left):
arr[k] = left[i]
i += 1
k += 1
while j < len(right):
arr[k] = right[j]
j += 1
k += 1
return inversion_count
# Test
arr = [2, 4, 1, 12, 9, 3]
print(count_inversions(arr)) # Output: 6
๐ Detailed Dry Run
Array: [2, 4, 1, 12, 9, 3]
[2, 4, 1, 12, 9, 3]
/ \
[2, 4, 1] [12, 9, 3]
/ \ / \
[2, 4] [1] [12, 9] [3]
/ \ | / \ |
[2] [4] [1] [12] [9] [3]
\ / | \ / |
[2,4] [1] [9,12] [3]
| 0 inv | | 1 inv |
\ / \ /
[1, 2, 4] [3, 9, 12]
| 1 inv | 0 inv
\ /
[1, 2, 3, 4, 9, 12]
| 4 inversions (split)
Total: 0 + 1 + 0 + 1 + 0 + 4 = 6 inversions
Step-by-Step Merge Details
Merging [2, 4] and [1]:
left=[2,4], right=[1]
Compare 2 vs 1: 2 > 1 โ [1] goes to result
inversion_count += len(left) - 0 = 2
(pairs: (2,1), (4,1))
Result: [1, 2, 4], inversions: 2
Merging [9, 12] and [3]:
left=[9,12], right=[3]
Compare 9 vs 3: 9 > 3 โ [3] goes to result
inversion_count += len(left) - 0 = 2
(pairs: (9,3), (12,3))
Result: [3, 9, 12], inversions: 2
Merging [1, 2, 4] and [3, 9, 12]:
i=0,j=0: left[0]=1 < right[0]=3 โ take 1, i=1
i=1,j=0: left[1]=2 < right[0]=3 โ take 2, i=2
i=2,j=0: left[2]=4 > right[0]=3 โ take 3, j=1
inversion_count += len(left) - 2 = 1 (pair: (4,3))
i=2,j=1: left[2]=4 < right[1]=9 โ take 4, i=3
Exhaust right: [9, 12]
Result: [1, 2, 3, 4, 9, 12], inversions: 1
Total inversions: 2 + 2 + 1 + โฆ (from recursive calls) = 6 โ
โ ๏ธ Common Bug: Index Confusion
WRONG:
inversion_count += mid - i + 1 # โ Uses original array indices
RIGHT:
inversion_count += len(left) - i # โ
Uses subarray size
Why: When you do left = arr[low:mid+1], the indices in left start from 0, not from low. So use len(left) - i to count remaining elements.
๐ Complexity Analysis
| Aspect | Complexity | Notes |
|---|---|---|
| Time | O(n log n) | Same as merge sort: T(n) = 2T(n/2) + O(n) |
| Space | O(n) | Temporary arrays in merge step + recursion stack O(log n) |
| Best Case | O(n log n) | No optimization possible (not like quicksort) |
| Worst Case | O(n log n) | Reverse-sorted array: n(n-1)/2 inversions |
Proof of Time Complexity:
- Each element is compared once during merge: O(n) per level
- Tree has log(n) levels (divide-and-conquer)
- Total: O(n log n)
Alternative (Brute Force): O(nยฒ) โ compare every pair, but too slow for interviews.
๐ Variation 1: Merge Sort Indices (Track Original Positions)
Sometimes interviewers ask: โReturn the actual inversion pairs, not just the count.โ
def get_inversion_pairs(arr):
"""Returns list of (index_i, index_j) pairs where i < j and arr[i] > arr[j]"""
pairs = []
indexed_arr = [(val, idx) for idx, val in enumerate(arr)]
def merge_and_collect(arr, low, high):
if low >= high:
return
mid = (low + high) // 2
merge_and_collect(arr, low, mid)
merge_and_collect(arr, mid + 1, high)
left = arr[low:mid+1]
right = arr[mid+1:high+1]
i = j = 0
k = low
while i < len(left) and j < len(right):
if left[i][0] <= right[j][0]:
arr[k] = left[i]
i += 1
else:
arr[k] = right[j]
# Collect all inversions
for idx in range(i, len(left)):
pairs.append((left[idx][1], right[j][1]))
j += 1
k += 1
# Exhaust remaining
while i < len(left):
arr[k] = left[i]
i += 1
k += 1
while j < len(right):
arr[k] = right[j]
j += 1
k += 1
merge_and_collect(indexed_arr, 0, len(indexed_arr) - 1)
return pairs
# Test
arr = [2, 4, 1, 12, 9, 3]
pairs = get_inversion_pairs(arr)
print(pairs) # [(0,2), (1,2), (1,5), (3,4), (3,5), (4,5)]
๐ Variation 2: Count Inversions Modulo M
Problem: Count inversions mod 10^9+7 (common in competitive programming).
MOD = 10**9 + 7
def count_inversions_mod(arr):
def merge_and_count(arr, low, mid, high):
left = arr[low:mid+1]
right = arr[mid+1:high+1]
i = j = 0
k = low
count = 0
while i < len(left) and j < len(right):
if left[i] < right[j]:
arr[k] = left[i]
i += 1
elif left[i] > right[j]:
arr[k] = right[j]
j += 1
count = (count + len(left) - i) % MOD
else:
arr[k] = left[i]
i += 1
j += 1
k += 1
while i < len(left):
arr[k] = left[i]
i += 1
k += 1
while j < len(right):
arr[k] = right[j]
j += 1
k += 1
return count
def danq(arr, low, high):
if low >= high:
return 0
mid = (low + high) // 2
count = danq(arr, low, mid)
count = (count + danq(arr, mid + 1, high)) % MOD
count = (count + merge_and_count(arr, low, mid, high)) % MOD
return count
n = len(arr)
return danq(arr, 0, n - 1) if n > 0 else 0
๐ Variation 3: Count Inversions in Sorted Order (Fenwick/BIT)
Problem: Count inversions using a Balanced BST or Fenwick Tree (more efficient for certain follow-ups).
This approach is useful when:
- Array has very large values (need coordinate compression)
- You need to handle updates (online inversion counting)
class FenwickTree:
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1)
def update(self, i, delta=1):
i += 1 # 1-indexed
while i <= self.n:
self.tree[i] += delta
i += i & (-i)
def query(self, i):
i += 1 # 1-indexed
s = 0
while i > 0:
s += self.tree[i]
i -= i & (-i)
return s
def range_query(self, l, r):
return self.query(r) - (self.query(l - 1) if l > 0 else 0)
def count_inversions_fenwick(arr):
"""
Process array from right to left.
For each element, query how many elements to the right are smaller.
"""
sorted_arr = sorted(set(arr))
compress = {v: i for i, v in enumerate(sorted_arr)}
fenwick = FenwickTree(len(sorted_arr))
inversions = 0
for i in range(len(arr) - 1, -1, -1):
compressed = compress[arr[i]]
# Count elements smaller than arr[i] seen so far (to the right)
if compressed > 0:
inversions += fenwick.query(compressed - 1)
fenwick.update(compressed)
return inversions
# Test
arr = [2, 4, 1, 12, 9, 3]
print(count_inversions_fenwick(arr)) # Output: 6
When to use:
- Merge sort: Simple, O(n log n), preferred in interviews
- Fenwick/BIT: When coordinate compression or updates are needed
๐ FAANG Interview Patterns
Pattern 1: Naive Follow-up
โCan you solve it without modifying the array?โ
Solution: Use a separate result array during merge.
def merge_and_count_no_modify(left, right):
result = []
i = j = 0
count = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
count += len(left) - i
result.extend(left[i:])
result.extend(right[j:])
return result, count
Pattern 2: Modified Definition
โNow count โnear-inversionsโ: pairs where i < j and arr[i] > arr[j] * 2.โ
Insight: Logic stays the same; only change the comparison:
if left[i] > right[j] * 2: # Modified condition
inversion_count += len(left) - i
Pattern 3: Return Actual Pairs
Already covered in Variation 1 above.
Pattern 4: Constraints on Value Range
โArray has values up to 10^5. Can you optimize?โ
Answer: Use Fenwick Tree (Variation 3) with coordinate compression. Merge sort is already optimal, but Fenwick can be faster in practice for certain distributions.
๐ ๏ธ Edge Cases & Tricky Inputs
| Case | Input | Expected | Notes |
|---|---|---|---|
| Empty array | [] | 0 | Handle in wrapper |
| Single element | [1] | 0 | Base case: low >= high |
| Already sorted | [1,2,3,4] | 0 | Best case |
| Reverse sorted | [4,3,2,1] | 6 | Max inversions: n(n-1)/2 |
| All equal | [5,5,5,5] | 0 | Equality handling: donโt count |
| Duplicates | [3,1,3,1] | 4 | Use <= comparison |
Key: Test with duplicates to ensure you handle the == case correctly.
๐ Key Insights
-
Inversion โ โUnsortednessโ: The more inversions, the further from sorted.
-
Merge Sort is Natural: Divide-and-conquer naturally separates local and split inversions.
-
The Trick: When
left[i] > right[j], ALL remaining elements in left also invert withright[j]. This is the O(n log n) magic. -
Index Confusion is Common: Always use
len(subarray) - i, notoriginal_index - i. -
Space Tradeoff: Merge sort uses O(n) space. If space is ultra-critical, Fenwick Tree uses O(n) as well but can handle updates.
-
Interview Hierarchy:
- Explain: Start with O(nยฒ) brute force
- Optimize: Suggest merge sort approach
- Code: Implement cleanly
- Optimize: Discuss Fenwick Tree if asked about constraints
๐ Related Problems
- [[Merge Sort Notes|Merge Sort]] โ Core algorithm
- [[Number of Smaller Elements to the Right|LeetCode 315]] โ Count smaller elements; uses similar merge sort approach
- [[Reverse Pairs|LeetCode 493]] โ Count pairs where arr[i] > 2*arr[j]
- [[Fenwick Tree Notes|Fenwick/BIT]] โ Alternative approach for complex constraints
- [[Coordinate Compression|Technique]] โ Handling large value ranges
๐ก Interview Script
You: โThe brute force is O(nยฒ) โ check every pair. But we can optimize using merge sort.โ
Explain: โDuring merge, when an element from the right is smaller than an element from the left, all remaining left elements form inversions with it. We count these in O(n log n).โ
Code: [Live code the solution]
Optimize: โIf the array canโt be modified, use a separate result array. If we need actual pairs, track indices. If values are huge, use coordinate compression with a Fenwick Tree.โ
โ Checklist for Code Review
- Handle empty array
- Base case:
low >= highreturns 0 - Correctly split:
mid = (low + high) // 2 - Three recursive calls: left, right, merge
- Use
len(left) - i, NOTmid - i + 1 - Handle
==case (donโt double-count) - Merge exhaustion loops are complete
- Test with duplicates and reverse-sorted arrays