【演算法課】遞迴與分治法
概述
演算法
若干指令組成的有窮序列。
- 輸入:零或多個外部輸入
- 輸出:至少一個輸出
- 確定性:每條指令無歧義
- 有限性:每條指令執行次數有限,總執行時間有限
複雜性
分時間和空間複雜性。
計算時間複雜度的時候,通過計算其核心語句的執行次數,匯出其關於問題規模N的複雜度計量T(N)。
而當N→∞,T(N)→∞。此時通過求T(N)的漸進式來簡化複雜度計量。引入漸進意義下記號:O、Ω、θ和o。
O(上界)
設f(N)和g(N)為正數集上的正函式。存在正的常數C和自然數N0,使N>=N0時,總有f(N)<=g(N),則稱g(N)是f(N)在N充分大時的一個上界,記為f(N)=O(g(N))
其餘漸進符號類推。
遞迴與分治法
遞迴
直接或間接呼叫自身的演算法。
分治法
將規模為n的問題分為k個規模較小的子問題。子問題和原問題相同且相互獨立。遞迴地解決子問題並將子問題的解合併為原問題的解。
一般而言,將問題分為大小相近的子問題是最有效率的。通常將問題一分為二。
從設計模式可以看出,分治法一般用遞迴實現。所以分治法的效率可以通過遞迴表示式進行分析。則有:
其中問題規模最小為1,其時解所耗費的時間為常數單位。規模大於1時,將問題分解為k個規模為n/m的子問題。將這k個子問題的解合併耗費的時間為f(n)。則展開上式可得:
經典分治演算法
二分搜尋:
將陣列分為兩半,將中間元素和目標比較,根據結果對左邊或右邊遞迴進行二分搜尋。
合併排序:
將陣列分為等長的兩半,對兩個子陣列遞迴進行合併排序,然後再把兩個有序的子數組合並。
快速排序:
以陣列中特定元素為基準,把陣列分為比它大和比它小的兩部分,再對兩部分遞迴進行快速排序。
Strassen矩陣乘法:
n階矩陣A和B相乘,可以分解為其子矩陣的乘法:
即:
然後子矩陣的乘法再分解,直到子矩陣規模為2*2.
但這種拆分沒有減少矩陣乘法次數,時間複雜度和直接做矩陣乘法沒有差別。故Strassen提出了新的演算法:
首先算出7個矩陣:
然後有
這樣只需7次子矩陣乘法就完成了矩陣相乘,演算法複雜度為
最近點對:
最近點對問題是針對一個點的集合,找出當中距離最近的兩個點。最原始的做法就是算出每個點和其餘n-1個點的距離,然後找出距離最小的那個點對。這個做法的時間複雜度為O(n^2)。
這個問題其實可以用分治法來達到更優的解決時間。將點集分為兩半,遞迴地對兩個點集找到其中的最近點對。但問題在於如何將兩個點集的解合併。如果最近點對的兩個點都在同一個子點集中,那麼解的合併很容易。但如果兩個點分屬不同的子集呢?
先看一維空間中的問題解法。將點按座標排序後,以點m為基準把點集分為規模相等的兩半。遞迴求出第一個子集中的最近點對p1和q1,第二子集中的最近點對p2和q2.那麼對於原點集,其最近點對可能是p1q1,p2q2或者p3q3,其中p3和q3分屬兩個不同的子集。假設p1q1和p2q2中距離更小的一對的距離為d。可以知道如果存在分屬兩個自己的最近點對p3q3,兩個點距離小於d,則可知p3與q3各自和分割點m的距離都小於d。又對於p3所在子集,p3與任意點的距離都大於d,也即是其子集除p3外任意點和分割點m距離都大於d。q3同理。故以分割點m為中心,半徑為d的區域內,只存在p3與q3兩個點。如此就可以通過計算每個點與分割點的距離,從而判斷是否存在p3q3點對。這一次判斷複雜度為O(n)。則可得以下遞迴方程:
可解此遞迴方程得T(n) = O(nlogn)
接下來把演算法推廣至二維,點集分佈在平面上,每個點都有二維座標x和y。為了將點集分割為規模相等的兩個子集,選取垂線x=m為分割直線。m為點集中所有點的x座標的中位數。和一維情況一樣,遞迴求子集的解求得p1q1和p2q2,然後判斷是否存在兩個點分屬兩個子集的最近點對的情況。
在一維情況下,分割點為中心半徑為d的區域內只會存在一個點對,所以可以簡單確定最近點對。但二維情況複雜得多,兩個子集中的每個點都可能是p3q3的組成。
首先同樣假設兩個子集的解中距離更近的一對的距離為d。那麼如果存在p3q3,其距離必然小於d。那麼對於其中一個子集中的任一點p,另一子集中可能與p組成最近點對的點必然處在以分割線為邊,直線y=yp為中線,長為2d寬為d的長方形中。
由於在第二子集中任意點對的距離都大於d,故dx2d長方形中最多隻會存在6個點。如此就可以檢查第一個子集中每一個於分割線距離小於d的點與其對應在第二子集區域內最多6個點的距離即可,最大需要檢查的點對數量為6xn/2=3n。
而對於特定點p,要找出與其匹配的最多6個點,可以先把整個點集按y座標排序,然後檢查點p時只要檢查這個有序序列上p相鄰的y座標差小於d的點即可。如此可以在O(n)時間完成檢查。遞推公式同一維,解得時間複雜度為O(nlogn),而點集基於y軸排序的時間複雜度也是O(nlogn),則總的時間複雜度就是O(nlogn)。
順序統計量:
對於n個元素的集合S,找出第i小的元素。
常規做法是先將集合排序,然後取第i位元素。時間複雜度為O(nlogn)。但有沒有可能線上性時間複雜度求解。
可以使用基於快排的隨機切分演算法。也即通過幾個基數將集合切分為左右兩部分,然後將左邊小的部分的元素個數與i比較,根據結果遞迴地對左邊或右邊求解。
C++實現:
#include <iostream>
using namespace std;
void swap(int* A, int l, int r) {
int temp = A[l];
A[l] = A[r];
A[r] = temp;
}
int rand_partition(int* A, int p, int q) {
int l = p + 1, r = q;
while (l < r) {
while (A[l] < A[p])
l++;
while (A[r] > A[p])
r--;
if (l < r) {
swap(A, l++, r--);
}
}
swap(A, p, r);
return r;
}
int rand_select(int* A, int p, int q, int i) {
if (p == q)
return A[p];
int r = rand_partition(A, p, q);
int k = r - p + 1;
if (i == k)
return A[r];
else if (i < k)
return rand_select(A, p, r - 1, i);
else
return rand_select(A, r + 1, q, i - k);
}
int main() {
int A[6] = { 2,5,6,7,3,1 };
int i = 5;
cout << rand_select(A, 0, 5, i);
cin.get();
}
這種演算法在一般情況下時間複雜度為O(n),最壞情況下為O(n^2)。為了使最壞情況下都可以在O(n)時間內求解,需要保證對陣列的切分是好的切分。那就是找出p到q中元素的中位數。
查詢中位數的方法是將元素五個一組,分為n/5+1組。然後找出每一組中的中位數,然後在這個中位數的集合中找到中位數。