【算法課】遞歸與分治法

概述

算法

若干指令組成的有窮序列。

  1. 輸入:零或多個外部輸入
  2. 輸出:至少一個輸出
  3. 確定性:每條指令無歧義
  4. 有限性:每條指令執行次數有限,總運行時間有限

複雜性

分時間和空間複雜性。

計算時間複雜度的時候,通過計算其核心語句的執行次數,導出其關於問題規模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個規模較小的子問題。子問題和原問題相同且相互獨立。遞歸地解決子問題並將子問題的解合併爲原問題的解。

一般而言,將問題分爲大小相近的子問題是最有效率的。通常將問題一分爲二。

從設計模式可以看出,分治法一般用遞歸實現。所以分治法的效率可以通過遞歸表達式進行分析。則有:

T(n) = \left\{\begin{matrix} O(1) & n = 1\\ kT(n/m)+f(n) &n > 1 \end{matrix}\right.

其中問題規模最小爲1,其時解所耗費的時間爲常數單位。規模大於1時,將問題分解爲k個規模爲n/m的子問題。將這k個子問題的解合併耗費的時間爲f(n)。則展開上式可得:

T(n) = n^{log_{m}k} + \sum_{j = 0}^{log_{m}n-1}k^{j}f(n/m^j)

經典分治算法

二分搜索:

將數組分爲兩半,將中間元素和目標比較,根據結果對左邊或右邊遞歸進行二分搜索。

合併排序:

將數組分爲等長的兩半,對兩個子數組遞歸進行合併排序,然後再把兩個有序的子數組合並。

快速排序:

以數組中特定元素爲基準,把數組分爲比它大和比它小的兩部分,再對兩部分遞歸進行快速排序。

Strassen矩陣乘法:

n階矩陣A和B相乘,可以分解爲其子矩陣的乘法:

\begin{bmatrix} C_{11} &C_{12} \\ C_{21} &C_{22} \end{bmatrix} = \begin{bmatrix} A_{11} &A_{12} \\ A_{21} &A_{22} \end{bmatrix}\begin{bmatrix} B_{11} &B_{12} \\ B_{21} &B_{22} \end{bmatrix}

即:

\begin{matrix} C_{11} = A_{11}B_{11}+A_{12}B_{21}\\ C_{12} = A_{11}B_{12}+A_{12}B_{22}\\ C_{21} = A_{21}B_{11}+A_{22}B_{21}\\ C_{22} = A_{21}B_{12}+A_{22}B_{22} \end{matrix}

然後子矩陣的乘法再分解,直到子矩陣規模爲2*2.

但這種拆分沒有減少矩陣乘法次數,時間複雜度和直接做矩陣乘法沒有差別。故Strassen提出了新的算法:

首先算出7個矩陣:

\begin{matrix} M_1 = A_{11}(B_{12}-B_{22})\\ M_2 = (A_{11}+A_{12})B_{22}\\ M_3 = (A_{11}+A_{22})B_{11}\\ M_4 = A_{22}(B_{21}-B_{11})\\ M_5 = (A_{11}+A_{22})(B_{11}+B_{22})\\ M_6 = (A_{12}-A_{22})(B_{21}+B_{22})\\ M_7 = (A_{11}-A_{21})(B_{11}+B_{12}) \end{matrix}

然後有

\begin{matrix} C_{11} = M_5+M_4-M_2+M_6\\ C_{12} = M_1+M_2\\ C_{21} = M_3+M_4\\ C_{22} = M_5+M_1-M_3-M_7 \end{matrix}

這樣只需7次子矩陣乘法就完成了矩陣相乘,算法複雜度爲O(n^{log7})\approx O(n^{2.81})

最近點對:

最近點對問題是針對一個點的集合,找出當中距離最近的兩個點。最原始的做法就是算出每個點和其餘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) = \left\{\begin{matrix} O(1) &n<4 \\ 2T(\frac{n}{2})+O(n) &n\geq 4 \end{matrix}\right.

可解此遞歸方程得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組。然後找出每一組中的中位數,然後在這個中位數的集合中找到中位數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章