0 0 0
0 0 1
0 0 2
0 1 0
0 1 1
0 1 2
0 2 0
0 2 1
0 2 2
1 0 0
1 0 1
1 0 2
1 1 0
1 1 1
1 1 2
1 2 0
1 2 1
1 2 2
2 0 0
2 0 1
2 0 2
2 1 0
2 1 1
2 1 2
2 2 0
2 2 1
2 2 2
這種方法雖然簡單,但是效率比較差。要生成n個元素的全排列需要遍歷 nn 次才能得到 n! 個解(看上面那個綠色和黑色的下標的比例也可以有一個直觀的感覺)。能不能直接把當前排列增加 i 得到下一個排列呢?例如,把 012 增加 2 得到 021 (按 3 進制計算),把 021 增加 4 得到 102……把 201 增加 2 得到 210?可以看到,有時候是增加 2,有時候是增加 4,很難摸清其中的規律(如果想求 [1,2,3,4] 的全排列,會發現有時候需要增加 3,有時候需要增加 6,有時候需要增加 9,甚至有時候需要增加 21)。這里的難點是,把數字增加 i 之后,可能會發生一連串的進位,而你很難知道這一連串的進位之后,哪兩個位上的數字會重復。當問題的變量很多,而這些變量又相互影響時,問題就會變得復雜而難以解決。要想簡化問題,就必須找到一個一致的方法表達這些相互影響的變量對結果的影響。把多個維度疊加到一個維度之上,是簡化問題的常用手段。譬如,能否找到一個一致的方法把全排列映射為一個每次增加1的序列呢? 也就是:
[0,1,2] => 0
[0,2,1] => 1
[1,0,2] => 2
[1,2,0] => 3
[2,0,1] => 4
[2,1,0] => 5
這樣給出[2,0,1]就可以知道它是第5個排列了;反過來,根據這種獨特的映射方法,也有可能知道第5個排列是[2,0,1]。這個映射可以使用康托展開來實現。
康托展開
康托展開的公式是 X=an*(n-1)!+an-1*(n-2)!+...+ai*(i-1)!+...+a2*1!+a1*0! 其中,ai為當前未出現的元素中是排在第幾個(從0開始)。
這個公式可能看著讓人頭大,最好舉個例子來說明一下。例如,有一個數組 s = ["A", "B", "C", "D"],它的一個排列 s1 = ["D", "B", "A", "C"],現在要把 s1 映射成 X。n 指的是數組的長度,也就是4,所以
X(s1) = a4*3! + a3*2! + a2*1! + a1*0!
關鍵問題是 a4、a3、a2 和 a1 等于啥?
a4 = "D" 這個元素在子數組 ["D", "B", "A", "C"] 中是第幾大的元素。"A"是第0大的元素,"B"是第1大的元素,"C" 是第2大的元素,"D"是第3大的元素,所以 a4 = 3。
a3 = "B" 這個元素在子數組 ["B", "A", "C"] 中是第幾大的元素。"A"是第0大的元素,"B"是第1大的元素,"C" 是第2大的元素,所以 a3 = 1。
a2 = "A" 這個元素在子數組 ["A", "C"] 中是第幾大的元素。"A"是第0大的元素,"C"是第1大的元素,所以 a2 = 0。
a1 = "C" 這個元素在子數組 ["C"] 中是第幾大的元素。"C" 是第0大的元素,所以 a1 = 0。(因為子數組只有1個元素,所以a1總是為0)
所以,X(s1) = 3*3! + 1*2! + 0*1! + 0*0! = 20
康托展開的C#代碼如下:
// 返回s的康托展開值
static long X(string[] s)
{
long result = 0;
int len = s.Length;
for (int i = 0; i < len; i++)
{
result += An(s, i) * Factorial(len - i - 1);
}
return result;
}
// 返回 s[n] 是 s[n..length] 中第幾大的元素
static int An(string[] s, int n)
{
int result = 0;
for (int i = n + 1; i < s.Length; i++)
{
if (s[n].CompareTo(s[i]) >= 0)
result++;
}
return result;
}
// 返回 input 的階乘
static int Factorial(int input)
{
int result = 1;
for (int i = 2; i <= input; i++)
{
result = result * i;
}
return result;
}
測試一下:
string[] s = new string[] { "A", "B", "C" };
foreach (string[] p in Permutation2(s))
{
Console.WriteLine(p.Montage(t => t, " ") + " | " + X(p).ToString());
}
Permutation2() 和 Montage() 函數請參考上一篇。測試輸出如下:
A B C | 0
A C B | 1
B A C | 2
B C A | 3
C A B | 4
C B A | 5
通過康托逆展開生成全排列
如果已知 s = ["A", "B", "C", "D"],X(s1) = 20,能否推出 s1 = ["D", "B", "A", "C"] 呢?
因為已知 X(s1) = a4*3! + a3*2! + a2*1! + a1*0! = 20,所以問題變成由 20 能否唯一地映射出一組 a4、a3、a2、a1?如果不考慮 ai 的取值范圍,有
3*3! + 1*2! + 0*1! + 0*0! = 20
2*3! + 4*2! + 0*1! + 0*0! = 20
1*3! + 7*2! + 0*1! + 0*0! = 20
0*3! + 10*2! + 0*1! + 0*0! = 20
0*3! + 0*2! + 20*1! + 0*0! = 20
等等。但是滿足 0 <= ai <= n-1 的只有第一組。可以使用輾轉相除的方法得到 ai,如下圖所示:
知道了a4、a3、a2、a1的值,就可以知道s1[0] 是子數組["A", "B", "C", "D"]中第3大的元素 "D",s1[1] 是子數組 ["A", "B", "C"] 中第1大的元素"B",s1[2] 是子數組 ["A", "C"] 中第0大的元素"A",s[3] 是子數組 ["C"] 中第0大的元素"C",所以s1 = ["D", "B", "A", "C"]。
這樣我們就能寫出一個函數 Permutation3(),它可以返回 s 的第 m 個排列。
// 返回 s 的第 m 個排列(m 從 0 開始)。要求 s 是升序排列的
static string[] Permutation3(string[] s, int m)
{
IList<string> sub = new List<string>(s);
IList<string> result = new List<string>();
for (int i = s.Length - 1; i >= 0; i--)
{
int f = Factorial(i);
int ai = m / f;
string item = sub[ai]; // 由于數組是升序排列的,所以第 ai 大的元素就是 sub[ai]
result.Add(item);
sub.RemoveAt(ai);
m = m % f;
}
return result.ToArray();
}
測試一下:
string[] s = new string[] { "A", "B", "C", "D" };
for (int i = 0; i < Factorial(s.Length); i++)
{
string[] s1 = Permutation3(s, i);
Console.WriteLine(s1.Montage(t => t, " "));
}
附錄 康托展開是怎么來的?
很顯然,康托展開是本文的關鍵所在。你說康托他老人家當初是怎么想出來這種展開的方法的呢?我們還是以 s=["A", "B", "C"] 為例:
A B C | 0
A C B | 1
B A C | 2
B C A | 3
C A B | 4
C B A | 5
他的思路可能是這樣的:首先,確定一個目標:將每個排列映射為一個自然數,這個自然數是順序增長的(或者至少要有一定的規律)。要說映射成自然數,第一個想到的方法自然是把數組的下標當作一個n進制的數字,但是正如本文開篇所討論的,這個數字并沒有什么規律;第二個方法是計數,也就是令 X = 當前排列之前有多少個排列。例如 A B C 是第一個排列,它前面沒有任何排列,所以 X(ABC) = 0;A C B 前面有一個排列,所以 X(ACB) = 1……那么如何才能知道 X(BCA) = 3 也就是 B C A 的前面有3個排列呢?這里的技巧仍然是分解——把問題分隔成相互獨立的有限的小塊。具體的方法是:先求出 B 第一次出現在最高位(也就是 B A C 這個排列)時前面有幾個排列,再求出 B C A 是 B A C 后面第幾個排列,把這個兩個數相加就是想要的結果了。
先看第一個問題:B 第一次出現在最高位(也就是 B A C 這個排列)時前面有幾個排列?由于已知 B A C 前面的排列一定是 A 開頭的,所以只有 A 后面的兩個元素可以變化,所以排列數是 P(2,2) = 2! 個。
第二個問題:B C A 是 B A C 后面第幾個排列?因為都是 B 開頭的,所以可以把開頭的 B 忽略,問題變成 C A 是 A C 后面的第幾個排列?同樣,可以先考慮 C 第一次出現在最高位時前面有幾個排列,因為 C A 前面的排列肯定是 A 開頭的,所以只有 A 后面的一個元素可以變化,所以排列數是 P(1,1) = 1! 個。
所以 X(BCA) = 2! + 1! = 3
再例如想求 X(CBA),同樣是先考慮 C 第一次出現在最高位時前面有多少個排列,因為比 C 小的元素有 A 和 B 兩個,所以是 2*2! 個。再求出 B A 是 A B 后面的第 1! 個排列。就可以知道 X(CBA) = 2*2! + 1! = 5 了。
感謝徐少俠的幫助!

浙公網安備 33010602011771號