• 沒有找到結果。

for Sprout 2014 by Yuan

N/A
N/A
Protected

Academic year: 2022

Share "for Sprout 2014 by Yuan"

Copied!
71
0
0

加載中.... (立即查看全文)

全文

(1)

Algorithm Design Methods

Divide & Conquer

for Sprout 2014 by Yuan

(2)

從一個例子開始…

從一個例子開始…

• 占三格的 L 形方塊

• 是否可以不重疊的放入 8x8 ,且缺了一格的棋盤中呢?

我們來試試看…

很「貪心」的盡量 把每個方格塞滿

糟糕了…好像行不通

怎麼辦?難道要回 去窮舉每一種可能 嗎 ?

(3)

別緊張 !!

別緊張 !!

• 遇到看似不可解的問題怎麼辦?

• 大問題不會解,小問題總會解了吧?

• 不妨從較小規模的合理問題開始思考 !

• 圖一是否可解呢?看起來易如反掌

• 放大一倍試試看!

• 圖二是否可解呢?看起來沒那麼難……

• 要怎麼從小問題的解法獲得一些線索呢 ?

圖一

圖二

(4)

切割! 切割!

• 這樣一來,就很像解四次圖一的 問題了!

• 可是有個小小的插曲:其中的三 格並沒有缺格

• 我們正好可以放上一塊方塊完美 的解決這個問題!

• 產生四個規模較小的子問題 ~

(5)

遞迴過程 遞迴過程

X 3

X4 X4

問題夠小,我們會解了

邊長 =2 為終止條件

(6)

什麼是「分治」?

什麼是「分治」?

• 切割問題,然後征服問題!

• 把問題切割成相似的子問題

• 利用相似的方法解決它

• 剛剛的例子,由「分治法」構造出一組正確的方案

• 遞迴是方便實做分治想法的好工具

• 遞迴的本質是用堆疊保存每一層函數之區域變數的狀態

(7)

且慢,貪心不好嗎?

且慢,貪心不好嗎?

• 貪心很棒啊,總是拿目前最有利的解

• 剛剛的問題,不知道該怎麼貪心

• 也有些問題,太貪心,得不償失

• HW5, Problem 1

• 總是以面額較高之貨幣付款,能讓使用的貨幣數量最小化嗎?

• 如果硬幣的面額是 {1,2,4,8}, 想要湊出面額 15, 怎麼做?

• 如果硬幣的面額是 {1,500,501}, 想要湊出面額 1000, 怎麼做

(8)

那,窮舉總行了吧 那,窮舉總行了吧

• 太過曠日費時,天荒地老

• 盲目的窮舉並沒有好好的利用問題的性質

• 剛剛的問題,如果對於每一個方格,窮舉四個方向

• 總複雜度需要 O(4^N) !!

• 其中 , N 是擺放的 L 型數量

(9)

• 直接解決大問題很難…

• 要是問題滿足以下條件:

• 規模小的時候 , 輕鬆簡單 , 易如反掌

• 規模大的時候 , 既不能貪心 , 窮舉又不切實際……

幸好,我們可以把大問題切割成小問題

• 而且,小問題的答案有助於我們探尋大問題的答案!

• 就可以考慮使用分治的概念解題

(10)

分工合作的想法 分工合作的想法

• 把大問題變成一些相似的小問題

• 該怎麼變 , 術語叫”切割問題”

• 不一定會有多個子問題,可能一個就足夠

• 有些不可能對答案造成影響的子問題,可以直接忽略

• 把小問題算出答案 ( 怎麼算的不重要 , 算得出來就好 )

• 就是”遞迴求解”囉

• 只要問題的切割有遇到終止條件的一天 , 一定算得出來 !

• 把小問題的答案變成大問題的答案

• 術語叫”合併問題”

• 善加利用我們已經得到的特性

(11)

複習一下二分搜尋法 複習一下二分搜尋法

• 在 S 中搜尋某數

• 不可能對答案造成影響的子問題,可以直接忽略

• 於是拋棄不用求解的子問題

• 分割問題為兩個規模接近的子問題,子問題的規模是原問題的一半

• 只有分,不太需要治

• 因為最後都只有一個可能的分支

• 子問題的解直接就會是答案

a M1 b M c M2 d

S

a M1 b M c M2 d

S

(12)

適用分治的時機 適用分治的時機

• 天生就適合分治的問題

• 問題本身就長得很遞迴 ( 原問題可以切割成許多規模較小的問題 )

• 資料結構由遞迴定義而得

• 用分治的想法可以讓程式很清楚!我是 Heap!

我也是 Heap!

我是二元搜尋樹 ! 我的子孫也是 !

(13)

遞迴定義 遞迴定義

f()

=

=

(14)

適用分治的時機 適用分治的時機

• 看起來貌似沒有分治結構的問題,如剛剛的 L 型方塊

• 但分治可以幫助我們求解

• 再舉一個例子

• 輸入 n ,請構造出一組 1~n 的排列,滿足任意選擇其中三個數,

按照原本的順序排列,均不會形成等差數列。

• n=8

• 3 4 2 1 7 8 5 6

(15)

• 想要直接構造長度為 n 的解答,似乎很困難?

• 不妨考慮分治:如果我們在已有長度為 n/2 的解答 X 的情況下,

是否有辦法構造出一組長度為 n 的解答 X’ 呢?

• 既然解答 X 合法,表示從 X 中任意取三個數,必定不會形成等差 數列

• 如果對於 X 中的每個元素加減一個數,是否還保持此性質?

• 如果對於 X 中的每個元素乘上一個數,是否還保持此性質?

(16)

• N=3 的解: 3 1 2

• N=6 的解:要如何構造,使得 1~6 都會用到呢?

• 使用 N=3 的解做一些變化,但避免變化前與變化後形成等差數列

• [3 1 2] 每個數值都加上 3 ,得到 [6 4 5]

• [3 1 2] 與 [6 4 5] 組合,這樣可以用到每個數

• [3 1 2 6 4 5]

• 似乎並未有效的防止等差數列產生

• 換個想法,利用乘法得到較大的數

(17)

於是乎 於是乎

• [3 1 2] 每個數值都乘以 2 ,得到 [6 2 4]

• 剩下 1, 3, 5 不妨把 [6 2 4] 各減 1 ,得到 [5 1 3] 這樣可以用到每個數

• 該怎麼決定他們的位置呢?

• 如果把奇數都擺在偶數的前面,無論是 [ 奇奇偶 ] 或 [ 奇偶偶 ] 都不可能 形成等差數列!當然,因為遞迴的性質, [ 奇奇奇 ] 與 [ 偶偶偶 ] 也不可能 我們成功找到一種由 N=3 的解 構造出 N=6 的解 的方法了!

• 有了這個思路後,剩下的想法就容易許多,不妨自己試試看,如果 n 是奇數的話,也可以這樣做嗎?

(18)

那那… 那那…

• 以上都是如果不把問題規模縮小,我們難以構造的問題

• 對於原本我們就會解答的問題

• 分治法有沒有任何幫助呢?

• 給定 a,n,M (int 範圍內 ) ,請求出 (a^n)%M

• 還記得嗎 ~~ (a*b)%M = ( a%M * b%M ) % M

• 因此,提前取餘數並不會造成結果不同,

• 於是我們在此不用擔心溢位問題

(19)

• Ans = (a^n)%M

• 用迴圈求解

• for(ans=1,i=0;i<n;i++)

• ans=ans*a%M;

• 時間複雜度為 O(n)!

• 可是 n 在 int 範圍內,這樣不夠快

• 其實,我們隱含著把求解「 a^n 」分割成求解「 a^(n-1) 」與「 a^1 」的 兩個子問題,再利用相乘合併,求得「 a^n 」的答案

• 兩個子問題的規模懸殊太大,並不是個理想的分割方法

(a^n)%M

(20)

換個想法 換個想法

• 在生活中,我們如何計算 2^16 呢 ?

• 2*2=4

• 4*4=16

• 16*16=256

• 256*256=65536

• 只要四次就足夠了

• 那 2^17 呢 ?

• 再多乘一次 2 就好 !

(21)

• Divide ( 把問題分隔成相似的小問題 )

• a^n =

• If n%2 == 0, a^n = [a^(n/2)]^2

• If n%2 == 1, a^n = [a^((n-1)/2)]^2*a

• a^(n/2) 確實是「性質相近」且「規模較小」的子問題

• Recursive ( 求出小問題的解答 )

• 終止條件: If n = 1, 則 a 就是解答了 !

• Conquer! (Merge 利用小問題的答案求解大問題 )

• 設 b = a^(n/2), 則 If n%2 == 0, a^n = b*b, else a^n=b*b*a, 都是常數 時間可以完成的工作!

(22)

分析一下時間 分析一下時間

• 直觀的看來,每次可以約略把 n 的大小變為一半或更小

• 直到 n 變成 1 為止

• 從大問題到子問題的規模: n -> n/2 -> n/4 -> …… -> 1

• 總共有 (int)log_2(n)+1 層,每一層的合併時間為常數

• 因此,我們可以在 O(log n) 時間內求出解答

• 盡量把問題分為兩個大小約略相等的子問題,讓子問題的最大規 模盡量快速的變小,才能有效率的降低複雜度

• 思考:如何快速的計算 (a + a^2 + a^3 + … + a^n) 呢 ?

(23)

常見的排序問題 常見的排序問題

• 插入排序法

• 泡沫排序法

• 選擇排序法

• 時間複雜度 = ?

(24)

合併排序法 Merge Sort 合併排序法 Merge Sort

• 現在想要排序一個陣列 長度為 n 的陣列

• 依樣畫葫蘆

• 分割 : 把陣列分成左右長度差不多的兩部分

• 遞迴 : 把左右兩部分都各自排序完成

• 合併 : 把左右部分排序後的結果合併起來,成為大問題的答案

• 想一想:給定兩個已經排序過的陣列 A,B, 有沒有可以快速得到 A 與 B 所 有元素一起排序過後的結果的方法呢?

(25)

由兩個分別排序過的陣列 合併出整體的結果

由兩個分別排序過的陣列 合併出整體的結果

• A = [1 5 6 8 9] B = [2 4 7 10]

• C[1] = 目前 A 跟 B 當中最小的元素

• 最小的在哪裡 ? 只可能在 A 的開頭或 B 的開頭

• C[1] = 1

• A = [1 5 6 8 9] B = [2 4 7 10]

• C[2] = 2

• A = [1 5 6 8 9] B = [2 4 7 10]

• C[3] = 4

• A = [1 5 6 8 9] B = [2 4 7 10] ……..

• C[4] = 5

假設兩部分都已經排序完成 , 該怎麼合併呢 !?

(26)

繼續 繼續

A = [1 5 6 8 9] B = [2 4 7 10] => C[5] = 6

A = [1 5 6 8 9] B = [2 4 7 10] => C[6] = 7

A = [1 5 6 8 9] B = [2 4 7 10] => C[7] = 8

A = [1 5 6 8 9] B = [2 4 7 10] => C[8] = 9

A = [1 5 6 8 9] B = [2 4 7 10] => C[9] = 10

A = [1 5 6 8 9] B = [2 4 7 10] => 完成 !

C = [1,2,4,5,6,7,8,9,10]

(27)

合併需要幾次運算 合併需要幾次運算

• 每次從 A 的開頭與 B 的開頭 , 挑選數值較小者

• 如果是空的就忽略他

• 決定 C 的每一項 , 只需要一次比較 => O(1)

• 每次合併所需時間:該部分的數列長度

• A = [1 5 6 8 9] B = [2 4 7 10]

• C = [1,2,4,5,6,7,8,9,10]

• 在這個例子中長度是 9

(28)

遞迴樹 遞迴樹

• 雖然看不見,但實際上我們求解問題的過程可以畫成一 個樹狀結構,我們稱之為「遞迴樹」,樹上的每個節點 實際上代表每個子問題

• 不同於以往,貪心法是「直接走向最佳解所在的分支」

,並得以證明 ( 至少一組 ) 最佳解在該分支中;

• 現在行不通了,必須「綜合考量可能分支所得到的解」

,然後藉由這些小問題的解,找出原本大問題的解!

(29)

29

Merge sort ~ Merge sort ~

7 2 9 4  3 8 6 1

(30)

30

Merge sort ~ Merge sort ~

• 求左半邊

7 2  9 4

7 2 9 4  3 8 6 1

(31)

31

• 左半邊的左半邊

7 2  9 4

7  2

7 2 9 4  3 8 6 1

(32)

32

Merge sort ~ Merge sort ~

• 到”終止問題”囉 , 剩下一個 !

7 2  9 4

7  2

7  7

7 2 9 4  3 8 6 1

(33)

33

• 繼續做

7 2  9 4

7  2

7  7 2  2

7 2 9 4  3 8 6 1

(34)

34

• 合併 !

7 2  9 4

7  2  2 7

7  7 2  2

7 2 9 4  3 8 6 1

(35)

35

• 合併 !

7 2  9 4

7  2  2 7 9 4  4 9

7  7 2  2

7 2 9 4  3 8 6 1

9  9 4  4

(36)

36

• 繼續合併

7 2  9 4  2 4 7 9

7  2  2 7 9 4  4 9

7  7 2  2 9  9 4  4

7 2 9 4  3 8 6 1

(37)

37

• 右邊也做一做

7 2  9 4  2 4 7 9 3 8 6 1  1 3 6 8

7  2  2 7 9 4  4 9 3 8  3 8 6 1  1 6

7  7 2  2 9  9 4  4 3  3 8  8 6  6 1  1

7 2 9 4  3 8 6 1

(38)

38

• 完成囉 !

7 2  9 4  2 4 7 9 3 8 6 1  1 3 6 8

7  2  2 7 9 4  4 9 3 8  3 8 6 1  1 6

7  7 2  2 9  9 4  4 3  3 8  8 6  6 1  1

7 2 9 4  3 8 6 1  1 2 3 4 6 7 8 9

(39)

分析一下執行時間 分析一下執行時間

長度為 n/2 長度為 n/2

長度為 n/4 長度為 n/4 長度為 n/4 長度為 n/4

長度 1 長度 1 長度 1 長度 1 長度 1 長度 1 長度 1 長度 1

長度為 n

第一層總和 : 1 個節點 , 每個長度 O(n)

第二層總和 : 2 個節點 , 每個長度約 (n/2), 總和 O(n)

第三層總和 : 4 個節點 , 每個長度約 (n/4), 總和 O(n)

第四層總和 : 8 個節點 , 每個長度約 (n/8), 總和 O(n)

第 log n 層總和 : n 個節點 , 每個長度 1, 總和 O(n)

(40)

每一層的時間都是 O(n) ! 每一層的時間都是 O(n) !

• 終止條件為 n=1, 總共有幾層呢?

• 顯然由一些簡單的數學 , 會發現共有 log N 層 !

• 總時間 O(n log n) !!

• 空間呢 ? 我們只要利用原本的陣列操作就可以囉 !

• 需要一個輔助陣列暫存合併後的結果

• 不可以覆蓋到原本的兩個要合併的陣列

(41)

Merge-sort function Merge-sort function

• N 、 A[0~(N-1)]

• mergesort(0, N):  其中包含 Left, 不包含 Right

• Example: mergesort(3, 7) => 排序 A[3], A[4], A[5], A[6]

mergesort(Left, Right):

if(Left+1==Right) return;  長度為 1, 終止條件 int Mid = (Left + Right) / 2;  找出中間的位置

mergesort(Left, Mid);  遞迴求解 , 排序好 A[Left], …, A[Mid-1]

mergesort(Mid, Right);  遞迴求解 , 排序好 A[Mid], …, A[Right-1]

int L=Left, R=Mid, K=Left;

while(L<Mid || R<Right)  開始合併 左半邊開頭 = L, 右半邊 = R if( L<Mid &&

(R>=Right || A[L]<=A[R]) )

// 若左半邊還有東西 , 且 (1) 右半邊空了 (2) 左半邊的開頭較小 B[K++] = A[L++];  放進左半邊的開頭 , 而開頭位置 L 加 1

else

B[K++] = A[R++];  放進右半邊的開頭 , 而開頭位置 R 加 1 for(L=Left; L<Right; L++)  丟回去原本的陣列

A[L]=B[L];

(42)

另外的一種思路 另外的一種思路

• 快速排序法

• 對問題先好好的分割,讓合併時不那麼麻煩!

• 分割 :

• 從陣列中選一個值 x

• 亂排一下 , 把陣列分成三個區域 順序保持 L; M; R

• L: <x 的元素 ( 順序不重要 )

• M: =x 的元素

• R: >x 的元素 ( 順序不重要 )

• 遞迴 : 用快速排序法分別把 L 跟 R 排好

• 合併 : 把左右各自的結果合併起來

• 怎麼合併 ? 不用合併 , 遞迴完成自然而然整個都是遞增的

(43)

快速排序法 ~ 快速排序法 ~

• 選擇 x 值 = 6, 讓數列滿足

• 在此為了講解方便 , 三個部份我們依照原本的順序排列

• 這步有很多種不同的實作方式

• (L) 7 2 9 4 3 7 6 1 (R)

• 反覆進行以下操作:直到 L>=R 的位置為止

• 從 L 往右找第一個 >=6 的數 p

• 從 R 往左找第一個 <=6 的數 q

• 交換 p,q , L 往右移一格, R 往左移一格

• 7 (L) 2 9 4 3 7 6 (R) 1

• 1 2 9 (L) 4 3 7 (R) 6 7

• 1 2 6 4 3 7 9 7

• 在此為了講解方便 , 我們假設結果為 [2 4 3 1] 6 [7 9 7]

7 2 9 4 3 7 6 1  2 4 3 1 6 7 9 7

小於 6 等於 6 大於 6

(44)

快速排序法 ~ 快速排序法 ~

• 開始排序 , 左右部分已經分好囉

7 2 9 4 3 7 6 1  2 4 3 1 6 7 9 7

(45)

快速排序法 ~ 快速排序法 ~

• 遞迴排好左半邊

2 4 3 1  1 2 4 3

7 2 9 4 3 7 6 1  2 4 3 1 6 7 9 7

(46)

快速排序法 ~ 快速排序法 ~

• 遇到終止條件

2 4 3 1  1 2 4 3

1

7 2 9 4 3 7 6 1  2 4 3 1 6 7 9 7

(47)

快速排序法 ~ 快速排序法 ~

• 1 的順序確定了

7 2 9 4 3 7 6 1  2 4 3 1 6 7 9 7

2 4 3 1  1 2 4 3

1

(48)

快速排序法 ~ 快速排序法 ~

• 排右半邊

7 2 9 4 3 7 6 1  2 4 3 1 6 7 9 7

2 4 3 1  1 2 4 3

1 4 3  3 4

4

(49)

快速排序法 ~ 快速排序法 ~

• 3 與 4 的順序確定了 , 由於原數列滿足

• 所以兩個部分各自排好後 , 整體就滿足從小到大的關係

7 2 9 4 3 7 6 1  2 4 3 1 6 7 9 7

2 4 3 1  1 2 3 4

1 4 3  3 4

4

(50)

快速排序法 ~ 快速排序法 ~

• 左半邊都排序完成了

7 2 9 4 3 7 6 1  1 2 3 4 6 7 9 7

2 4 3 1  1 2 3 4

1 4 3  3 4

4

(51)

快速排序法 ~ 快速排序法 ~

• 開始做右半邊

7 9 7 1  1 3 8 6 7 2 9 4 3 7 6 1  1 2 3 4 6 7 9 7

2 4 3 1  1 2 3 4

1 4 3  3 4

4

(52)

快速排序法 ~ 快速排序法 ~

• 小於 7 的部分是空的 , 不需要遞迴求解

7 9 7  7 7 9 6 7 2 9 4 3 7 6 1  1 2 3 4 6 7 9 7

2 4 3 1  1 2 3 4

1 4 3  3 4

4

9

(53)

快速排序法 ~ 快速排序法 ~

• 更新回去 ( 雖然只有一個元素 )

7 9 7  7 7 9 6 7 2 9 4 3 7 6 1  1 2 3 4 6 7 9 7

2 4 3 1  1 2 3 4

1 4 3  3 4

4

9

(54)

快速排序法 ~ 快速排序法 ~

• 兩個部分都做好了 , 大功告成 @_@

7 9 7  7 7 9 6 7 2 9 4 3 7 6 1  1 2 3 4 6 7 7 9

2 4 3 1  1 2 3 4

1 4 3  3 4

4

9

(55)

分析時間 分析時間

• 每次需要比較一個元素與 x 的大小關係 , 決定他的位置在左邊還是右邊

• 所需時間與該部分元素數量呈線性關係

• 每一層總和都接近 O(n) 等級

(56)

長度為 n/2 長度為 n/2

長度為 n/4 長度為 n/4 長度為 n/4 長度為 n/4

長度 1 長度 1 長度 1 長度 1 長度 1 長度 1 長度 1 長度 1

長度為 n

第一層總和 : 1 個節點 , 每個長度 O(n)

第二層總和 : 2 個節點 , 每個長度約 (n/2), 總和 O(n)

第三層總和 : 4 個節點 , 每個長度約 (n/4), 總和 O(n)

第四層總和 : 8 個節點 , 每個長度約 (n/8), 總和 O(n)

第 log n 層總和 : n 個節點 , 每個長度 1, 總和 O(n)

好的情況

( 長相跟 Merge sort 一模一樣 ) 好的情況

( 長相跟 Merge sort 一模一樣 )

(57)

分析時間 分析時間

• 在一般的情形下 (x 介於中間 , 使得 L 與 R 的長度差不 多 ) 也只會有 log n 層 , 於是在這種情形下的時間複雜 度與 Merge sort 相等

• O(n) * O(log n) = O(n log n)

• 還記得演算法的執行時間中 , 除了時間複雜度 ( 執行時間 與問題規模的相對增長速率 ) 以外 , 執行時間的常數也會 稍微影響執行速度

快速排序法在好的情況略比 Merge Sort 快

(58)

糟糕的情況 糟糕的情況

• 長度總和 n

• 長度總和 n-1

• 長度總和 n-2

• 長度總和 n-3

• …………

• 長度總和 1

• 1 + 2 + 3 + … + n = n(n+1)/2 = O(n^2) !!

(59)

所以…… 所以……

• 總共可能高達 n 層 !!!

• 最差情況 : O(n) * O(n) = O(n^2) 遠大於 O(n log n)

• 幸運的是 , 這並不常發生

• 邪惡的出題者表示 : 依據莫非定律 , 你覺得 不會發生的情況有時特別容易發生 OAOAOAO

(60)

比較一下兩種排序的方法 比較一下兩種排序的方法

• 合併排序 :

• 隨意分割問題 ( 左右切一半 ), 然後好好的合併

• 因為分割的兩個問題大小接近 , 所以遞迴的深度不會超過 O(log n), 總時間 O(n log n).

• 快速排序 : ( 聽起來很迅速 )

• 好好的分割問題 ( 排列好大小關係 ), 然後自然就合併了

• 因為分割的兩個問題大小可能差異很大 , 所以遞迴的深度高達 O (n), 總時間最差 O(n^2), 平均來說 O(n log n).

• 我們可以隨機的選取中心點 , 這樣期望複雜度為 O(n log n)

(61)

經典問題 – 逆序數對數量計算 經典問題 – 逆序數對數量計算

• A = [2, 4, 1, 3]

• 對於一個陣列 A 中 , 任取兩個元素 , 順序不變 , 如果前者大於後者 , 則我們稱為”逆序數對”

• (2,4)

• (2,1) => 逆序數對

• (2,3)

• (4,1) => 逆序數對

• (4,3) => 逆序數對

• (1,3)

(62)

計算逆序數對的數量 計算逆序數對的數量

• 既然不知道答案 , 就每一種組合都試試看 !

• 還記得傳說中的枚舉法

• 枚舉所有數對…檢查一下大小就好 !

• 有 n(n-1)/2 = O(n^2) 種 !

• 成本太高囉

• 仔細想想 , 我們並不是真的需要找出所有逆序數對

(63)

分治 ! 分治 !

• 我們試著把陣列分成兩半

• 線條就是”逆序數對”

• 分成兩半後有兩種情形

• 1. 在同一部分 ( 上面的線條 )

• 2. 在不同部分 ( 下面的線條 ), 橫跨兩邊

7 2 9 4  3 8 6 1

(64)

分割問題 分割問題

• 1. 在同一部分 => 遞迴求解

• 2. 在不同部分 => 合併的時候處理

7 2 9 4  3 8 6 1

(65)

遞迴形式 遞迴形式

• 因為分割問題的方法與 Merge Sort 非常類似,因此就沿用一 樣的定義吧 !

(66)

寫成函數 寫成函數

N 、 A[0~(N-1)]

int counting(Left, Right):  其中包含 Left, 不含 Right

回傳逆序數對的數量

N=8, A[0~(N-1)] = [7,2,9,4,3,8,6,1]

counting(0,4) = 3

counting(5,8) = 4

Mid=(Left,Right)/2

counting(Left,Right)=

counting(Left, Mid) ( 都

在左邊

)+counting(Mid, Right) (

都在右邊

)

橫跨左右部分的逆序數對 ( 下面的線條 )

7 2 9 4  3 8 6 1

(67)

合併問題 合併問題

• counting(Left,Right)=

• counting(Left, Mid) ( 都在左邊)+counting(Mid, Right) ( 都在右邊 )

• 橫跨左右部分的逆序數對 ( 下面的線條 )

• 橫跨左右部分的逆序數對 : 怎麼求呢 ?

• 枚舉左邊的每一項 , 右邊的每一項

• 在第一層就 O(n/2) * O(n/2) = O(n^2) !

• 辛苦的分治白忙一場 , 問題的合併變成瓶頸

• 有沒有更好的合併方法呢

(68)

觀察性質 觀察性質

• 觀察 1: 因為這些數對橫跨兩邊 , 因此 :

• 即使將左右兩部分排序 , 這部分的數量依然相同 !

• 觀察 2: 經過排序以後瓊舉左邊的每一項 , 與它有關的逆序 數對是原本右邊滿足的部分 , 加上往右找比它小的那些數值

!

7 2 9 4  3 8 6 1

2 4 7 9  1 3 6 8

1 對 : (2,1) 2 對 : (4,1)/

(4,3) 3 4

共有 10 對 ! 3>1,

6>4,

(69)

時間呢 時間呢

• 回傳 : 左半部分與右半部分 ( 排序前 ) 的逆序數對數量 : 3+4

• 加上上面所有箭頭的長度和 : 10 = 3+4+10 = 17

• 每瓊舉左邊的一項 x, 就只要檢查上一次瓊舉的部分箭頭終點的下一項 y, 如 果 y<x, 那箭頭就可以延伸 !

• 設該層共有 n 個元素 , 左右各 n/2 個

• 左邊枚舉 n/2 次

• 箭頭也至多只會延伸 n/2 次

• O(n) !!

• 與合併排序法的時間分析一模一樣

2 4 7 9  1 3 6 8

(70)

寫寫程式吧 寫寫程式吧

• 以合併排序法為基礎

• 在合併前額外加上計算 “橫跨左右部分的逆序數對數量”

• 回傳三個部份的總和 , 就是在處理範圍中的逆序數對數量

• 順便也幫忙排序完成了 !

• 在此問題中,子問題的答案並不只有「該區間內逆序數對的數 量」,也隱含了「該區間內排序過的結果」

(71)

counting function counting function

• N 、 A[0~(N-1)]

• int counting(Left, Right):  其中包含 Left, 不含 Right

• 回傳逆序數對的數量

int counting (Left, Right):

if(Left+1==Right) return 0;  長度為 1, 終止條件 int Mid = (Left + Right) / 2;  找出中間的位置

int Cnt = counting(Left, Mid);  遞迴求解 , 順便排序 A[Left]~A[Mid-1]

Cnt += counting(Mid, Right);  遞迴求解 , 順便排序 A[Mid]~A[Right-1]

int L, R=Mid;  R: 箭頭的起點永遠在中間

for(L=Left; L<Mid; L++){  枚舉左邊的每一項

while(R<Right && A[R]<A[L])  如果箭頭可以延伸的話 R++;  就延伸吧 !

Cnt += R-Mid;  加上箭頭的長度 }

//Merge sort: 合併左右兩個部分 return Cnt;

參考文獻

相關文件

Algorithm Design Methods Greedy Algorithm.. by Chin

Algorithm Design Methods Divide &amp; Conquer..

for Sprout 2014 by Chin

Initially, in closed shock tube, water column moves at u = 1 from left to right, yielding air compression at right. &amp; air expansion

by 2019

Algorithm Design Methods Enumeration.. by Chin

if left_sum&gt;=right_sum and left_sum&gt;=cross_sum return (left_low,left_high,left_sum). else if right_sum&gt;=left_sum and right_sum&gt;=cross_sum

證明比較鬆的upper bound或lower bound來慢慢 接近tight