我在黃昏時坐在地球上
我這樣說并不表明晚上
我就不在地球上
——海子 《明天醒來我會在哪一只鞋子里》
思路1:加大搜索空間,使用評估函數(shù)找到解
“如果在可能的地方找不到,就去不可能的地方找。”這是一句廢話,也是一句挺有道理的話,甚至有時候,它還是一句有用的話。因為生活中有時真的會有神奇的事情發(fā)生。元宵節(jié)的前一天晚上,我和一位同事加班到10點半,正準備回家的時候,突然發(fā)現(xiàn)大事不妙——辦公室唯一的一把鑰匙找不到了——沒有鑰匙就沒辦法鎖門,我們回不去家了。辦公室是客戶借給我們的,備份鑰匙只有客戶有,但是這么晚了實在不好意思打電話求救。鑰匙是我?guī)讉€小時前放在同事的桌子上的,卻不知被同事隨手扔到哪里去了。桌子上,抽屜里,所有的包包和衣服兜全找過了,就是不見鑰匙的蹤影。現(xiàn)在我們有3種選擇:1)放棄。在辦公室里睡到天亮或者不鎖門就回家(但是那一屋子的電腦還有服務(wù)器實在讓人放心不下);2)再找一遍。不過已經(jīng)找了2遍了,再原樣重復(fù)一遍結(jié)果也不會有什么改變。3)去不可能的地方找。我決定碰碰運氣。結(jié)果,5分鐘之后,我居然在桌子上一個裝著湯圓的袋子里找到了那把已經(jīng)凍得冰涼的鑰匙。
如果沒辦法直接取得沙子里的金子,就把沙子和金子一同撈起來,再用水慢慢淘去沙子,留下砂金。如果沒有辦法直接求解,可以先找到一個雖然大一些但是比較容易取得的包含所有解的搜索空間,再使用評估函數(shù)判斷搜索空間里每一個潛在解的質(zhì)量。
現(xiàn)在我們想求得["a","b","c"]的全排列,也就是想要得到解(為了看著舒服省略了引號):
[[a,b,c],
[a,c,b],
[b,a,c],
[b,c,a],
[c,a,b],
[c,b,a]]
數(shù)組的下標的排列是:
0 1 2
0 2 1
1 0 2
1 2 0
2 0 1
2 1 0
有沒有什么方法可以由“0 1 2”經(jīng)過某種計算得到下一個下標的排列“0 2 1”呢?如果把 012 看作是一個3進制的數(shù)字,那么把 012 加 1 就得到了 021。但是,把 021 加 1 得到的是 022 而 不是 102。所以,用這種方法比較容易得到可重復(fù)的排列,從000開始,每次增加1可以得到:
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
其中綠顏色表示的是我們想要的結(jié)果,我們只要把它們挑出來就好。
如果把 currentIndexList 看作是一個 currentIndexList.Count 進制的數(shù)字,GetNextIndexList() 函數(shù)的作用是把這個數(shù)字增加1:
接著,我們只要調(diào)用 GetNextIndexList() 函數(shù) nn 次(n=source.Count),就可以遍歷整個搜索空間,然后再判斷是否所有下標都不重復(fù)就可以知道是不是想要的解了:
測試一下:
注:Montage()是一個方便把列表拼接成字符串的擴展方法。
這種方法的缺點是效率比較差。要生成n個元素的全排列需要遍歷 nn 次才能得到 n! 個解(看上面那個綠色和黑色的下標的比例也可以有一個直觀的感覺)。
由于這種方法可以根據(jù)當(dāng)前下標計算出下一個下標,所以可以使用 yield return “按需生成”所需要的全排列,如果只需要得到前面幾個而不是所有的全排列,會很劃算。
思路2:一致,一致,一致!
第一種方法雖然簡單,但是效率太差。能不能每次不是加1,而是加n,一下子就得到下一個排列呢?你可以親自嘗試一下,要想知道每次應(yīng)該加多少真的很難。我們只好換個思路,能否通過交換數(shù)組元素得到下一個排列呢?譬如有數(shù)組 s = ["A", "B", "C", "D"],只要交換 s[2] 和 s[3] 就可得到下一個排列 A B D C。但是要得到下一個排列 A C B D 需要先交換 s[1] 和 s[3],再交換 s[2] 和 s[3]……有時只需交換一次,有時需要交換兩次,有時甚至需要交換3次。看圖找規(guī)律神馬的最討厭了。但是如果不能找出一個一致的處理方法,就沒法編寫程序。
從哪里著手呢?當(dāng)問題難以直接求解時,如果可以先得到一個間接結(jié)果,一個可跟蹤的線索,問題往往會變得簡單很多。
所以我們可以先試試能否找出一個一致的方法來確定第一次要交換的是哪兩個元素。我們先使用常規(guī)辦法:觀察一個樣本(例如ABCD的所有排列),看看是否能找到什么規(guī)律。但是這一次恐怕要失望了:第一次要交換的兩個元素有可能是相鄰的,也有可能不相鄰;有可能是最大(或最小)的元素,也可能不是;有可能是第一個(或最后一個)元素,也有可能不是……除了前一個元素一定比后一個小之外,似乎難以發(fā)現(xiàn)其他規(guī)律。
好像要卡住了。我們換個思路——反著想:“什么情況下無法通過交換兩個元素來得到下一個排列?”為什么這個問題有意義呢?讓我們回想一下目標:得到下一個(字典序的)排列。“下一個(字典序的)排列”的定義是:只比前一個排列大一點點的那個排列。第一個排列(例如 A B C D)是最小的那個排列,它一定是最小的元素在最高位、最大的元素在最低位。當(dāng)我們想得到 A B C D 的下一個排列時,一定是優(yōu)先交換它的最低位的那個元素(也就是 C 和 D),這樣才能保證得到的是僅比當(dāng)前排列大一點點的那個排列。只有無法交換最低2位來得到下一個排列時,才會考慮交換更高位的元素(例如想得到 A B D C 的下一個排列時,就不能交換 D 和 C)。
那么,什么情況下無法通過交換兩個元素來得到下一個排列呢?答案是,當(dāng)子數(shù)組已經(jīng)是最后一個排列時。例如,當(dāng)前排列是 A B D C 時,因為 “D C” 這個排列是子數(shù)組 [C,D] 的最后一個排列,所以交換 D C 的任意兩個元素都只會得到比 D C 這個排列更小的排列。同理,當(dāng)前排列是 A D C B 時,由于 “D C B” 已經(jīng)是子數(shù)組 [B,C,D] 的最后一個排列,所以無法通過交換 D C B 里的任意兩個元素的方法得到下一個排列,只能交換 A 和 B。這樣我們終于可以確定第一次要交換的兩個元素了:(從低位到高位找到的那個)已經(jīng)是最后一個排列的子數(shù)組 subs 前面的那個元素 a 和 subs里只比 a 大一點點的那個元素。舉幾個例子(下劃線標出的是 subs,綠顏色標出的是第一次要交換的兩個元素):
A B C D
A B D C
A C B D
A C D B
A D B C
A D C B
...
B D C A
subs 的特征很明顯,它一定是倒序排列的。
接下來想一想在做了第一次交換之后,還要做什么才能得到想要的下一個排列。例如想要得到 A D C B 的下一個排列 B A C D。首先,交換 A 和 B,得到 B D C A。這樣保證了最高位只比當(dāng)前排列大了一點點。然后再把 D C A 變成最小的排列,也就是要把 D C A 反轉(zhuǎn)得到 A C D。這樣得到的 B A C D 一定是只比 A D C B 大一點點的那個排列。
下面是源代碼:
測試一下:
我這樣說并不表明晚上
我就不在地球上
——海子 《明天醒來我會在哪一只鞋子里》
思路1:加大搜索空間,使用評估函數(shù)找到解
“如果在可能的地方找不到,就去不可能的地方找。”這是一句廢話,也是一句挺有道理的話,甚至有時候,它還是一句有用的話。因為生活中有時真的會有神奇的事情發(fā)生。元宵節(jié)的前一天晚上,我和一位同事加班到10點半,正準備回家的時候,突然發(fā)現(xiàn)大事不妙——辦公室唯一的一把鑰匙找不到了——沒有鑰匙就沒辦法鎖門,我們回不去家了。辦公室是客戶借給我們的,備份鑰匙只有客戶有,但是這么晚了實在不好意思打電話求救。鑰匙是我?guī)讉€小時前放在同事的桌子上的,卻不知被同事隨手扔到哪里去了。桌子上,抽屜里,所有的包包和衣服兜全找過了,就是不見鑰匙的蹤影。現(xiàn)在我們有3種選擇:1)放棄。在辦公室里睡到天亮或者不鎖門就回家(但是那一屋子的電腦還有服務(wù)器實在讓人放心不下);2)再找一遍。不過已經(jīng)找了2遍了,再原樣重復(fù)一遍結(jié)果也不會有什么改變。3)去不可能的地方找。我決定碰碰運氣。結(jié)果,5分鐘之后,我居然在桌子上一個裝著湯圓的袋子里找到了那把已經(jīng)凍得冰涼的鑰匙。
如果沒辦法直接取得沙子里的金子,就把沙子和金子一同撈起來,再用水慢慢淘去沙子,留下砂金。如果沒有辦法直接求解,可以先找到一個雖然大一些但是比較容易取得的包含所有解的搜索空間,再使用評估函數(shù)判斷搜索空間里每一個潛在解的質(zhì)量。
現(xiàn)在我們想求得["a","b","c"]的全排列,也就是想要得到解(為了看著舒服省略了引號):
[[a,b,c],
[a,c,b],
[b,a,c],
[b,c,a],
[c,a,b],
[c,b,a]]
數(shù)組的下標的排列是:
0 1 2
0 2 1
1 0 2
1 2 0
2 0 1
2 1 0
有沒有什么方法可以由“0 1 2”經(jīng)過某種計算得到下一個下標的排列“0 2 1”呢?如果把 012 看作是一個3進制的數(shù)字,那么把 012 加 1 就得到了 021。但是,把 021 加 1 得到的是 022 而 不是 102。所以,用這種方法比較容易得到可重復(fù)的排列,從000開始,每次增加1可以得到:
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
其中綠顏色表示的是我們想要的結(jié)果,我們只要把它們挑出來就好。
如果把 currentIndexList 看作是一個 currentIndexList.Count 進制的數(shù)字,GetNextIndexList() 函數(shù)的作用是把這個數(shù)字增加1:
// 獲取 currentIndexList 的下一個排列。例如 currentIndexList = [0,1,1],調(diào)用此函數(shù)之后,
// currentIndexList 將變?yōu)?[0,1,2];[0,2,2] 變?yōu)?[1,0,0]
static void GetNextIndexList(IList<int> currentIndexList)
{
int count = currentIndexList.Count;
for (int i = count-1; i >= 0; i--)
{
if (currentIndexList[i] < count - 1) // 不需要進位
{
currentIndexList[i]++; // 當(dāng)前位加1
return;
}
else
{
currentIndexList[i] = 0; // 進位前把當(dāng)前位置0
}
}
}
接著,我們只要調(diào)用 GetNextIndexList() 函數(shù) nn 次(n=source.Count),就可以遍歷整個搜索空間,然后再判斷是否所有下標都不重復(fù)就可以知道是不是想要的解了:
// 生成 source 的全排列。
static IEnumerable<IList<string>> Permutation1(IList<string> source)
{
// 初始化 indexList,使它具有 source.Count 個值為0的元素。
int count = source.Count;
IList<int> indexList = new List<int>();
for (int i = 0; i < count; i++)
{
indexList.Add(0);
}
for (int i = 0; i < Math.Pow(count, count); i++)
{
GetNextIndexList(indexList);
if (indexList.Distinct().Count() == count) // 不重復(fù)
{
IList<string> result = new List<string>();
foreach (int j in indexList)
{
result.Add(source[j]);
}
yield return result;
}
}
}
測試一下:
IList<string> s = new List<string> { "a", "b", "c" };
foreach (IList<string> item in Permutation1(s))
{
Console.WriteLine(item.Montage(t => t, " "));
}
注:Montage()是一個方便把列表拼接成字符串的擴展方法。
這種方法的缺點是效率比較差。要生成n個元素的全排列需要遍歷 nn 次才能得到 n! 個解(看上面那個綠色和黑色的下標的比例也可以有一個直觀的感覺)。
由于這種方法可以根據(jù)當(dāng)前下標計算出下一個下標,所以可以使用 yield return “按需生成”所需要的全排列,如果只需要得到前面幾個而不是所有的全排列,會很劃算。
思路2:一致,一致,一致!
第一種方法雖然簡單,但是效率太差。能不能每次不是加1,而是加n,一下子就得到下一個排列呢?你可以親自嘗試一下,要想知道每次應(yīng)該加多少真的很難。我們只好換個思路,能否通過交換數(shù)組元素得到下一個排列呢?譬如有數(shù)組 s = ["A", "B", "C", "D"],只要交換 s[2] 和 s[3] 就可得到下一個排列 A B D C。但是要得到下一個排列 A C B D 需要先交換 s[1] 和 s[3],再交換 s[2] 和 s[3]……有時只需交換一次,有時需要交換兩次,有時甚至需要交換3次。看圖找規(guī)律神馬的最討厭了。但是如果不能找出一個一致的處理方法,就沒法編寫程序。
從哪里著手呢?當(dāng)問題難以直接求解時,如果可以先得到一個間接結(jié)果,一個可跟蹤的線索,問題往往會變得簡單很多。
所以我們可以先試試能否找出一個一致的方法來確定第一次要交換的是哪兩個元素。我們先使用常規(guī)辦法:觀察一個樣本(例如ABCD的所有排列),看看是否能找到什么規(guī)律。但是這一次恐怕要失望了:第一次要交換的兩個元素有可能是相鄰的,也有可能不相鄰;有可能是最大(或最小)的元素,也可能不是;有可能是第一個(或最后一個)元素,也有可能不是……除了前一個元素一定比后一個小之外,似乎難以發(fā)現(xiàn)其他規(guī)律。
好像要卡住了。我們換個思路——反著想:“什么情況下無法通過交換兩個元素來得到下一個排列?”為什么這個問題有意義呢?讓我們回想一下目標:得到下一個(字典序的)排列。“下一個(字典序的)排列”的定義是:只比前一個排列大一點點的那個排列。第一個排列(例如 A B C D)是最小的那個排列,它一定是最小的元素在最高位、最大的元素在最低位。當(dāng)我們想得到 A B C D 的下一個排列時,一定是優(yōu)先交換它的最低位的那個元素(也就是 C 和 D),這樣才能保證得到的是僅比當(dāng)前排列大一點點的那個排列。只有無法交換最低2位來得到下一個排列時,才會考慮交換更高位的元素(例如想得到 A B D C 的下一個排列時,就不能交換 D 和 C)。
那么,什么情況下無法通過交換兩個元素來得到下一個排列呢?答案是,當(dāng)子數(shù)組已經(jīng)是最后一個排列時。例如,當(dāng)前排列是 A B D C 時,因為 “D C” 這個排列是子數(shù)組 [C,D] 的最后一個排列,所以交換 D C 的任意兩個元素都只會得到比 D C 這個排列更小的排列。同理,當(dāng)前排列是 A D C B 時,由于 “D C B” 已經(jīng)是子數(shù)組 [B,C,D] 的最后一個排列,所以無法通過交換 D C B 里的任意兩個元素的方法得到下一個排列,只能交換 A 和 B。這樣我們終于可以確定第一次要交換的兩個元素了:(從低位到高位找到的那個)已經(jīng)是最后一個排列的子數(shù)組 subs 前面的那個元素 a 和 subs里只比 a 大一點點的那個元素。舉幾個例子(下劃線標出的是 subs,綠顏色標出的是第一次要交換的兩個元素):
A B C D
A B D C
A C B D
A C D B
A D B C
A D C B
...
B D C A
subs 的特征很明顯,它一定是倒序排列的。
接下來想一想在做了第一次交換之后,還要做什么才能得到想要的下一個排列。例如想要得到 A D C B 的下一個排列 B A C D。首先,交換 A 和 B,得到 B D C A。這樣保證了最高位只比當(dāng)前排列大了一點點。然后再把 D C A 變成最小的排列,也就是要把 D C A 反轉(zhuǎn)得到 A C D。這樣得到的 B A C D 一定是只比 A D C B 大一點點的那個排列。
下面是源代碼:
/// <summary>
/// 返回source的全排列
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static IEnumerable<string[]> Permutation2(string[] source)
{
if (source.Length <= 0)
yield break;
string[] s = new string[source.Length];
Array.Copy(source, s, source.Length);
do
{
yield return s;
}
while (NextPermutation(s) == true);
}
// 將 s 變換為下一個字典序排列。當(dāng)前排列已經(jīng)是最后一個排列時,返回false
private static bool NextPermutation(string[] s)
{
int highIndex = 0;
int lowIndex = s.Length - 1;
int subsHighIndex = FindSubsHighIndex(s);
if (subsHighIndex == highIndex) // s 已經(jīng)是最后的排列
return false;
Swap(s, subsHighIndex - 1, LastIndex(s, t => s[subsHighIndex - 1].CompareTo(t) <= 0));
Reverse(s, subsHighIndex, lowIndex);
return true;
}
// 確定subs的高位index。subs是s最低n位的子數(shù)組,它已經(jīng)是此子數(shù)組的最后一個排列
private static int FindSubsHighIndex(string[] s)
{
int highIndex = 0;
int lowIndex = s.Length - 1;
int result = lowIndex;
for ( ; result > highIndex; result--)
{
if (s[result].CompareTo(s[result - 1]) > 0)
return result;
}
return result;
}
// 交換 s[i] 和 s[j]
private static void Swap(string[] s, int i, int j)
{
string temp = s[i];
s[i] = s[j];
s[j] = temp;
}
// 反轉(zhuǎn) s[startIndex]..s[endIndex]
private static void Reverse(string[] s, int startIndex, int endIndex)
{
while (startIndex < endIndex)
{
Swap(s, startIndex, endIndex);
startIndex++;
endIndex--;
}
}
// 從后向前搜索s,返回第一個滿足predicate()==true的元素下標;找不到時返回-1
private static int LastIndex(string[] s, Func<string, bool> predicate)
{
for (int i = s.Length - 1; i >= 0; i--)
{
if (predicate(s[i]) == true)
return i;
}
return -1;
}
測試一下:
string[] s = new string[] { "A", "B", "C", "D" };
foreach (string[] p in Permutation2(s))
{
Console.WriteLine(p.Montage(t => t, " "));
}

浙公網(wǎng)安備 33010602011771號