1. 程式人生 > >歸併演算法經典應用——求解逆序數

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

本文始發於個人公眾號:TechFlow,原創不易,求個關注


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

線性代數行列式

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

今天的這一篇文章,我想和大家聊聊逆序數的演算法,也是一道非常經典的演算法題,經常在各大公司的面試題當中出現。

我們先來回顧一下逆序數的定義,所謂逆序數指的是陣列當中究竟存在多少組數對,使得排在前面的數大於排在後面的數。我們舉個例子,假設當下有一個數組是: [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: XXXXXXXXX j XXXXXXXXXXXX
            /
          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作為標兵,可以簡化判斷超界的程式碼
    arr_l.append(sys.maxsize)
    arr_r.append(sys.maxsize)

    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
            new_arr.append(arr_l[i])
            i += 1
        else:
            new_arr.append(arr_r[j])
            j += 1
    return inverse, new_arr

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

今天的文章就到這裡,如果覺得有所收穫,請順手掃碼點個關注吧,你們的支援是我最大的動力。

相關推薦

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

本文始發於個人公眾號:TechFlow,原創不易,求個關注 在之前介紹線性代數行列式計算公式的時候,我們曾經介紹過逆序數:我們在列舉出行列式的每一項之後,需要通過逆序數來確定這一項符號的正負性。如果有忘記的同學可以回到之前的文章當中複習一下: 線性代數行列式 如果忘記呢,問題也不大,這個概念比較簡單,我想大

白話經典算法系列之九 從歸併排序到數列的序數對(微軟筆試題)

首先來看看原題   微軟2010年筆試題 在一個排列中,如果一對數的前後位置與大小順序相反,即前面的數大於後面的數,那麼它們就稱為一個逆序數對。一個排列中逆序的總數就稱為這個排列的逆序數。如{2,4,3,1}中,2和1,4和3,4和1,3和1是逆序數對,因此整個陣列的逆序數對個數為

分治法之歸併排序2-求解序數

public static void main(String[] args) {// TODO Auto-generated method stub// int aa[]={4,2,6,7,9,10,5,4,8,1}; int aa[]={4,2,6,7,9,1}; p

HDU 6318 Swaps and Inversions 思路很巧妙!!!(轉換為樹狀數組或者歸並求解序數

eve because owb else 離散化 try 自己 title esp Swaps and Inversions Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Ja

貪心演算法經典應用

1.選擇不相交區間問題 【問題】 給定n個開區間(ai,bi),選擇儘量多個區間,使得這些區間輛兩沒有公共點。 【解題思路】 首先,按照結束時間b1<=b2<=..<=bn的順序排序,依次考慮各個活動,如果沒有和已經選擇的活動衝突,就選;否則就不選。

|NOIOJ|二分歸併|7622:求排列的序數

描述 在Internet上的搜尋引擎經常需要對資訊進行比較,比如可以通過某個人對一些事物的排名來估計他(或她)對各種不同資訊的興趣,從而實現個性化的服務。 對於不同的排名結果可以用逆序來評價它們之間的差異。考慮1,2,…,n的排列i1,i2,…,in,如果其中存在

歸併排序求序數(排序演算法

歸併排序:歸併排序是建立在歸併操作上的一種有效的排序演算法,該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為

排序演算法歸併排序及利用歸併排序求序數

排序演算法之歸併排序 1. 自頂向下的歸併排序 中心思想 將待排序的陣列平均切分為兩半,將前半部分和後半部分分別進行排序,再講兩個有序陣列歸併到一個數組中 特點 遞迴,需要額外的空間(輔助陣列)來儲存切分完成的子陣列,主要難點在於合併

歸併排序(包含序數對的個數51Nod1019)

歸併排序是效率很好的排序方式,和快排效率一樣高,但在穩定性上優於快排,下面我們來介紹歸併排序。 歸併排序運用遞迴將序列不斷二分(其原理就是分治),就像一棵樹不斷向下分支,最後分到只剩一個元素,這樣這個元素就可當做有序的,因為只有一個元素嘛。然後是合併,怎麼分出來就怎麼合併回去,不過既然是排序,

POJ 3067 Japan(歸併序數

題目大意就是有東西兩片區域,每片區域從上到下分別有m和n個城市,編號為1,2,3…… 有k條高速公路連線著兩片區域的城市,比如輸入1 2就是東邊的一號城市連線著西邊的2號城市。然後要我們求的是這些公路的交叉點有多少個。 一開始我看就1000個城市嘛,暴力應該ok啊,然後寫了一發暴力,果斷

應用Python來計算排列中的序數個數

在一個排列中,如果一對數的前後位置與大小順序相反,即前面的數大於後面的數,那麼它們就稱為一個逆序。一個排列中逆序的總數就稱為這個排列的逆序數。一個排列中所有逆序總數叫做這個排列的逆序數。也就是說,對於n個不同的元素,先規定各元素之間有一個標準次序(例如n個 不同的自然數,可規定從小到大為標準次序),於是在這n

zcmu:1205: 正序數+1203: 序數(歸併排序)

 這兩道題目可以說是一毛一樣的,就放在一起說一下。用到了一個歸併排序的內容,我搜了幾篇部落格學習了下歸併排序,寫得都很好,基本認真看兩遍一定能看明白了 ***************歸併排序詳述***************   1203: 逆序數

貪心演算法幾個經典應用(待補全)

目錄 選擇不相交區間問題 區間選點問題 【問題描述】給定n個閉區間 [ai,bi],在數軸上選取儘量少的點,使得每個區間內都至少有一個點(不同區間內含的點可以是同一個) 【思路點撥】首先按區間的結束位置升序排列,在依次對每個區間進行選擇;對於當前區間,若集合中的數

【ZCMU1203】序數歸併

題目連結 1203: 逆序數 Time Limit: 1 Sec  Memory Limit: 128 MB Submit: 596  Solved: 130 [Submit][Status][Web Board] Description 在一個排列中,如果一對數的前

【資料結構排序】POJ1804——歸併排序求序數

問題描述: 給定一個數組,問最少經過多少次交換,才可以使得它有序 求解方法: 實際上就是求該陣列的逆序數,使用歸併排序即可 AC程式碼如下: #include<cstdio> #in

Intersections -序數-歸併排序

題意:1-n的兩個排列分別在兩行上,相同的數連線求線的交點數 思路:雜湊思想桶標記每個數在第一行的位置輸入第二行時把每個位置上的數改為在第一行的位置 #include<bits/stdc++.h> using namespace std; #d

資料結構實驗之排序五:歸併序數(SDUT 3402)

歸併排序詳解(戳我)。 以下是搬了別人的。 #include<stdio.h> #include<stdlib.h> long long sum = 0; int a[100005]; int temp[100005]; void Merge(int s1

51Nod-1019 序數序偶+歸併排序】

1019 逆序數  在一個排列中,如果一對數的前後位置與大小順序相反,即前面的數大於後面的數,那麼它們就稱為一個逆序。一個排列中逆序的總數就稱為這個排列的逆序數。 如2 4 3 1中,2 1,4 3,4 1,3 1是逆序,逆序數是4。給出一個整數序列,求該序列的逆序數。

幾種經典搜尋演算法以及應用

目錄 二、 窮舉 分支定界: A* 一、 評估你的複雜度 簡單的判斷演算法是否能滿足執行時間限制的要求 密切關注題中的給出的資料規模,選擇相應的演算法 一起試一下這個題目吧! 樣例: 輸入: 3

序數問題 使用歸併排序

在一個排列中,如果一對數的前後位置與大小順序相反,即前面的數大於後面的數,那麼它們就稱為一個逆序。一個排列中逆序的總數就稱為這個排列的逆序數。 比如3 4 1 2這個陣列有4對逆序:分別是[3,1] [