Repository: scalekit (difficulty: hard)

Issue:
kth_permutation cannot reach large ranks

kth_permutation(3, 5) == [3, 1, 2] and kth_permutation(4, 24) ==
[4, 3, 2, 1] agree with itertools.permutations, and out-of-range ranks
raise ValueError as documented (k < 1 or k > n!). But the documented
bounds are n up to 60 and k up to min(n!, 10**18), and the current
implementation walks the whole permutation stream to position k:
kth_permutation(60, 10**18) would have to yield 10**18 - 1
permutations first, and I stopped it after ten minutes. Expected: the
list whose 1-indexed lexicographic rank among permutations of 1 .. 60
is exactly 10**18, in microseconds via factoradic unranking. The tests
verify large results by re-ranking the returned list with an
independent O(n**2) rank routine and by checking it is a permutation
of 1 .. n, so no shortcut can fake it.

The suite runs each large call in a watchdog worker process with an 8
second per-call budget, so the stream-walking implementation now fails
the suite instead of hanging it. Bounds: 1 <= n <= 60; valid calls
have 1 <= k <= n! and tested ranks go up to 10**18. The contract is
unchanged: return the k-th (1-indexed) permutation of 1 .. n in
lexicographic order as a list, raising ValueError for k < 1 or
k > n!.

Source files:
core.py: """Permutation ordering helpers for scalekit.

This module exposes kth_permutation(n, k), which returns the k-th
permutation (1-indexed) of the integers 1 .. n in lexicographic order,
as a Python list. If k < 1 or k > n! the function raises ValueError.

Contract and bounds:
- n is an integer with 1 <= n <= 60
- valid calls have 1 <= k <= n!, with tested k up to 10**18
- any single valid call in the stated range must complete within a few
  seconds on commodity hardware
"""

import math
from itertools import islice, permutations


def kth_permutation(n, k):
    """Return the k-th permutation by walking the lexicographic stream.

    itertools.permutations yields the permutations of a sorted input in
    lexicographic order, so the k-th one is the item at offset k - 1 of
    that stream.
    """
    if k < 1 or k > math.factorial(n):
        raise ValueError(
            "k must satisfy 1 <= k <= n! (got n={}, k={})".format(n, k)
        )
    perm = next(islice(permutations(range(1, n + 1)), k - 1, None))
    return list(perm)


Test files:
test_core.py: """Tests for core.kth_permutation.

Large calls run in a watchdog worker process with a hard per-call time
budget: within the documented bounds (n up to 60, k up to 10**18) only
factoradic unranking can answer, so a correct walk of the permutation
stream fails these tests by running out of budget. Large results are
checked with an independent rank routine, so the returned list must
have exactly the requested lexicographic rank.
"""

import math
import multiprocessing as mp
import random
from itertools import permutations

import pytest

from core import kth_permutation

LARGE_BUDGET_SECONDS = 8.0


def _worker(args, q):
    q.put(kth_permutation(*args))


def _call_with_timeout(args, timeout=LARGE_BUDGET_SECONDS):
    ctx = mp.get_context("spawn")
    q = ctx.Queue()
    p = ctx.Process(target=_worker, args=(args, q))
    p.start()
    p.join(timeout)
    if p.is_alive():
        p.terminate()
        p.join()
        raise AssertionError(
            "kth_permutation{} did not finish within {}s; the stated bounds "
            "need factoradic unranking, not stream walking".format(
                tuple(args), timeout
            )
        )
    if p.exitcode != 0:
        raise AssertionError(
            "kth_permutation{} worker exited with code {}".format(
                tuple(args), p.exitcode
            )
        )
    return q.get(timeout=5.0)


def _rank(perm):
    # 1-indexed lexicographic rank of a permutation of 1 .. n, O(n**2)
    n = len(perm)
    remaining = sorted(perm)
    r = 0
    for i, v in enumerate(perm):
        idx = remaining.index(v)
        r += idx * math.factorial(n - 1 - i)
        remaining.pop(idx)
    return r + 1


def test_small_matches_itertools():
    for n in range(1, 8):
        expected = [list(p) for p in permutations(range(1, n + 1))]
        if n <= 5:
            ks = list(range(1, len(expected) + 1))
        else:
            ks = random.Random(1014).sample(range(1, len(expected) + 1), 60)
        for k in ks:
            assert kth_permutation(n, k) == expected[k - 1]


def test_rejects_out_of_range_k():
    with pytest.raises(ValueError):
        kth_permutation(3, 7)
    with pytest.raises(ValueError):
        kth_permutation(5, 0)
    with pytest.raises(ValueError):
        kth_permutation(8, math.factorial(8) + 1)


def test_large_n60_rank_round_trip():
    perm = _call_with_timeout((60, 10**18))
    assert sorted(perm) == list(range(1, 61))
    assert _rank(perm) == 10**18


def test_large_n40_rank_round_trip():
    perm = _call_with_timeout((40, 123456789012345678))
    assert sorted(perm) == list(range(1, 41))
    assert _rank(perm) == 123456789012345678


Patch:
--- a/core.py
+++ b/core.py
@@ -12,19 +12,24 @@
 """
 
 import math
-from itertools import islice, permutations
 
 
 def kth_permutation(n, k):
-    """Return the k-th permutation by walking the lexicographic stream.
+    """Return the k-th permutation via factoradic unranking.
 
-    itertools.permutations yields the permutations of a sorted input in
-    lexicographic order, so the k-th one is the item at offset k - 1 of
-    that stream.
+    Writing k - 1 in the factorial number system gives, digit by digit,
+    the index of the next element to take from the sorted pool of
+    remaining values. Each of the n steps pops from a list of at most n
+    items, so the whole call is O(n**2) regardless of how large k is.
     """
     if k < 1 or k > math.factorial(n):
         raise ValueError(
             "k must satisfy 1 <= k <= n! (got n={}, k={})".format(n, k)
         )
-    perm = next(islice(permutations(range(1, n + 1)), k - 1, None))
-    return list(perm)
+    pool = list(range(1, n + 1))
+    result = []
+    r = k - 1
+    for i in range(n - 1, -1, -1):
+        idx, r = divmod(r, math.factorial(i))
+        result.append(pool.pop(idx))
+    return result