Algorithm Search⌘K

Permutation rank and unrank algorithm in Python

Compute the rank of a permutation, or the permutation of a given rank, in O(n^2) or O(n log n) time.

File
permutation_rank_unrank.py, 83 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
permutation_rank_unrank.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/permutation-rank-unrank/. The authors are listed in prov/NW945N.jsonl.

from typing import Sequence


def _full_tree(n: int) -> list[int]:
    """Fenwick tree (1-based) in which every value 1..n is present once."""
    return [0] + [i & -i for i in range(1, n + 1)]


def _prefix(tree: list[int], i: int) -> int:
    """Number of present values among 1..i."""
    s = 0
    while i > 0:  # @line L16
        s += tree[i]
        i -= i & -i
    return s  # @line L17


def _add(tree: list[int], n: int, i: int, d: int) -> None:
    """Add d to the count of value i."""
    while i <= n:  # @line L18
        tree[i] += d
        i += i & -i


def _select(tree: list[int], n: int, k: int) -> int:
    """Return the k-th smallest present value, for 1 <= k <= number present."""
    pos = 0  # @line L13
    step = 1 << (n.bit_length() - 1) if n > 0 else 0
    while step > 0:  # @line L14
        if pos + step <= n and tree[pos + step] < k:
            pos += step
            k -= tree[pos]
        step >>= 1
    return pos + 1  # @line L15


def rank(a: Sequence[int]) -> int:
    """Return the lexicographic rank (from 0) of a permutation a of 1..n.

    Time O(n log n) arithmetic operations; the rank is an exact Python int.
    """
    n = len(a)
    if sorted(a) != list(range(1, n + 1)):
        raise ValueError("a must be a permutation of 1..n")
    tree = _full_tree(n)  # @line L1
    r = 0
    for i in range(1, n + 1):  # @line L2
        c = _prefix(tree, a[i - 1] - 1)  # @line L3
        _add(tree, n, a[i - 1], -1)  # @line L4
        r = r * (n - i + 1) + c  # @line L5
    return r  # @line L6


def unrank(r: int, n: int) -> list[int]:
    """Return the permutation of 1..n with lexicographic rank r, for 0 <= r < n!."""
    if n < 0 or r < 0:
        raise ValueError("need n >= 0 and r >= 0")
    d = [0] * (n + 1)
    for i in range(n, 0, -1):  # @line L7
        d[i] = r % (n - i + 1)
        r //= n - i + 1
    if r != 0:
        raise ValueError("r must be less than n!")
    tree = _full_tree(n)  # @line L8
    a = [0] * n
    for i in range(1, n + 1):  # @line L9
        a[i - 1] = _select(tree, n, d[i] + 1)  # @line L10
        _add(tree, n, a[i - 1], -1)  # @line L11
    return a  # @line L12


def lehmer_code(a: Sequence[int]) -> list[int]:
    """Return c with c[i] = number of j > i with a[j] < a[i], in O(n log n) time."""
    n = len(a)
    tree = _full_tree(n)
    out = []
    for x in a:
        out.append(_prefix(tree, x - 1))
        _add(tree, n, x, -1)
    return out
test_permutation_rank_unrank.py 71 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/permutation-rank-unrank/. The authors are listed in prov/NW945N.jsonl.

import itertools
import math
import random

import pytest

from permutation_rank_unrank import lehmer_code, rank, unrank


def brute_rank(a: list[int]) -> int:
    for k, p in enumerate(itertools.permutations(sorted(a))):
        if list(p) == list(a):
            return k
    raise AssertionError


def test_example() -> None:
    assert rank([3, 1, 4, 5, 2]) == 51
    assert unrank(51, 5) == [3, 1, 4, 5, 2]
    assert lehmer_code([3, 1, 4, 5, 2]) == [2, 0, 1, 1, 0]
    assert rank([1, 3, 6, 5, 4, 2]) == 47
    assert unrank(48, 6) == [1, 4, 2, 3, 5, 6]
    assert rank([3, 1, 6, 2, 5, 4]) == 259


def test_edges() -> None:
    assert rank([]) == 0
    assert unrank(0, 0) == []
    assert rank([1]) == 0
    assert unrank(0, 1) == [1]
    assert rank(list(range(1, 11))) == 0
    assert rank(list(range(10, 0, -1))) == math.factorial(10) - 1
    assert unrank(math.factorial(10) - 1, 10) == list(range(10, 0, -1))
    with pytest.raises(ValueError):
        unrank(math.factorial(5), 5)
    with pytest.raises(ValueError):
        rank([1, 1, 2])


def test_all_small_permutations_in_order() -> None:
    for n in range(0, 7):
        for k, p in enumerate(itertools.permutations(range(1, n + 1))):
            assert rank(p) == k
            assert unrank(k, n) == list(p)


def test_big_n_exact() -> None:
    n = 60
    a = list(range(n, 0, -1))
    assert rank(a) == math.factorial(n) - 1
    assert unrank(math.factorial(n) - 1, n) == a


def test_random_against_brute_force() -> None:
    rng = random.Random(6121)
    for _ in range(600):
        n = rng.randint(0, 7)
        a = list(range(1, n + 1))
        rng.shuffle(a)
        r = rank(a)
        assert r == brute_rank(a)
        assert unrank(r, n) == a
        c = lehmer_code(a)
        assert c == [sum(1 for j in range(i + 1, n) if a[j] < a[i]) for i in range(n)]
    for _ in range(200):
        n = rng.randint(8, 80)
        r = rng.randrange(math.factorial(n))
        assert rank(unrank(r, n)) == r

What the next level needs

  • the tests did not run in this build

Read the full page about Permutation rank and unrank algorithm