1. 程式人生 > >【演算法課】遞迴與分治法

【演算法課】遞迴與分治法

概述

演算法

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

  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組。然後找出每一組中的中位數,然後在這個中位數的集合中找到中位數。