cswork / libraryOpen practice workspace

Algorithm tutorials

Permutations: Cycles, Next Permutation, Ranking and Inversions

Work with permutations as functions: cycle decomposition for minimum swaps and in-place rearrangement, inverses, the next lexicographic permutation, converting between a permutation and its rank with the factorial number system, and counting inversions by merge sort.

A permutation of 0..n-1 is a rearrangement, best viewed as a function i -> p[i]. Interview questions about permutations revolve around a few concrete tools: cycle decomposition (minimum swaps, in-place rearrangement), the next permutation in lexicographic order, converting between a permutation and its rank, and counting inversions.

Intuition

Cycles. Start at i and keep following i -> p[i] -> p[p[i]] -> ...; since p is a bijection you must return to i. So every permutation splits into disjoint cycles, e.g. p = [2, 0, 1, 4, 3] has cycles (0 2 1) and (3 4). A cycle of length L can be fixed with L - 1 swaps and no fewer, so sorting by arbitrary swaps needs exactly n - (number of cycles) swaps.

Inverse and composition. The inverse q satisfies q[p[i]] = i ("where did element x go?"). Composing permutations is applying one after the other; it is associative but not commutative. A permutation and its inverse have the same cycle lengths.

Inversions and parity. An inversion is a pair i < j with p[i] > p[j]. The number of inversions equals the minimum number of adjacent swaps needed to sort, and its parity equals the parity of n - cycles. Every swap flips that parity, so it tells you whether an even or an odd number of swaps is needed; this is what decides puzzles like "can this sliding-tile position be solved".

Lexicographic order. Listing permutations like words in a dictionary, each permutation has a rank. The first element splits the list into n blocks of (n-1)!, the second element splits each block into blocks of (n-2)!, and so on: a mixed-radix (factorial) number system. That gives both directions, permutation to rank and rank to permutation.

When to use it in interviews / OAs

  • "Minimum number of swaps to sort" or "to make two arrays equal" (cycles).
  • "Rearrange in place with O(1) extra space according to an index array" (walk the cycles).
  • "Next greater arrangement of the digits", "next permutation" (the suffix algorithm).
  • "k-th permutation", "rank of a permutation" (factorial number system).
  • "Count pairs out of order", "minimum adjacent swaps" (inversions with merge sort or a Fenwick tree).

Template: cycles

def cycles(p: list[int]) -> list[list[int]]:
    # p is a permutation of 0..n-1; follow i -> p[i] until the walk returns
    seen = [False] * len(p)
    out = []
    for start in range(len(p)):
        if not seen[start]:
            cyc, i = [], start
            while not seen[i]:
                seen[i] = True
                cyc.append(i)
                i = p[i]
            out.append(cyc)
    return out


def inverse(p: list[int]) -> list[int]:
    q = [0] * len(p)
    for i, x in enumerate(p):
        q[x] = i
    return q


def min_swaps_to_sort(arr: list[int]) -> int:
    # distinct values; a cycle of length L costs L - 1 swaps
    order = sorted(range(len(arr)), key=arr.__getitem__)  # order[k] = index of the k-th smallest
    return len(arr) - len(cycles(order))

order maps each sorted position to the index currently holding that value. It is the inverse of "index -> sorted position", and inverses have the same cycles, so either works.

Template: next permutation

Scan from the right for the first ascent a[i] < a[i+1]; everything after i is non-increasing, i.e. already the largest arrangement of that suffix. Swap a[i] with the rightmost element larger than it, then reverse the suffix to make it as small as possible.

def next_permutation(a: list) -> bool:
    # next lexicographic arrangement in place; returns False (leaving a sorted) after the last one
    i = len(a) - 2
    while i >= 0 and a[i] >= a[i + 1]:  # rightmost ascent a[i] < a[i + 1]
        i -= 1
    if i >= 0:
        j = len(a) - 1
        while a[j] <= a[i]:  # rightmost element larger than a[i]
            j -= 1
        a[i], a[j] = a[j], a[i]
    a[i + 1:] = reversed(a[i + 1:])  # the non-increasing suffix becomes its smallest arrangement
    return i >= 0

The non-strict comparisons make it correct with duplicate values too: it then walks through distinct arrangements only.

Template: rank and k-th permutation

The rank (1-based) is 1 + sum of L[i] * (n-1-i)!, where L[i] counts the later elements smaller than p[i]. A Fenwick tree over values gives each L[i] in O(log n). Going the other way, repeatedly divide k - 1 by (i-1)! to pick the next element from the remaining ones.

from math import factorial


def permutation_rank(p: list[int]) -> int:
    # 1-based lexicographic rank of a permutation of 1..n
    n = len(p)
    tree = [0] * (n + 1)  # Fenwick tree over values already seen to the right
    fact, rank = 1, 0
    for i in range(n - 1, -1, -1):
        smaller, v = 0, p[i] - 1
        while v > 0:  # how many later elements are smaller than p[i]
            smaller += tree[v]
            v -= v & -v
        v = p[i]
        while v <= n:
            tree[v] += 1
            v += v & -v
        rank += smaller * fact  # weight (n - 1 - i)!
        fact *= n - i
    return rank + 1


def kth_permutation(n: int, k: int) -> list[int]:
    # the k-th (1-based) permutation of 1..n in lexicographic order
    digits = list(range(1, n + 1))
    k -= 1
    out = []
    for i in range(n, 0, -1):
        idx, k = divmod(k, factorial(i - 1))  # which block of (i - 1)! permutations
        out.append(digits.pop(idx))
    return out

Template: counting inversions

Merge sort counts inversions while merging: when an element from the right half is placed before the remaining left elements, it forms an inversion with each of them.

def count_inversions(a: list[int]) -> int:
    arr = a[:]  # leave the caller's list untouched

    def sort(lo: int, hi: int) -> int:
        if hi - lo <= 1:
            return 0
        mid = (lo + hi) // 2
        count = sort(lo, mid) + sort(mid, hi)
        merged, i, j = [], lo, mid
        while i < mid and j < hi:
            if arr[i] <= arr[j]:
                merged.append(arr[i])
                i += 1
            else:
                merged.append(arr[j])
                j += 1
                count += mid - i  # arr[j] is smaller than every remaining left element
        merged += arr[i:mid] + arr[j:hi]
        arr[lo:hi] = merged
        return count

    return sort(0, len(arr))

Complexity

Cycle decomposition and inverse: O(n). Minimum swaps: O(n log n) because of the sort. Next permutation: O(n). Rank: O(n log n) with the Fenwick tree; k-th permutation: O(n^2) because of list.pop, fine for n up to a few thousand. Inversions: O(n log n).

Common pitfalls

  • Arbitrary swaps vs adjacent swaps: the first is n - cycles, the second is the inversion count.
  • Duplicates: cycle-based minimum swaps assumes distinct values; with duplicates the problem is harder (it becomes a cycle packing question).
  • 0-based vs 1-based: ranks and k are usually 1-based in statements, the math is cleaner 0-based. Convert once at the boundary.
  • Factorial overflow: 20! fits in a signed 64-bit integer, 21! does not. Ranks of long permutations need big integers or a modulus.
  • Comparisons in next permutation: with > instead of >= (or < instead of <=) the scans misbehave on repeated values, treating equal neighbors as an ascent and running past the array.

Interview variations

  • Next permutation, next greater number with the same digits (apply it to the digit list and check the 32-bit limit).
  • Permutation sequence: k-th permutation; rank of a permutation: Lehmer code.
  • Minimum swaps to sort, couples holding hands (cycles in a pairing graph), array nesting (longest cycle).
  • Count inversions, reverse pairs (a[i] > 2 * a[j], same merge sort with a separate counting pass).
  • Apply a permutation in place with O(1) extra space: rotate along each cycle, marking visited entries.
  • Solvability of sliding puzzles: parity of inversions.