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 rules out every incremental approach before any code is written.
Application ➔ collaborative filtering: two users’ rankings 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 left (InvL) · both right (InvH) · one in each (InvS, a split inversion). Disjoint and exhaustive ⟹ the counts add.
Recursion handles two buckets for free ➔ 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 heads the left run or the right run.
Take from LEFT ➔ zero ➔ that element precedes every remaining right index and is ≤ all of them, so it inverts with none.
Take from RIGHT ➔ add the whole left remainder ➔ if B[j] is emitted while A[i…mid] is unconsumed, every one of those mid−i+1 elements is both larger and earlier ⟹ mid−i+1 split inversions in O(1).
⚡ Key Constraint: each else branch retires a whole block with one addition — the block is what buys Θ(N) per level over the Θ(N2) enumeration.
⚙️ Core Implementation
🔹 sort_and_count — merge sort with a counter threaded through
returns the inversion count; 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 reveals mid−i+1 inversions at once, not one. A +1 silently returns the number of merge steps that saw an inversion — O(NlogN), and plausible-looking.
💡 Common Mistake:Using < in the tie-break ➔ equal elements are not inversions (V[i]>V[j] is strict), so ties must come from the left with <=. The sheet’s distinct integers hide the bug.
⚖️ Complexity
Approach
Best / Average / Worst
Auxiliary space
Why
Exhaustive pair scan
Θ(N2)
O(1)
tests all (2N) pairs
sort_and_count
Θ(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 and InvH=1; 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 {(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 bogus term. Guard the count inside the else branch only.
💡 Sorting first, then counting ➔ a sorted array has zero inversions, so the count must accumulate during the sort; any two-pass 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.
🧠 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 merge’s split += mid - i + 1 retires an entire block with one addition, which is 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 cost Θ(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.
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 pays off only when the quantity decomposes additively across the split; finding a linear way to compute the cross term is the design step, and 2T(N/2)+Θ(N) is Θ(NlogN) by the r=1 regime ➔ Solving Recurrences (Telescoping).