Backtracking · 19 of 19
Find the K-th Permutation Sequence of first N natural numbers
Line up every order of the numbers 1 to n like words in a dictionary, and jump straight to the k-th one without writing the others.
- Simple way
- time O(n * n!)
room O(n) - Clever way
- time O(n^2)
room O(n)
The problem, simply
Take the numbers 1 to n. Write down every order they can stand in, sorted like words in a dictionary: 123, 132, 213, and so on. There are n! orders.
Give back the order at position k, counting from 1.
A story to remember it
Picture it
All 6 orders of 1, 2, 3 as a choice tree. Read the leaves left to right: 123, 132, 213, 231, 312, 321. Order number 3 is 213.
- answer
- path
Examples
A small one
- In
n = 3, k = 3- Out
213
The orders are 123, 132, 213, 231, 312, 321. The third is 213.
A bigger one
- In
n = 4, k = 9- Out
2314
Orders 1 to 6 start with 1. Orders 7 to 12 start with 2. Inside those, 7 and 8 start with 21, and 9 and 10 start with 23. Order 9 is 2314.
The very first
- In
n = 3, k = 1- Out
123
The first order is always the numbers in rising order.
The very last
- In
n = 3, k = 6- Out
321
The last order is always the numbers in falling order.
One number
- In
n = 1, k = 1- Out
1
Only one order exists.
Way 1: the simple way
Build the orders one by one in dictionary order, and stop at the k-th.
- At each spot, try the unused numbers from smallest to biggest. This is what makes the order match the dictionary.
- When all
nspots are filled, count one more order. - When the count reaches
k, save it and stop everything.
class Solution {
private int count;
private String answer;
public String getPermutation(int n, int k) {
count = 0;
answer = "";
build(n, k, new StringBuilder(), new boolean[n + 1]);
return answer;
}
private void build(int n, int k, StringBuilder cur, boolean[] used) {
if (count >= k) return; // already found it
if (cur.length() == n) {
count++;
if (count == k) answer = cur.toString();
return;
}
for (int d = 1; d <= n; d++) { // smallest first keeps dictionary order
if (used[d]) continue;
used[d] = true;
cur.append(d);
build(n, k, cur, used);
cur.deleteCharAt(cur.length() - 1); // undo
used[d] = false;
}
}
}How long it takes: when k is near n!, we build almost every order, and each one takes n steps, so O(n * n!). For n = 9 that is fine, but it grows terribly fast. Extra room: the current order, the used marks and the call stack, O(n).
Way 2: the clever way
Do not walk the tree. Jump through it. With m numbers left, each choice for the next spot owns a block of (m - 1)! orders. So dividing tells you which block you are in.
- Turn
kinto a count from 0:k = k - 1. Nowkis how many orders to skip. - Keep a list of numbers not yet used, smallest first.
- For each spot, with
mnumbers left:block = (m - 1)!. The index of the next number isk / block. Take that number out of the list and add it to the answer. - Keep only the skip inside that block:
k = k % block.
Jump straight to order number 9
01 / 06
There are 4 x 3 x 2 x 1 = 24 orders. They come in 4 groups of 6, one group per first number. We want order 9, so we count from 0 and look for number 8.
- k
- 8
- group size
- 6
- looking now
- answer
- path
- ignored
import java.util.*;
class Solution {
public String getPermutation(int n, int k) {
int[] fact = new int[n + 1];
fact[0] = 1;
for (int i = 1; i <= n; i++) fact[i] = fact[i - 1] * i;
List<Integer> left = new ArrayList<>();
for (int i = 1; i <= n; i++) left.add(i); // numbers not used yet, smallest first
k--; // how many orders to skip
StringBuilder sb = new StringBuilder();
for (int m = n; m >= 1; m--) {
int block = fact[m - 1]; // orders per choice at this spot
int index = k / block; // how many whole blocks we skip
sb.append(left.remove(index)); // take that number out
k %= block; // skip left inside the block
}
return sb.toString();
}
}How long it takes: n spots, and removing from the list costs up to n each time, so O(n^2). Extra room: the factorials and the list, O(n).
Watch out for
- Forgetting
k - 1. Withkcounted from 1,k / blockis off by one at the edges, for examplek = 6in a block of 6. - Removing by value instead of by index. In Java,
left.remove(index)with anintremoves by index, which is what we want. With anIntegerit removes by value. - Overflow.
13!does not fit in anint. The usual limit isn <= 9, but say so.
What interviewers ask next
- The reverse: given an order, find its position. For each spot, count the unused numbers smaller than the one there, multiply by
(m - 1)!, add them all, then add 1. - Make it faster than
O(n^2)for bign. Keep the unused numbers in a Fenwick tree or an order statistic tree, so "find the index-th unused number" takesO(log n). TotalO(n log n). - What if the numbers have repeats, like
1, 1, 2? Then each different number you could pick owns(m - 1)! / (c1! * c2! * ...)orders, where thecvalues are how many of each number are left after the pick. Walk the different numbers in order and subtract whole blocks the same way. - Give the next order after a given one. That is next permutation: find the first dip from the right, swap it with the next bigger number to its right, and reverse the tail.
Remember this
Grown-up words: dictionary order is lexicographic order, the orders are permutations, the groups of size (m - 1)! come from the factorial number system, and the digits k / block form the Lehmer code.