程序員編程藝術第四十一章~四十二章:荷蘭國旗、矩陣相乘Strassen算法

       第四十一章~四十二章:荷蘭國旗問題、矩陣相乘之Strassen算法


前言

    本文要講的兩個問題:荷蘭國旗和矩陣相乘之Strassen算法都跟分治法相關,故把這兩個問題放到了一起。所謂分治,便是分而治之的意思,好比打戰時面對敵人龐大的武裝部隊,採取避其主力,各個擊破的策略。

    有何問題,歡迎隨時不吝指正,thanks。



第十一章、荷蘭國旗問題

題目描述

現有紅白藍三個不同顏色的小球,亂序排列在一起,請重新排列這些小球,使得紅白藍三色的同顏色的球在一起。這個問題之所以叫荷蘭國旗,是因爲我們可以將紅白藍三色小球想象成條狀物,有序排列後正好組成荷蘭國旗。如下圖所示:

    


思路分析

    初看此題,我們貌似除了暴力解決並無好的辦法,但聯想到我們所熟知的快速排序算法呢?我們知道,快速排序時基於分治模式處理的,對一個典型子數組A[p...r]排序的分治過程爲三個步驟:

  1. 分解:A[p..r]被劃分爲倆個(可能空)的子數組A[p ..q-1]和A[q+1 ..r],使得A[p ..q-1] <= A[q] <= A[q+1 ..r]
  2. .解決:通過遞歸調用快速排序,對子數組A[p ..q-1]和A[q+1 ..r]排序。
  3. 合併。

    也就是說,快速排序的主要思想便是依託於一個partition分治過程,每一趟排序的過程中,選取的主元都會把整個數組排列成一大一小的序列,繼而遞歸排序完整個數組。

    如下僞代碼所示:

快速排序算法的關鍵是PARTITION過程,它對A[p..r]進行就地重排:
PARTITION(A, p, r)
1  x ← A[r]
2  i ← p - 1
3  for j ← p to r - 1
4       do if A[j] ≤ x
5             then i ← i + 1
6                  exchange A[i] <-> A[j]
7  exchange A[i + 1] <-> A[r]
8  return i + 1

繼而遞歸完成整個排序過程:

QUICKSORT(A, p, r)
1 if p < r
2    then q ← PARTITION(A, p, r)   //關鍵
3         QUICKSORT(A, p, q - 1)
4         QUICKSORT(A, q + 1, r)

舉個例子如下:i 指向數組頭部前一個位置,j 指向數組頭部元素,j 在前,i 在後,雙雙從左向右移動。

① j 指向元素2時,i 也指向元素2,2與2互換不變
     i p/j
  2   8   7   1   3   5   6   4(主元)


② 於是j 繼續後移,直到指向了1,1 <= 4,於是i++,i 指向8,故j 所指元素1 與 i 所指元素8 位置互換:
            i         j
  2   1   7   8   3   5   6   4


③ j 繼續後移,指到了元素3,3 <= 4,於是同樣i++,i 指向7,故j 所指元素3 與 i 所指元素7 位置互換:
               i         j
  2   1   3   8   7   5   6   4


④ j 一路後移,沒有再碰到比主元4小的元素:
  i                   j
  2   1   3   8   7   5   6   4


⑤ 最後,A[i + 1] <-> A[r],即8與4交換,所以,數組最終變成了如下形式:
        2   1   3   4   7   5   6   8

ok,至此快速排序第一趟完成。就這樣,4把整個數組分成了倆部分,2 1 3,7 5 6 8,再遞歸對這倆部分分別進行排序。

全部過程可以參看此文:快速排序算法,或看下我以前在學校裏畫的圖:

    而我們面對的問題是,重新排列使得所有球排列成三個不同顏色的球,是否可以設定三個指針,借鑑partition過程呢?

解法一、partition分治

    通過前面的分析得知,這個問題,類似快排中partition過程。只是需要用到三個指針,一前begin,一中current,一後end,倆倆交換。

  1. current遍歷,整個數組序列,current指1不動,
  2. current指0,與begin交換,而後current++,begin++,
  3. current指2,與end交換,而後,current不動,end--。

    爲什麼,第三步,current指2,與end交換之後,current不動了列,對的,正如algorithm__所說:current之所以與begin交換後,current++、begin++,是因爲此無後顧之憂。而current與end交換後,current不動,end--,是因有後顧之憂。

    讀者可以試想,你最終的目的無非就是爲了讓0、1、2有序排列,試想,如果第三步,current與end交換之前,萬一end之前指的是0,而current交換之後,current此刻指的是0了,此時,current能動麼?不能動啊,指的是0,還得與begin交換列。

    ok,說這麼多,你可能不甚明瞭,直接引用下gnuhpc的圖,就一目瞭然了:

    

    

    參考代碼如下:

//引用自gnuhpc
while( current<=end )      
{           
  if( array[current] ==0 )           
   {               
      swap(array[current],array[begin]);                
      current++;                
      begin++;          
   }           
   else if( array[current] == 1 )          
   {               
      current++;          
   } 
          
   else //When array[current] =2 
   {             
      swap(array[current],array[end]);              
      end--;          
   }    
}

    本章完。



第四十二章:矩陣相乘之Strassen算法

題目描述

    請編程實現矩陣乘法,並考慮當矩陣規模較大時的優化方法。

思路分析

    根據wikipedia上的介紹:兩個矩陣的乘法僅當第一個矩陣B的列數和另一個矩陣A的行數相等時才能定義。如A是m×n矩陣和B是n×p矩陣,它們的乘積AB是一個m×p矩陣,它的一個元素其中 1 ≤ i ≤ m, 1 ≤ j ≤ p。

    

    值得一提的是,矩陣乘法滿足結合律和分配率,但並不滿足交換律,如下圖所示的這個例子,兩個矩陣交換相乘後,結果變了:

     下面咱們來具體解決這個矩陣相乘的問題。

解法一、暴力解法

    其實,通過前面的分析,我們已經很明顯的看出,兩個具有相同維數的矩陣相乘,其複雜度爲O(n^3),參考代碼如下:

//矩陣乘法,3個for循環搞定  
void Mul(int** matrixA, int** matrixB, int** matrixC)  
{  
	for(int i = 0; i < 2; ++i)   
	{  
		for(int j = 0; j < 2; ++j)   
		{  
			matrixC[i][j] = 0;  
			for(int k = 0; k < 2; ++k)   
			{  
				matrixC[i][j] += matrixA[i][k] * matrixB[k][j];  
			}  
		}  
	}  
}

解法二、Strassen算法

    在解法一中,我們用了3個for循環搞定矩陣乘法,但當兩個矩陣的維度變得很大時,O(n^3)的時間複雜度將會變得很大,於是,我們需要找到一種更優的解法。

    一般說來,當數據量一大時,我們往往會把大的數據分割成小的數據,各個分別處理。遵此思路,如果丟給我們一個很大的兩個矩陣呢,是否可以考慮分治的方法循序漸進處理各個小矩陣的相乘,因爲我們知道一個矩陣是可以分成更多小的矩陣的。

    如下圖,當給定一個兩個二維矩陣A B時:

    這兩個矩陣A B相乘時,我們發現在相乘的過程中,有8次乘法運算,4次加法運算:

    矩陣乘法的複雜度主要就是體現在相乘上,而多一兩次的加法並不會讓複雜度上升太多。故此,我們思考,是否可以讓矩陣乘法的運算過程中乘法的運算次數減少,從而達到降低矩陣乘法的複雜度呢?答案是肯定的。

    1969年,德國的一位數學家Strassen證明O(N^3)的解法並不是矩陣乘法的最優算法,他做了一系列工作使得最終的時間複雜度降低到了O(n^2.80)。

    他是怎麼做到的呢?還是用上文A B兩個矩陣相乘的例子,他定義了7個變量:

    如此,Strassen算法的流程如下:

  • 兩個矩陣A B相乘時,將A, B, C分成相等大小的方塊矩陣:

  • 可以看出C是這麼得來的:


  • 現在定義7個新矩陣(讀者可以思考下,這7個新矩陣是如何想到的):

  • 而最後的結果矩陣C 可以通過組合上述7個新矩陣得到:

    表面上看,Strassen算法僅僅比通用矩陣相乘算法好一點,因爲通用矩陣相乘算法時間複雜度是,而Strassen算法複雜度只是。但隨着n的變大,比如當n >> 100時,Strassen算法是比通用矩陣相乘算法變得更有效率。

    如下圖所示:

解法三、持續優化

    根據wikipedia上的介紹,後來,Coppersmith–Winograd 算法把 N* N大小的矩陣乘法的時間複雜度降低到了:,而2010年,Andrew Stothers再度把複雜度降低到了,一年後的2011年,Virginia Williams把複雜度最終定格爲:



參考文獻

  1. 快速排序算法:http://blog.csdn.net/v_july_v/article/details/6116297
  2. 快速排序算法的深入分析:http://blog.csdn.net/v_july_v/article/details/6211155
  3. gnuhpc:http://blog.csdn.net/gnuhpc/article/details/6207285
  4. wikipedia上關於Strassen算法的介紹:http://zh.wikipedia.org/wiki/%E6%96%BD%E7%89%B9%E6%8B%89%E6%A3%AE%E6%BC%94%E7%AE%97%E6%B3%95
  5. 第42章部分圖來自此文“ Computer Algorithms: Strassen's Matrix Multiplication” :http://www.stoimen.com/blog/2012/11/26/computer-algorithms-strassens-matrix-multiplication/
  6. 上文的翻譯版,來自圖靈社區:http://www.ituring.com.cn/article/17978
  7. Coppersmith–Winograd 算法: http://en.wikipedia.org/wiki/Coppersmith%E2%80%93Winograd_algorithm


後記

    編程藝術原計劃寫到第五十章,如今只剩下最後八章,感謝各位一直以來的關注。預祝本博客所有的讀者新春快樂,在馬年一切都能心想事成,thanks。

    July、二零一四年一月二十八日。

發佈了166 篇原創文章 · 獲贊 1萬+ · 訪問量 1604萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章