Để mô tả một hoán vị của n phần tử, bạn thấy rằng đối với vị trí mà phần tử đầu tiên kết thúc, bạn có n khả năng, vì vậy bạn có thể mô tả điều này với một số từ 0 đến n-1. Đối với vị trí mà phần tử tiếp theo kết thúc, bạn có n-1 khả năng còn lại, vì vậy bạn có thể mô tả điều này bằng một số từ 0 đến n-2.
Vân vân cho đến khi bạn có n số.
Ví dụ cho n = 5, hãy xem xét hoán vị mang abcde
đến caebd
.
a
, phần tử đầu tiên, kết thúc ở vị trí thứ hai, vì vậy chúng tôi gán nó chỉ mục 1 .
b
kết thúc ở vị trí thứ tư, sẽ là chỉ số 3, nhưng nó là chỉ số thứ ba còn lại, vì vậy chúng tôi gán nó là 2 .
c
kết thúc ở vị trí còn lại đầu tiên, luôn là 0 .
d
kết thúc ở vị trí cuối cùng còn lại, mà (chỉ trong số hai vị trí còn lại) là 1 .
e
kết thúc ở vị trí duy nhất còn lại, được lập chỉ mục là 0 .
Vì vậy, chúng ta có chuỗi chỉ mục {1, 2, 0, 1, 0} .
Bây giờ bạn biết rằng ví dụ trong một số nhị phân, 'xyz' có nghĩa là z + 2y + 4x. Đối với một số thập phân,
đó là z + 10y + 100x. Mỗi chữ số được nhân với một số trọng lượng và kết quả được tính tổng. Mô hình rõ ràng trong trọng số tất nhiên là trọng số là w = b ^ k, với b là cơ số của số và k là chỉ số của chữ số. (Tôi sẽ luôn đếm các chữ số từ bên phải và bắt đầu từ chỉ số 0 cho chữ số tận cùng bên phải. Tương tự như vậy khi tôi nói về chữ số 'đầu tiên', tôi muốn nói ở ngoài cùng bên phải.)
Các lý do tại sao các trọng cho các số theo mô hình này là số lượng cao nhất có thể được đại diện bởi các chữ số từ 0 đến k phải chính xác 1 thấp hơn so với số lượng thấp nhất có thể được đại diện bởi chỉ sử dụng chữ số k + 1. Trong hệ nhị phân, 0111 phải thấp hơn một dưới 1000. Trong hệ thập phân, 099999 phải thấp hơn 100000.
Mã hóa thành cơ số biến
Khoảng cách giữa các số tiếp theo chính xác là 1 là quy tắc quan trọng. Nhận ra điều này, chúng ta có thể biểu diễn chuỗi chỉ mục của mình bằng một số cơ sở biến đổi . Cơ số cho mỗi chữ số là số khả năng khác nhau của chữ số đó. Đối với số thập phân, mỗi chữ số có 10 khả năng, đối với hệ thống của chúng tôi, chữ số tận cùng bên phải sẽ có 1 khả năng và ngoài cùng bên trái sẽ có n khả năng. Nhưng vì chữ số tận cùng bên phải (số cuối cùng trong dãy số của chúng ta) luôn là 0 nên chúng ta bỏ nó đi. Điều đó có nghĩa là chúng ta còn lại với các cơ số 2 đến n. Nói chung, chữ số thứ k sẽ có cơ số b [k] = k + 2. Giá trị cao nhất cho phép của chữ số k là h [k] = b [k] - 1 = k + 1.
Quy tắc của chúng tôi về trọng số w [k] của các chữ số yêu cầu rằng tổng của h [i] * w [i], trong đó tôi đi từ i = 0 đến i = k, bằng 1 * w [k + 1]. Được phát biểu một cách lặp lại, w [k + 1] = w [k] + h [k] * w [k] = w [k] * (h [k] + 1). Trọng số đầu tiên w [0] phải luôn là 1. Bắt đầu từ đó, chúng ta có các giá trị sau:
k h[k] w[k]
0 1 1
1 2 2
2 3 6
3 4 24
... ... ...
n-1 n n!
(Quan hệ tổng quát w [k-1] = k! Dễ dàng được chứng minh bằng quy nạp.)
Số chúng ta nhận được từ việc chuyển đổi chuỗi của chúng ta sau đó sẽ là tổng của s [k] * w [k], với k chạy từ 0 đến n-1. Ở đây s [k] là phần tử thứ k (ngoài cùng bên phải, bắt đầu từ 0) của dãy. Ví dụ: lấy {1, 2, 0, 1, 0} của chúng tôi, với phần tử ngoài cùng bên phải bị loại bỏ như đã đề cập trước đây: {1, 2, 0, 1} . Tổng của chúng ta là 1 * 1 + 0 * 2 + 2 * 6 + 1 * 24 = 37 .
Lưu ý rằng nếu chúng tôi chiếm vị trí tối đa cho mọi chỉ mục, chúng tôi sẽ có {4, 3, 2, 1, 0} và chuyển đổi thành 119. Vì trọng số trong mã hóa số của chúng tôi đã được chọn nên chúng tôi không bỏ qua bất kỳ số nào, tất cả các số từ 0 đến 119 đều hợp lệ. Có chính xác 120 trong số này, là n! với n = 5 trong ví dụ của chúng ta, chính xác là số các hoán vị khác nhau. Vì vậy, bạn có thể thấy các số được mã hóa của chúng tôi hoàn toàn chỉ định tất cả các hoán vị có thể có.
Giải mã từ cơ sở biến
Giải mã tương tự như chuyển đổi sang nhị phân hoặc thập phân. Thuật toán phổ biến là:
int number = 42;
int base = 2;
int[] bits = new int[n];
for (int k = 0; k < bits.Length; k++)
{
bits[k] = number % base;
number = number / base;
}
Đối với số cơ sở biến đổi của chúng tôi:
int n = 5;
int number = 37;
int[] sequence = new int[n - 1];
int base = 2;
for (int k = 0; k < sequence.Length; k++)
{
sequence[k] = number % base;
number = number / base;
base++; // b[k+1] = b[k] + 1
}
Điều này giải mã chính xác 37 của chúng tôi trở lại {1, 2, 0, 1} ( sequence
sẽ là {1, 0, 2, 1}
trong ví dụ mã này, nhưng bất cứ điều gì ... miễn là bạn lập chỉ mục phù hợp). Chúng ta chỉ cần thêm 0 vào cuối bên phải (hãy nhớ rằng phần tử cuối cùng luôn chỉ có một khả năng cho vị trí mới của nó) để lấy lại dãy ban đầu {1, 2, 0, 1, 0}.
Hoán vị danh sách bằng chuỗi chỉ mục
Bạn có thể sử dụng thuật toán dưới đây để hoán vị danh sách theo một chuỗi chỉ mục cụ thể. Thật không may, đó là một thuật toán O (n²).
int n = 5;
int[] sequence = new int[] { 1, 2, 0, 1, 0 };
char[] list = new char[] { 'a', 'b', 'c', 'd', 'e' };
char[] permuted = new char[n];
bool[] set = new bool[n];
for (int i = 0; i < n; i++)
{
int s = sequence[i];
int remainingPosition = 0;
int index;
// Find the s'th position in the permuted list that has not been set yet.
for (index = 0; index < n; index++)
{
if (!set[index])
{
if (remainingPosition == s)
break;
remainingPosition++;
}
}
permuted[index] = list[i];
set[index] = true;
}
Biểu diễn phổ biến của hoán vị
Thông thường bạn sẽ không biểu diễn hoán vị một cách không trực quan như chúng ta đã làm, mà chỉ đơn giản bằng vị trí tuyệt đối của mỗi phần tử sau khi hoán vị được áp dụng. Ví dụ của chúng tôi {1, 2, 0, 1, 0} cho abcde
to caebd
thường được biểu thị bằng {1, 3, 0, 4, 2}. Mỗi chỉ mục từ 0 đến 4 (hoặc nói chung, 0 đến n-1) xảy ra đúng một lần trong biểu diễn này.
Áp dụng một hoán vị ở dạng này rất dễ dàng:
int[] permutation = new int[] { 1, 3, 0, 4, 2 };
char[] list = new char[] { 'a', 'b', 'c', 'd', 'e' };
char[] permuted = new char[n];
for (int i = 0; i < n; i++)
{
permuted[permutation[i]] = list[i];
}
Đảo ngược nó rất giống nhau:
for (int i = 0; i < n; i++)
{
list[i] = permuted[permutation[i]];
}
Chuyển đổi từ biểu diễn của chúng ta sang biểu diễn thông thường
Lưu ý rằng nếu chúng ta sử dụng thuật toán để hoán vị danh sách bằng cách sử dụng chuỗi chỉ mục và áp dụng nó cho hoán vị danh tính {0, 1, 2, ..., n-1}, chúng ta nhận được hoán vị nghịch đảo , biểu diễn ở dạng chung. ( {2, 0, 4, 1, 3} trong ví dụ của chúng tôi).
Để có được tiền đặt trước không đảo ngược, chúng tôi áp dụng thuật toán hoán vị mà tôi vừa trình bày:
int[] identity = new int[] { 0, 1, 2, 3, 4 };
int[] inverted = { 2, 0, 4, 1, 3 };
int[] normal = new int[n];
for (int i = 0; i < n; i++)
{
normal[identity[i]] = list[i];
}
Hoặc bạn có thể chỉ áp dụng hoán vị trực tiếp bằng cách sử dụng thuật toán hoán vị nghịch đảo:
char[] list = new char[] { 'a', 'b', 'c', 'd', 'e' };
char[] permuted = new char[n];
int[] inverted = { 2, 0, 4, 1, 3 };
for (int i = 0; i < n; i++)
{
permuted[i] = list[inverted[i]];
}
Lưu ý rằng tất cả các thuật toán để xử lý các hoán vị ở dạng phổ biến là O (n), trong khi áp dụng một hoán vị ở dạng của chúng ta là O (n²). Nếu bạn cần áp dụng một hoán vị nhiều lần, trước tiên hãy chuyển nó thành biểu diễn chung.