Counting Inversions

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 with and ➔ exhaustive search is ; a merge sort that counts while it merges is .
  • 📦 Core Components: recurse left | recurse right | merge (split inversions) | total .
  • ⚡ Key Constraint: the recursive calls must return sorted subarrays — sortedness is not a side effect, it is the precondition that makes countable in instead of .

📝 How It Works

1. The counting-argument that forces the design

  • Output can be ➔ a reversed array has inversions, so any 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 () · both in the right half () · one in each (, a split inversion). Disjoint and exhaustive ⟹ the counts simply add.
  • Recursion handles two buckets for free ➔ the recursive calls already return and ; the only new work is , and it must cost 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 is emitted while is unconsumed, then every one of those elements is both larger and earlier split inversions, counted in .
  • ⚡ Key Constraint: each else branch counts a whole block of inversions with one addition — the block is exactly what buys per level over the enumeration.

⚙️ Core Implementation

🔹 sort_and_count — merge sort with a counter threaded through

⚖️ Complexity

ApproachBestAverageWorstAuxiliary spaceWhy
Exhaustive pair scantests all pairs
sort_and_count scratch stack ,

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 after both halves return (from ) and (from ); left run at , right run :

StepHead leftHead rightTakesplit +=Running splittmp
0 (Init) () ()[]
1left ()[1]
2 ()right ()[1,2]
3 ()left ()[1,2,3]
4— ()right (drain)[1,2,3,4]

Read-off: — the pair , the only inversion straddling the halves. Total , matching the exhaustive count ✓.

⚠️ Common Mistakes

  • 💡 Counting on the drain ➔ once the left run is exhausted () the remaining right elements invert with nothing; adding 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 .
  • 💡 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