歸併演算法經典應用 求解逆序數

2021-10-02 19:14:52 字數 3258 閱讀 3509

在之前介紹線性代數行列式計算公式的時候,我們曾經介紹過逆序數:我們在列舉出行列式的每一項之後,需要通過逆序數來確定這一項符號的正負性。如果有忘記的同學可以回到之前的文章當中複習一下:

線性代數行列式

如果忘記呢,問題也不大,這個概念比較簡單,我想大家很快就能都搞清楚。

面試題當**現。

我們先來回顧一下逆序數的定義,所謂逆序數指的是陣列當中究竟存在多少組數對,使得排在前面的數大於排在後面的數。我們舉個例子,假設當下有乙個陣列是: [1, 3, 2]。

對於數對(3, 2)來說,由於3排在2前面,並且3 > 2,那麼就說明(3, 2)是一對逆序數對。整個陣列當中所有逆序數對的個數就是逆序數。

我們從逆序數的定義當中不難發現,逆序數其實是用來衡量乙個陣列有序程度的乙個指標。逆序數越大,說明這個陣列遞增性越差。如果逆序數為0,說明這個序列是嚴格遞增的。如果乙個長度為n的陣列的逆序數是\(c_n^2\),那麼說明這個陣列是嚴格遞減的,此時逆序數最大。

那麼,我們怎麼快速地求解逆序數呢?

顯然,這個問題可以暴力求解,我們只需要遍歷所有的數對,然後判斷是否構成逆序即可,最後累加一下所有逆序數對的個數就是最終的答案。

這個**非常簡單,只需要幾行:

inverse = 0

for i in range(1, 10):

for j in range(0, i):

if arr[j] > arr[i]:

inverse += 1

這樣當然是可以的,但是我們也很容易發現,這樣做的時間複雜度是\(o(n^2)\),這在很多時候是我們不能接受的。即使是執行速度非常快的c++,在單核cpu上一秒鐘的時間,也就能跑最多n=1000這個規模。再大需要消耗的時間就會陡然增加,而在實際應用當中,乙個長度超過1000的陣列簡直是家常便飯。顯然,我們需要優化這個演算法,不能簡單地暴力求解。

我們可以嘗試使用分治演算法來解決這個問題。

對於乙個陣列arr來說,我們試著將它拆分成兩半,比如當下arr是[32, 36, 3, 9, 29, 16, 35, 73, 34, 82]。我們拆分成兩半之後分別是[32, 36, 3, 9, 29]和[16, 35, 73, 34, 82]。我們令左邊半邊的子陣列是a,右邊半邊的子陣列是b。顯然a和b都是原問題的子問題,我們可以假設通過遞迴可以求解出a和b子問題的結果。

那麼問題來了,我們怎麼通過a、b子問題的結果來構建arr的結果呢?也就是說,我們怎麼通過子問題分治來獲取原問題的答案呢?

在回答之前,我們先來分析一下當前的情況。當我們將arr陣列拆分成了ab兩個部分之後,整個arr的逆序數就變成了三個部分。分別是a陣列之間的逆序數、b陣列之間的逆序數,以及ab兩個陣列之間的逆序數,也就是乙個元素在a中,乙個元素在b中的逆序數對。我們再來分析一下,會發現a陣列中的元素交換位置,只會影響a陣列之間的逆序數,並不會影響b以及ab之間構成的逆序數。因為a中的元素即使交換位置,也在b陣列所有元素之前。

我們舉個例子:

假設arr=[3, 5, 1, 4],那麼a=[3, 5], b=[1, 4]。對於arr而言,它的逆序數是3分別是(3, 1), (5, 1)和(5, 4)。對於a而言,它的逆序數是0,b的逆序數也是0。我們試著交換一下b當中的位置,交換之後的b=(4, 1),此時arr=[3, 5, 4, 1]。那麼b的逆序數變成1,a的逆序數依然還是0。而整體arr的逆序數變成了4,分別是:(3, 1),(5, 1),(5, 4)和(4,1),很明顯整體的逆序數新增的就是b交換元素帶來的。通過觀察,我們也能發現,對於a中的3和5而言,b中的1和4的順序並不影響它們構成逆序數的數量。

想明白了這一層,剩下的就簡單了。既然a和b當中的元素無論怎麼交換順序也不會影響對方的結果,那麼我們就可以放心地使用分治演算法來解決了。我們先假設,我們可以通過遞迴獲取a和b各自的逆序數。那麼剩下的問題就是找出所有a和b個佔乙個元素的逆序數對的情況了。

我們先對a和b中的元素進行排序,我們之前已經驗證過了,我們調整a或者b當中的元素順序,並不會改變橫跨ab逆序數的數量,而我們通過遞迴已經求到了a和b中各自逆序數對的數量,所以我們存下來之後,就可以對a和b中的元素進行排序了。a和b中元素有序了之後,我們可以用插入排序的方法,將a中的元素依次插入b當中。

b: ********* j ************

/ai

從上圖我們可以看出來,假設我們把\(a_i\)這個元素插入b陣列當中j的位置。由於之前\(a_i\)排在b這j個元素之前,所以構成了j個逆序數對。我們對於所有a中的元素\(a_i\)求出它在b陣列所有插入的位置j,然後對j求和即可。

比較容易想到,由於b元素有序,我們可以通過二分的方法來查詢a當中元素的位置。但其實還有更好的辦法,我們乙個步驟就可以完成ab的排序以及插入,就是將ab兩個有序的陣列進行歸併。在歸併的過程當中,我們既可以知道插入的b陣列中的位置,又可以完成陣列的排序,也就順帶解決了a和b排序的問題。所以整個步驟其實就是歸併排序的延伸,雖然整個**和歸併排序差別非常小,但是,這個過程當中的推導和思考非常重要。

如果你能理解上面這些推導過程,我相信**對你來說並不困難。如果你還沒能完全理解,也沒有關係,藉著**,我相信你會理解得更輕鬆。話不多說了,讓我們來看**吧:

def inverse_num(array):

n = len(array)

if n <= 1:

return 0, array

mid = n // 2

# 將陣列拆分為二往下遞迴

inverse_l, arr_l = inverse_num(array[:mid])

inverse_r, arr_r = inverse_num(array[mid:])

nl, nr = len(arr_l), len(arr_r)

# 插入最大的int作為標兵,可以簡化判斷超界的**

i, j = 0, 0

new_arr =

# 儲存array對應的逆序數

inverse = inverse_l + inverse_r

while i < nl or j < nr:

if arr_l[i] <= arr_r[j]:

# 插入a[i]的時候逆序數增加j

inverse += j

i += 1

else:

j += 1

return inverse, new_arr

從**層面來看,上面這段**實現了歸併排序的同時也算出了逆序數。所以這就是為什麼很多人會將兩者相提並論的原因,也是我個人非常喜歡這個問題的原因。看起來完全不相關的兩個問題,竟然能用幾乎同一套**來解決,不得不感嘆演算法的神奇。也正是因此,我們這些演算法的研究和學習者,才能獲取到源源不斷的樂趣。

分治遞迴逆序數 歸併演算法經典應用 求解逆序數

原創不易,求個關注 在之前介紹線性代數行列式計算公式的時候,我們曾經介紹過逆序數 我們在列舉出行列式的每一項之後,需要通過逆序數來確定這一項符號的正負性。如果有忘記的同學可以回到之前的文章當中複習一下 線性代數精華1 從行列式開始 如果忘記呢,問題也不大,這個概念比較簡單,我想大家很快就能都搞清楚。...

演算法複習 歸併排序求解逆序數問題 分治

題目描述 解析 本題使用歸併排序來求解逆序對數的問題,交換元素的次數正好就等於逆序數的個數。include include include include using std cin using std cout using std endl using std vector using std s...

分治法求解逆序數 歸併排序

題目內容 設a1,a2,an是集合的乙個排列,如果iaj,則序偶 ai,aj 稱為該排列的乙個逆序。例如,2,3,1有兩個逆序 3,1 和 2,1 設計演算法統計給定排列中含有逆序的個數。輸入格式 第一行輸入集合中元素個數n,第二行輸入n個集合元素 輸出格式 含有逆序的個數 輸入樣例 32 3 1 ...