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

2021-10-14 10:02:45 字數 2821 閱讀 7590

原創不易,求個關注

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

線性代數精華1——從行列式開始

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

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

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

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

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

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

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

這樣當然是可以的,但是我們也很容易發現,這樣做的時間複雜度是 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各自的逆序數。那麼剩下的問題就是將ab歸併之後的逆序數對,也就是找出所有a和b各佔乙個元素的逆序數對的情況了。

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

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

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

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

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

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

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

分治遞迴逆序數 逆序數的分治演算法

給我們乙個序列,讓我們求其逆序數 如3 2 1 4 逆序數為 2 1 0 0 3 我們這樣定義乙個序列的逆序數 序列a1 a2 a3 a2 an 這個序列的逆序數c,等於a1,a2.的逆序數的和.即 c sum ci ci為滿足ai aj j i 的數的總的個數,即ci sum ai aj j i ...

分治遞迴逆序數 模板 歸併排序 逆序數 分治

歸併排序 圖來自維基 遞迴呼叫的過程需要在腦中模擬清楚 然後是 的細節問題 多複習多理解 劉汝佳版 include using namespace std const int maxn 1e5 10 int ans 0 int arr maxn int brr maxn void mergesort...