排列組合演算法1:產生全部有序列

來源:互聯網
上載者:User

產生長度為N的全部有序列(n-tuples) 

在QQ群上和朋友聊天(嗯,我還在用QQ,盡情鄙視我吧。什麼時候MSN支援像QQ那樣任意添加表情,任意貼圖,而不是把我添加的表情圖壓縮得面目全非,我再放棄QQ不遲。連“徹底地全身心地毫無保留地崇拜你”都不能用,MSN my ass。QQ上的表情:同樣的圖添加到MSN後: 。什麼世道!),常遇到的話題之一是怎麼產生一個有序列的所有組合,一個集合的所有子集(冪集),或者所有的全排列。一些論壇上也常出現類似的問題。很有意思的話題。在編程中時不時要遇到之外,也是鍛煉大腦防止老年癡呆的上佳練習材料,尤其適合好靜坐,喜油條, 30歲以上從不上健身房的程式員。不用左右看了。就是老大您!做這類題目還有個好處:重溫當年寫小程式的快樂。不知道多少人會享受搭建工資管理系統的全過程,津津有味地調試奇形怪狀的API,反覆修改龐雜的XML設定檔。反正我不會。寫小程式就不同了。沒有期限的壓力,不用擔心系統的羈絆,無需顧慮程式的架構。可以糾纏演算法的每一個細節,也可以執著於提高代碼的每一分效能。施主隨喜。心智澄明,目光通透,心隨意動,運指如飛。敲下的字元直切問題要害,摧枯拉朽。層層屏障隨著代碼的延伸支離破碎。寫小程式解謎題的過程,就好像懵懂小孩兒紮堆遊戲,純粹為了好玩兒。一晃眼,一下午過去。他們滿身泥漿,精疲力盡。但他們眼神依然興奮,依然盼望下一次遊戲。嗯,今年不收禮,收禮只收智力題。 今天又開始犯賤,萬事壓身就是不想動手。不過秉承拖拉也要拖有所得的原則,乾脆聊聊這類問題的常用演算法。知道怎麼產生全部有序列的演算法,也就知道怎麼產生冪集。這兩者有直觀的對應關係。產生全排列則另有一套演算法。產生所有有序列要簡單些,所以先用它開胃。當然,這類問題早有大牛寫了詳盡的指南, 我也就噹噹廉價的搬運工而已。好在還能用大牛本人的話安慰一下自己:每當發現什麼有趣的問題後,輕輕Google一把,總能不幸發現有聰明人已經做出答案。一個N-tuple是一個包含N個元素的序列,一般寫作 。比如說 就是一個6-tuple。注意tuple和集合的區別。Tuple裡可以包含重複的元素。而且Tuple的元素是有序的,就跟數組的元素一樣。那麼給定一個N-tuple, ,我們怎麼產生全部有序列呢?由簡入繁總是學習的不二法門。所以我們從簡單的二進位N元有序列(binary n-tuple)開始:二進位N元有序列的每個元素都是一個bit,非0即1。比如說一個二進位3-tuple,產生的全部有序列為(0, 0, 0)(0, 0, 1)(0, 1, 0)(0, 1, 1)(1, 0, 0)(1, 0, 1)(1, 1, 1)一共8個。二進位N元有序列雖然簡單,卻已經有了廣泛的用途。比如說知道怎麼產生二進位N元有序列,我們也就知道怎麼產生冪集:一個N元集合 ,我們規定 屬于于一個子集,若且唯若對應的二進位N元有序列的元素 。用上面的3-tuple作例子。給出一個集合 ,則下面還會討論更多的應用。而且從討論二進位N元有序列中得出的結論為我們以後探討怎麼產生更複雜的組合樣式奠定基礎。產生全部二進位N元有序列的方法再簡單不過:我們從位元 開始,逐次加一,直到得到 為止。利用進位加法,我們剛好遍曆了所有的N-tuples。簡單,但是美妙。代碼用Ruby實現的。因為Ruby的代碼和虛擬碼差別不大,會不會Ruby的老大都可以看懂。再說演算法就是演算法。無所謂那門語言。學演算法時非找到《XXX語言演算法》,純屬無聊。 外層迴圈負責一個一個地加1,而內層迴圈負責進位。如果所有位都是1了,則跳出所有迴圈,因為加無可加。
def binary_all_tuples(tuple_length)
  tuple =
 Array.new(tuple_length, 0)
    
  loop do
    puts tuple.inspect 

    j = tuple_length - 1

    while tuple[j] == 1 do
      tuple[j] =
 0
      j -= 1

    end    

    
return if j == -1
    tuple[j] += 1
  end
end
 
我們知道迴圈一共進行了 次。所以我們不用每次判斷j的值。不過這是細節,無關主旨。
def binary_all_tuples_2(tuple_length)
  tuple = Array.new(tuple_length, 0)
 1.upto(1<<tuple_length) do |i|
    puts tuple.inspect   
    j = tuple_length -
1
    while tuple[j] == 1 do
      tuple[j] =
0
      j -=
1
    end   
    tuple[j] += 1    
  end
end 
有了這個演算法,要產生一個集合的所有子集也就容易了:
def each_tuple(tuple_length=0, &proc)
  tuple = Array.new(tuple_length, 0)  
  loop do
    yield tuple   
    j = tuple_length - 1
   while tuple[j] == 1
do
      tuple[j] =
0
      j -=
1
    end   
    return if j == -1
    tuple[j] += 1
end
end
require ’set’
def powerset(
set = [])
 each_tuple(
set.size) do |tuple|
    puts “tuple: #{tuple.inspect}”   
    subset = []
   tuple.each_index do |index|
      subset << set[index] if tuple[index] ==
1
    end   
    puts subset.inspect
  end
end
第一個函數each_tuple()和最前面的binary_all_tuples()基本一樣。唯一的區別是each_tuple()沒有列印出得到的每一個tuple,而是yield每一個得到的tuple,讓附著的過程&proc來處理。第二個函數powerset()只需要找出每個tuple裡為一的元素對應的座標index,然後把對應的set[index]收集起來。這麼簡單的演算法也有精彩的引申。比如說,我們可以把二進位擴充到10進位。十進位的N-tuple: 裡, 。所以我們只需要把 逐步遞加到 。我們還能進一步總結,處理多進位序列。也就是說,n元序列裡的任意元素 的進位是 ,用公式表達為:對不同的 , 不一定相同。本質上,產生全序列的方法並沒有改變。我們現在只需要對混合進位的數執行累加。這個混合進位的數可以寫為:累加的演算法仍然直觀:對滿足條件(1)的混合進位序列,我們不斷在公式(2)裡的數上累加1。進位規則與以前統一進位的進位規則沒有本質區別。在第 位時,加到 再進位就行了。
def all_tuples(radix_list = [])
  tuple_length = radix_list.size
  tuple = Array.new(tuple_length, 0)   
  loop do
    puts tuple.inspect   
    j = tuple_length - 1
    while tuple[j] == radix_list[j] -1
do
      tuple[j] =
0
      j -=
1
    end



    return if j == -1
    tuple[j] += 1
  end
end
比較詭異的是,高老太爺建議當內嵌的迴圈數目小時,不如直接手寫成N層嵌套迴圈,反而簡單。其實也有道理。手寫嵌套迴圈,無非敲的字多了點,但省去了構思和排錯的時間,代碼也比較直觀。當初我們幾個朋友一起做作業系統的作業。當我還在那裡構思怎麼把複雜的迴圈寫得簡單通用的時候,同組的一個快槍手早已調試完畢。他就是先手寫嵌套迴圈和判斷,事後再來重構。上述的演算法按字典順序(或者算術順序)產生所有的序列。有時候,我們需要按其它順序產生序列。最有名的就是所謂的格雷碼了,也就是產生的序列中,任何相鄰的兩個序列只相差一位。比如說,2進位三元序列的格雷碼產生順序是:(0, 0, 0)(0, 0, 1)(0, 1, 1)(0, 1, 0)(1, 1, 0)(1, 1, 1)(1, 0, 1)(1, 0, 0)很明顯,我們可以對用一序列產生不同的格雷碼。下面是另外一組格雷碼:(0, 0, 0)(1, 0, 0)(1, 0, 1)(0, 0, 1)(0, 1, 1)(0, 1, 0)(1, 1, 0)(1, 1, 1)格雷碼應用廣泛。從類比訊號到遺傳演算法到離散數學到九連環的解法,通通有它的影子。有興趣的可以去google或者百度一下。討論到後面我們可以看到九連環的解法和格雷碼的關係,以及從解法中推匯出高效的演算法。有很多等價的格雷碼定義。我們從簡單的開始,看一個比較直觀的:公式(3)裡的 表示空字串, 表示把序列 裡每一個字串前加一個0,組成新的序列,而 則表示把序列 翻轉,然後把裡面每一字串前置一個1,形成新的序列。看看前N = 3的情況:n = 0 ,我們得到空序列 n = 1, ,所以我們得到唯一的格雷碼:0, 1n = 2, = 0(0, 1), 1(1, 0) = 00, 01, 11, 10n = 3, = 0(00, 01, 11), 1(11, 01, 00) = 000, 001, 011, 111, 101, 000  直觀上也好理解,所有序列都是從無到有搭建起來的。所以 和 裡所有字串都是符合格雷碼定義――相鄰字串相差一位。而 的最後一個字串剛好等於 的第一個字串,所以分別加上0和1後, 的最後一個字串和 剛好相差一位。有了直觀的概念,歸納法證明近乎瑣碎(本來公式就是用來表達我們的直觀思想的)。前面的例子可算初始條件,假設 是格雷碼,下面的步驟不用寫出來了吧? 因為公式(3)是遞迴定義,我們能直接把它轉換成代碼:
def gray_rec(n)
  return [] if n ==
0
  return [“0″,“1″] if n ==
1
  partial = gray_rec(n-1)     
  return partial.collect{ |e| “0″+e} + partial.reverse.collect{|e|“1″+e}
end
加上 return [“0”, “1”] if n == 1純粹是為了省下後面對空集的判斷。用慣LISP系列的老大們多半不能沒有經過tail-recursion最佳化的代碼,所以我們小小改動一下。Ruby其實沒有tail-recursion最佳化,所以下面的代碼應該沒有什麼實質性的改進。
def gray_rec_helper(count, max, partial_result)
  return [] if max ==
0
  return partial_result if count == max

  return gray_rec_helper(count+1,
        max,
partial_result.collect{ |e| “0″+e}+ partial_result.reverse.collect{ |e| “1″+
e})
end

def tail_gray(n)
  return gray_rec_helper(1, n, [“0″,“1″]
)
end
這樣的產生方式並不經濟。我們後面會通過玩兒九連環推匯出更快的演算法。再逐步改進,得到不需要內迴圈的高速演算法。  

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.