Context:FIT2004_MOC · Applied 2 Problem 5 — the unit’s flagship “adapt a paradigm to a NEW problem” exercise (Divide and Conquer LO1): Merge Sort’s combine step is repurposed to count rather than merely order.
Parent Framework:Merge Sort
Quick Revision
🎯 Objective: count pairs (i,j) with i<j and V[i]>V[j] ➔ exhaustive search is Θ(N2); a merge sort that counts while it merges is Θ(NlogN).
⚡ Key Constraint: the recursive calls must return sorted subarrays — sortedness is not a side effect, it is the precondition that makes InvS countable in Θ(N) instead of Θ(N2).
📝 How It Works
1. The counting-argument that forces the design
Output can be Θ(N2) ➔ a reversed array has (2N) inversions, so any O(NlogN) algorithm cannot inspect inversions individually — it must count them in blocks. This is the observation that rules out every incremental approach before any code is written.
Application ➔ collaborative filtering: two users’ rankings of the same items are “similar” when few pairs disagree, so the inversion count is a distance measure between permutations.
2. The three-way partition of the inversion set
Every inversion sits in exactly one bucket ➔ both indices in the left half (InvL) · both in the right half (InvH) · one in each (InvS, a split inversion). Disjoint and exhaustive ⟹ the counts simply add.
Recursion handles two buckets for free ➔ the recursive calls already return InvL and InvH; the only new work is InvS, and it must cost Θ(N) to preserve the merge sort recurrence.
3. Counting split inversions during the merge
Sortedness is the lever ➔ at each merge step the smallest unconsumed element is the head of the left run or the head of the right run.
Take from LEFT ➔ zero ➔ that element’s index precedes every remaining right index and it is ≤ all of them, so it inverts with none of them.
Take from RIGHT ➔ add the whole left remainder ➔ if B[j] is emitted while A[i…n1] is unconsumed, then every one of those n1−i+1 elements is both larger and earlier ⟹ n1−i+1 split inversions, counted in O(1).
⚡ Key Constraint: each else branch counts a whole block of inversions with one addition — the block is exactly what buys Θ(N) per level over the Θ(N2) enumeration.
⚙️ Core Implementation
🔹 sort_and_count — merge sort with a counter threaded through
returns (sorted_slice, inversions); raw index arithmetic, no library calls
def sort_and_count(array, lo, hi): if lo == hi: # base: 1 element, 0 inversions return 0 mid = (lo + hi) // 2 inv_l = sort_and_count(array, lo, mid) # both indices left inv_h = sort_and_count(array, mid + 1, hi) # both indices right inv_s = merge_and_count(array, lo, mid, hi) # one index each return inv_l + inv_h + inv_sdef merge_and_count(array, lo, mid, hi): tmp = [0] * (hi - lo + 1) i, j, k, split = lo, mid + 1, 0, 0 while i <= mid or j <= hi: if j > hi or (i <= mid and array[i] <= array[j]): tmp[k] = array[i]; i += 1 # from LEFT -> count nothing else: tmp[k] = array[j]; j += 1 split += mid - i + 1 # from RIGHT -> whole left remainder k += 1 for k in range(len(tmp)): array[lo + k] = tmp[k] return split
💡 Common Mistake:Incrementing split by 1 ➔ emitting B[j] early does not reveal one inversion, it reveals mid−i+1 of them at once. A +1 silently returns the number of merge steps that saw an inversion, which is O(NlogN) and looks plausible.
💡 Common Mistake:Using < in the tie-break ➔ equal elements are not inversions (V[i]>V[j] is strict), so ties must be taken from the left with <=; < counts each tied pair as an inversion. The sheet assumes distinct integers, which hides the bug.
⚖️ Complexity
Approach
Best
Average
Worst
Auxiliary space
Why
Exhaustive pair scan
Θ(N2)
Θ(N2)
Θ(N2)
O(1)
tests all (2N) pairs
sort_and_count
Θ(NlogN)
Θ(NlogN)
Θ(NlogN)
Θ(N) scratch +Θ(logN) stack =Θ(N)
T(N)=2T(N/2)+Θ(N), r=a/b=1
T(N)=2T(N/2)+cN⇒T(N)=Nb+cNlog2N=Θ(NlogN)
When It Flips: the counting version is asymptotically free — it inherits Merge Sort's bounds exactly, because the added work is one integer addition inside an existing loop iteration. It destroys the input order, so keep a copy if the original permutation is still needed.
📊 Exam Execution Trace
Top-level merge of V=[3,1,4,2] after both halves return InvL=1 (from (3,1)) and InvH=1 (from (4,2)); left run A=[1,3] at lo=0,mid=1, right run B=[2,4]:
Step
Head left
Head right
Take
split +=
Running split
tmp
0 (Init)
1 (i=0)
2 (j=2)
—
—
0
[]
1
1
2
left (1≤2)
0
0
[1]
2
3 (i=1)
2
right (3>2)
mid−i+1=1
1
[1,2]
3
3
4 (j=3)
left (3≤4)
0
1
[1,2,3]
4
— (i>mid)
4
right (drain)
0
1
[1,2,3,4]
Read-off:InvS=1 — the pair (3,2), the only inversion straddling the halves. Total =1+1+1=3, matching the exhaustive count {(3,1),(3,2),(4,2)} ✓.
⚠️ Common Mistakes
💡 Counting on the drain ➔ once the left run is exhausted (i>mid) the remaining right elements invert with nothing; adding mid−i+1 there yields a negative or bogus term. Guard the count inside the else branch only.
💡 Sorting first, then counting ➔ a sorted array has zero inversions, so the count must be accumulated during the sort. Any two-pass “sort then compare against the original” scheme is back to Θ(N2).
💡 Claiming the pattern is merge-sort-specific ➔ the transferable move is “attach an accumulator to a D&C combine step whose subproblems must be preprocessed”; the exam reward is naming that shape, not reciting this instance.
🧠 Active Recall
Why can no O(NlogN) inversion-counting algorithm look at inversions one at a time?
Hint: Bound the size of the output quantity, not the input.
Answer
Short answer: a reversed array has (2N)=Θ(N2) inversions, so enumerating them is already Ω(N2) work.
Why:Count in blocks ➔ the algorithm must add many inversions per operation; the merge’s split += mid - i + 1 retires an entire block of them with one addition, which is exactly how Θ(N2) facts fit inside Θ(NlogN) steps.
The recursive calls are asked to sort as well as count. Is the sorting incidental, or load-bearing?
Hint: What does the merge step assume about its two inputs?
Answer
Short answer: load-bearing — without sorted halves the “take from the right ⟹ the whole left remainder inverts” rule is false, and split inversions would have to be counted pairwise at Θ(N2) per level.
Why:Sortedness converts a comparison into a block count ➔ because A[i…mid] is ascending, B[j]<A[i] implies B[j]<A[t] for everyt≥i in one deduction. The sort is the preprocessing that makes the Θ(N) combine possible.
State the general pattern this problem teaches, in a form usable on an unseen exam question.
Hint: The examiner will not say “merge sort”.
Answer
Short answer: partition the answer set by where its members’ indices fall relative to the split, let recursion supply the within-half counts, and design the combine so the cross-half count costs Θ(N).
Why:Disjoint-and-exhaustive decomposition ➔ D&C only pays off when the quantity being computed decomposes additively across the split; identifying the cross-half term and finding a linear way to compute it is the whole design step, and the resulting 2T(N/2)+Θ(N) is Θ(NlogN) by the r=a/b=1 regime in Solving Recurrences (Telescoping).