Generating Inversion Table
This programming exercise comes from TAoCP, Vol 3, Section 5.1.1 – 6:
Design an algorithm that computes the inversion table
corresponding to a given permutation of , where the running time is essentially proportional to on typical computers.
Knuth’s solution in the book had me stumped for a while. He also mentions an alternative approach—essentially a modified merge sort. But let’s first unravel the bitwise algorithm.
Implementation
My C++ implementation differs slightly from Knuth’s original: I use 0-indexed arrays instead of his 1-indexed convention. Admittedly, this isn’t the most elegant version—my goal was simply to transform the pseudocode into something executable.
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char const* argv[])
{
// input
int n; cin >> n;
vector<int> v(n, 0);
for (auto& vi: v) cin >> vi;
// algorithm: TAoCP vol 3. 5.1.1-6
// init
vector<int> b(n, 0);
vector<int> x((n>>1)+1, 0);
int k = 0, r, s;
for (int nn=n; nn>1; nn >>= 1) k++; // compute floor(lg(n))
for (; k >= 0; k--) { // traversal through bits 0 -> \floor(\lg N)
for (int s = 0; s <= n>>(k+1); s++) // init array x = 0 \forall elements
x[s] = 0;
for (int j = 0; j < n; ++j) {
r = (v[j] >> k) & 1;
s = v[j] >> (k+1);
if (r) x[s] += 1;
else b[v[j]-1] += x[s];
}
}
// output
for (auto bi: b) cout << bi << " ";
cout << endl;
return 0;
}
Analysis
The running time is straightforward to analyze: the outer loop on
But how on earth does this algorithm actually work? The solution is too subtle to grasp on first encounter.
Given the index of bitstring x[] plays
a role as a counter which checks how many elements of
For example, given the input sequence
5: 0101
9: 1001
1: 0001
8: 1000
2: 0010
6: 0110
4: 0100
7: 0111
3: 0011
Let’s trace through the algorithm step by step:
k=3., x[0] = 2.b = 1 2 2 2 0 2 2 0 0.k=2. Arrayxhave two items, and whose values are 4, 0, respectively. b = 2 3 6 2 0 2 2 0 0.k=1. There are 3 posibilities of, namely , , whose values are 2, 2, 0, respectively. Eventually, b is 2 3 6 3 0 2 2 0 0.k=0, There are 5 items inx:, , , , , we get the final output b = 2 3 6 4 0 2 2 1 0.
The mergesort-based algorithm
Rather than constructing an inversion table, this algorithm counts the total number of inversions in a permutation by leveraging the merging procedure from the venerable mergesort.
class inversion {
private:
vector<int> x, aux;
long long int cnt;
public:
inversion(const vector<int>& x_): x(x_), aux(x_.size()), cnt(0) {
merge_sort(x, 0, x.size());
} //ctor
void merge_sort(vector<int>& a, int low, int high) {
if (low >= high-1) return ;
int mid = low + (high-low)/2;
merge_sort(a, low, mid);
merge_sort(a, mid, high);
merge(a, low, mid, high);
}
void merge(vector<int>& a, int low, int mid, int high) {
int subidx_1 = low;
int subidx_2 = mid;
for (int j = low; j < high; ++j) {
if (subidx_1 >= subidx_2) aux[j] = a[subidx_2++];
else if (subidx_2 >= high) aux[j] = a[subidx_1++];
else if (a[subidx_1] <= a[subidx_2]) aux[j] = a[subidx_1++];
else {
cnt += (mid-subidx_1); // (1)
aux[j] = a[subidx_2++];
}
}
// copy back to a
copy(aux.begin()+low, aux.begin()+high, a.begin()+low);
}
inline long long int count() const { return cnt;}
};
This corresponds to exercise 5.2.5 – 21 in TAoCP—unfortunately, Knuth left the solution as an exercise for the reader. The key modification is adding line (1) to the merging step, which counts inversions for a[subidx_2]—the element from the second array currently being merged.
Here’s the insight: elements from mid to subidx_2 are all less than or equal to a[subidx_2], contributing zero inversions. However, when a[subidx_1] > a[subidx_2], the sorted-array invariant tells us that all elements from subidx_1 to mid must also exceed a[subidx_2]. Hence, we accumulate exactly mid - subidx_1 inversions.
Reference
- gywangmtl’s post: This post was instrumental in helping me understand the algorithm. And thank you, Google Translate!