Permutations and arrangements are common concepts in many kinds of problems.
This article is not about the number of permutations
This article discusses permutations of a set, rather than the number of arrangements counted in permutations and combinations. For the latter topic, see Permutations and combinations.
Convention
Unless otherwise stated, this article always discusses finite sets.
Definition
A bijection (that is, a one-to-one correspondence) from a set to itself, denoted by , is a permutation of . If the set also has a total order, its permutation is also commonly called a (full) arrangement. This total order is called the natural order on the set.
“Permutation” and “arrangement”
In Chinese, “permutation” usually refers to changing the order of elements, while “arrangement” usually refers to putting elements into a sequence. When the elements have a natural order, the two concepts are the same: an “arrangement” can be viewed as the result of a “permutation”; relative to the natural order of the elements, the order of the elements in an “arrangement” specifies the “permutation”. Since this article assumes that a natural order exists on the set whenever it uses the word “arrangement”, it does not deliberately distinguish between the two concepts.
Of course, elements without a natural order can also be “arranged”. Such “arrangements” often occur in combinatorial counting problems, which are outside the scope of this article.
If the size of a set is , then the total number of permutations of is . In particular, : the empty set has exactly one permutation, namely the empty permutation.
Notation
Permutations concern the correspondences between elements, not what the elements specifically are. Therefore, when discussing a set of size , we usually assume that the set is ; when a natural order is needed, we usually use the natural order on the natural numbers.
Representations
There are several ways to represent a permutation. We use the following permutation as an example to discuss them.
Two-line notation
A permutation on the set can be represented as
This means that permutation maps to . Of course, we need . In the two-line notation of a permutation, the order in which elements appear in the first row is not important; what matters is the correspondence between the two rows.
For example, the example above can be written in two-line notation as
and it can of course also be written as
One-line notation
In many cases, the set has a natural order. If the first row in two-line notation is assumed to be written in natural order and is omitted, the permutation can be represented as
This is closer to the concept of an arrangement in natural language. Therefore, “arrangement” is sometimes used to refer to this ordered tuple.
The example above can be written in one-line notation as
This one-line notation is often used to compare the sizes of different permutations.
Cycle notation
A permutation also has a more compact representation called its cycle notation. It represents a permutation as a product of pairwise disjoint cycles. The steps for writing a given permutation in cycle notation are described below.
Given a permutation , write it in cycle notation as follows:
- If still contains an element that has not been written, write a left parenthesis and then write any such element;
- If the most recently written element is ,
- if has already been written, write a right parenthesis and return to step 1;
- if has not yet been written, write and continue with step 2;
- Stop when every element of has been written.
The elements inside each pair of parentheses form a cycle. The number of elements inside the parentheses is called the length of the corresponding cycle. In practice, cycles of length one are often omitted.
The example above can be written in cycle notation as
In the identity transformation, all cycles have length one, and it is common to write rather than omit them all.
Composition
Composition of permutations is composition of mappings. It is also commonly called multiplication of permutations.
Given two permutations
the product is
In simple terms, first apply the mapping , then apply the mapping . Note that in the two-line notation above, the order of the second row of the inner mapping is the same as the order of the first row of the outer mapping .
Because permutations and are fundamentally mappings, . The order of composition of permutations is from right to left. Multiplication of permutations is not commutative, so using the wrong order can produce an incorrect result.
The product of several consecutive copies of a permutation is called a power of the permutation, and binary exponentiation can accelerate its computation.
Inverse permutation
Because a permutation is a bijection, it always has an inverse permutation.
Given the permutation
its inverse is
In cycle notation, invert each cycle to obtain the inverse permutation. To invert a cycle, simply reverse the order in which its elements are written. For example, the inverse of the permutation above has cycle notation
A permutation of and the sequence of the ranks of its elements are inverse permutations of each other.
Cycles
A cycle is itself a special permutation. The defining property of a cycle is that, starting from any point in the cycle, repeatedly applying the permutation reaches any other point in the cycle. A cycle of length is also called a -cycle (also written as a -cycle). Repeatedly applying a -cycle times gives the identity transformation: every element returns to its original position.
The cycle notation of a permutation can be viewed as expressing it as a product of these special permutations (cycles), so it can also be viewed as the cycle decomposition of the permutation. For every permutation, its decomposition into a product of cycles is unique up to the order of the cycles. Cycles can be viewed as the basic building blocks of a permutation.
The cycle decomposition of a permutation has a clear geometric meaning. If a permutation on a set is represented by treating each ordered pair in it as an edge of a directed graph whose vertices are , then the cycles are the loops in this graph. If permutation decomposes into cycles, the corresponding directed graph has loops in total, including self-loops. These loops are naturally pairwise disjoint.
Fixed points
A -cycle is a fixed point of a permutation. For a set and a permutation on it, usually denotes the set of fixed points of , namely .
Transpositions
A -cycle is also called a transposition. In other words, a transposition is a permutation that swaps exactly one pair of elements. Its cycle notation is , meaning that it swaps the positions of and .
Every permutation can be written as a product of transpositions. This means that any ordering of elements can be restored to a specified sorted order by repeatedly swapping two elements. This is exactly what comparison-based sorting algorithms do.
Moreover, the correctness of bubble sort actually shows that every permutation can be written as a product of adjacent transpositions. An adjacent transposition is a transposition that swaps two adjacent elements.
Properties
Applications often require us to consider properties of an individual permutation.
Parity
The decomposition of a cycle into transpositions is not unique. For example,
However, when a permutation is decomposed into a sequence of transpositions, the parity of the number of transpositions required is fixed. The parity of the number of transpositions in a decomposition of a permutation is also called the parity of the permutation.
A permutation that can be decomposed into a product of an even number of transpositions is called an even permutation; one that can be decomposed into a product of an odd number is called an odd permutation. When , the numbers of odd and even permutations of size are equal.
Sign
Based on the parity of a permutation, we can define its sign, denoted by . The sign of an even permutation is defined as , and that of an odd permutation as .
The sign of a product of permutations equals the product of their signs:
In other words, composing two permutations with the same parity gives an even permutation, while composing two permutations with different parities gives an odd permutation. From the viewpoint of decomposing into transpositions, this conclusion is immediate.
In particular, a single transposition necessarily changes the parity of a permutation. This also explains why, although the decomposition into transpositions is not unique, the parity of the required number of transpositions is fixed.
The sign of a permutation appears in the Leibniz expansion of the determinant.
Order of a permutation
The order of a permutation is the smallest positive integer satisfying the following condition: after applying the permutation repeatedly times, every element returns to its original position. That is,
On a finite set, every permutation has finite order. This means that, starting from an initial ordering, repeatedly scrambling a given sequence according to a fixed pattern will always restore the original arrangement in finite time.
Cycle type
Decompose a permutation of elements into cycles. The cycle type of the permutation is the multiset of the cycle lengths in the decomposition. These lengths form an integer partition of . If cycles of length occur in the decomposition, with of them, the cycle type is commonly written as
where the coefficients satisfy .
Given a cycle type, the number of distinct permutations is
Analysis
This is because every permutation of can be partitioned according to the cycle type into a corresponding cycle decomposition. However, the order of cycles of the same length does not affect the permutation, so we divide by . In addition, each individual cycle is actually a circular arrangement, and choosing its starting point does not affect the permutation, so we divide by . This gives the formula above.
If only the number of cycles in the permutation decomposition is given, the number of distinct permutations is the Stirling number of the first kind . The number of distinct cycle types for permutations of length is the partition number .
The cycle type makes it easy to determine properties such as the order and parity of a permutation.
Because the order of a -cycle is and distinct cycles are disjoint, the order of permutation is
Likewise, because the parity of a -cycle is opposite to the parity of , the parity of permutation is the parity of
Here, is the number of cycles, including -cycles (fixed points).
Cycle types play an important role in Pólya enumeration.
Topics related to arrangements
If the set itself has a natural order, permutation is commonly called an arrangement and represented in one-line notation as
Do not confuse this with a cycle.
Inversions
In an arrangement, if a larger number appears before a smaller number, the two numbers form an inversion. The comparison here uses the natural order.
The total number of inversions in an arrangement is called the inversion count of the permutation. The inversion count is the minimum number of adjacent transpositions needed to restore it to the sorted sequence. Therefore, the parity of the inversion count agrees with the parity of the corresponding permutation. This gives an equivalent definition of permutation parity.
Algorithms for computing inversion counts include merge sort and the binary indexed tree, both with time complexity . Their explanations are given in the corresponding sections; reference implementations are provided here.
Reference implementation
Merge sort
#include <iostream>
#include <vector>
// Merge sort and count inversions.
long long merge_sort(std::vector<int>& nums, int b, int e) {
// Return 0 when length <= 1.
if (e - b <= 1) return 0;
long long res = 0;
int m = (b + e) / 2;
// Merge_sort two halves.
res += merge_sort(nums, b, m);
res += merge_sort(nums, m, e);
// Temporary vector to store sorted array.
std::vector<int> tmp(e - b);
int i = b, j = m, k = 0;
while (i < m && j < e) {
if (nums[j] < nums[i]) {
tmp[k] = nums[j++];
// In this case, all elements in [i,m) are larger than element j.
res += m - i;
} else {
tmp[k] = nums[i++];
}
++k;
}
// Deal with remaining elements.
for (; i < m; ++i, ++k) {
tmp[k] = nums[i];
}
for (; j < e; ++j, ++k) {
tmp[k] = nums[j];
}
// Copy back to original vector.
for (i = b, k = 0; i < e; ++i, ++k) {
nums[i] = tmp[k];
}
return res;
}
int main() {
int n;
std::cin >> n;
std::vector<int> nums(n);
for (int& num : nums) {
std::cin >> num;
}
std::cout << merge_sort(nums, 0, n);
return 0;
}
Binary indexed tree
#include <algorithm>
#include <iostream>
#include <unordered_map>
#include <vector>
// A simple BIT implementation.
class BIT {
int n;
std::vector<int> su;
public:
BIT(int n) : n(n), su(n + 1) {}
// Add v to the x-th number.
void add(int x, int v) {
for (; x <= n; x += x & (-x)) {
su[x] += v;
}
}
// Get the cumulative sum till the x-th number.
int query(int x) {
int res = 0;
for (; x; x &= x - 1) {
res += su[x];
}
return res;
}
};
// Count inversions.
long long solve(const std::vector<int>& nums) {
// Discretization.
std::vector<int> sorted(nums);
std::sort(sorted.begin(), sorted.end());
sorted.erase(std::unique(sorted.begin(), sorted.end()), sorted.end());
std::unordered_map<int, int> ids;
int m = sorted.size();
for (int i = 0; i < m; ++i) {
// Reverse the order.
// Now a smaller id means a larger element.
ids[sorted[i]] = m - i;
}
// Main part.
BIT bit(m);
long long res = 0;
for (int num : nums) {
int id = ids[num];
// Get inversion pair (i,j) with j the current element.
// Namely, count the number of elements larger than
// the current one but located before it.
res += bit.query(id - 1);
// Insert the current element to the BIT.
bit.add(id, 1);
}
return res;
}
int main() {
int n;
std::cin >> n;
std::vector<int> nums(n);
for (int& num : nums) {
std::cin >> num;
}
std::cout << solve(nums);
return 0;
}
Order
Permutations can be compared by size. Since each one-line notation is a string, the order of permutations is the lexicographic order of these strings.
In the C++ STL library <algorithm>, prev_permutation and next_permutation can be used to find the previous and next permutations of the current one in lexicographic order.
Rank
List all permutations of elements in lexicographic order from smallest to largest. The position of a permutation in this sequence is its rank. This establishes a one-to-one correspondence between permutations and positive integers, and is often used for state compression in permutation problems.
In Chinese competitive programming, this rank is often called the “Cantor expansion” of a permutation, but that name is not standard. More precisely, the Cantor expansion of a permutation's rank corresponds to its Lehmer code.
About “Cantor expansion”
As the name suggests, a Cantor expansion is a method of expanding a natural number into a sequence. It can be viewed as a special numeral system, also called the factorial number system. In this system, the bases (radices) corresponding to different digits are not the same. For example, the decimal number can be represented in the factorial number system as
This representation means
Cantor studied numeral systems with such mixed bases, so the digit representation of a natural number in this system is also called its Cantor expansion.
Example
Readers unfamiliar with how to compute a rank can use this simple example to understand the basic idea of the algorithm below.
To compute the rank of the permutation , count how many permutations are lexicographically smaller than , then add one. The idea is similar to digit DP: consider the positions one by one.
- At position , the value must be smaller than that in , so it can only be one of . The remaining positions can be chosen arbitrarily, giving possible permutations;
- If position also contains , then at position the value must be smaller than that in , so it can only be one of (the has already been used). The remaining positions can be chosen arbitrarily, giving possible permutations;
- Similarly, if the first positions agree with , the value at position must be smaller than that in , so it can only be . The remaining positions can be chosen arbitrarily, giving possible permutations;
- If the first positions agree with , the value at position must be smaller than that in , so it can only be one of . The remaining positions can be chosen arbitrarily, giving possible permutations;
- If the first positions agree with , the value at position must be smaller than that in , so it can only be . The remaining position can be chosen arbitrarily, giving possible permutation;
- If the first positions agree with , no choice at position can produce a permutation smaller than , so there are possible permutations.
Therefore, the rank of permutation is
For different permutations, the key is determining the coefficients before the factorials. In fact, each coefficient is the number of elements that appear after the element at that position but are smaller than it.
From the example, the algorithm for finding the rank of a given permutation can be divided into two steps:
-
Convert the given permutation, of length , into its Lehmer code, a sequence of length called , where the -th entry is
that is, the number of elements after position in the permutation that are smaller than . This is equal to the rank minus one of the given element among the unused elements.
-
Treat the Lehmer code as the Cantor expansion of a natural number, recover the original natural number, and add one. In other words, the final rank is
To solve the inverse problem—given a rank, find the corresponding permutation—simply perform the above process in reverse. The sum of the digits in the Lehmer code obtained during this process is the inversion count of the permutation.
When implementing this, the key is to quickly compute the rank of a given element among the unused elements (when finding a rank) and the element with a given rank among the unused elements (when finding a permutation). Both can be maintained with data structures such as a binary indexed tree or a segment tree. Both forward and reverse operations take time.
Reference implementation
Find the rank of a given permutation
#include <iostream>
#include <vector>
// A simple BIT implementation.
class BIT {
int n;
std::vector<int> su;
public:
BIT(int n) : n(n), su(n + 1) {}
// Add v to the x-th number.
void add(int x, int v) {
for (; x <= n; x += x & (-x)) {
su[x] += v;
}
}
// Get the cumulative sum till the x-th number.
int query(int x) {
int res = 0;
for (; x; x &= x - 1) {
res += su[x];
}
return res;
}
};
// Get the rank of a permutation of 1~n.
long long find_rank(const std::vector<int>& nums) {
int n = nums.size();
BIT bit(n);
long long fac = 1;
long long res = 0;
// Reverse iteration.
for (int i = n - 1; i >= 0; --i) {
// Count the number of elements smaller than the current one.
res += bit.query(nums[i] - 1) * fac;
// Insert the current element into the BIT.
bit.add(nums[i], 1);
// Update the factorial.
fac *= n - i;
}
return res + 1;
}
int main() {
int n;
std::cin >> n;
std::vector<int> nums(n);
for (int& num : nums) {
std::cin >> num;
}
std::cout << find_rank(nums);
return 0;
}
Find the permutation with a given rank
#include <cmath>
#include <iostream>
#include <vector>
// A simple BIT implementation.
class BIT {
int n;
std::vector<int> su;
public:
BIT(int n) : n(n), su(n + 1) {}
// Fill the BIT with one.
void fill() {
for (int x = 1; x <= n; ++x) {
su[x] += x & (-x);
}
}
// Add v to the x-th number.
void add(int x, int v) {
for (; x <= n; x += x & (-x)) {
su[x] += v;
}
}
// Get the k-th smallest element.
int find_kth(int k) {
int ps = 0, x = 0;
for (int i = log2(n); i >= 0; --i) {
x += 1 << i;
if (x >= n || ps + su[x] >= k) {
x -= 1 << i;
} else {
ps += su[x];
}
}
return x + 1;
}
};
// Find the k-th permutation of 1~n.
std::vector<int> find_permutation(int n, long long k) {
--k;
// Expand rank to Lehmer code.
std::vector<int> lehmer(n);
for (int i = 1; i <= n; ++i) {
lehmer[n - i] = k % i;
k /= i;
}
BIT bit(n);
// Set all values in BIT to one.
bit.fill();
std::vector<int> res(n);
for (int i = 0; i < n; ++i) {
// Find the lehmer[i]-th smallest unused element.
res[i] = bit.find_kth(lehmer[i] + 1);
// Remove it from the BIT.
bit.add(res[i], -1);
}
return res;
}
int main() {
int n;
long long k;
std::cin >> n >> k;
auto res = find_permutation(n, k);
for (int num : res) {
std::cout << num << ' ';
}
return 0;
}