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 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 () · both right () · one in each (, a split inversion). Disjoint and exhaustive ⟹ the counts add.
  • Recursion handles two buckets for free ➔ 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 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 is emitted while is unconsumed, every one of those elements is both larger and earlier ⟹ split inversions in .
  • ⚡ Key Constraint: each else branch retires a whole block with one addition — the block is what buys per level over the enumeration.

⚙️ Core Implementation

🔹 sort_and_count — merge sort with a counter threaded through

⚖️ Complexity

ApproachBest / Average / WorstAuxiliary 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 and ; 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 ✓.

⚠️ Common Mistakes

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