【算法课】递归与分治法

概述

算法

若干指令组成的有穷序列。

  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组。然后找出每一组中的中位数,然后在这个中位数的集合中找到中位数。

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