分治算法总体思想
将要求解的较大规模的问题分割成
k
k
k 个更小规模的子问题,对这
k
k
k 个子问题分别求解。如果子问题的规模仍然不够小,则再划分为k个子问题,如此递归的进行下去,直到问题规模足够小,很容易求出其解为止。
将求出的小规模的问题的解合并为一个更大规模的问题的解,自底向上逐步求出原来问题的解。
分治法的设计思想是将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
2.1 递归的概念
直接或间接地调用自身的算法称为递归算法。用函数自身给出定义的函数称为递归函数。
由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。
分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。
边界条件与递归方程是递归函数的二个要素,递归函数只有具备了这两个要素,才能在有限次计算后得出结果。
当一个函数及它的一个变量是由函数自身定义时,称这个函数是双递归函数。
Ackerman 函数
{ A ( 1 , 0 ) = 2 A ( 0 , m ) = 1 , m ≥ 0 A ( n , 0 ) = n + 2 , n ≥ 2 A ( n , m ) = A ( A ( n − 1 , m ) , m − 1 ) , n , m ≥ 1 \begin{cases} A(1,0)=2\\ A(0,m)=1\ ,\ m\ge0\\ A(n,0)=n+2\ ,\ n\ge2\\ A(n,m)=A(A(n-1,m),m-1)\ ,\ n,m\ge1 \end{cases} ⎩ ⎨ ⎧A(1,0)=2A(0,m)=1 , m≥0A(n,0)=n+2 , n≥2A(n,m)=A(A(n−1,m),m−1) , n,m≥1
Ackerman函数无法找到非递归的定义。
**优点:**结构清晰,可读性强,而且容易用数学归纳法来证明算法的正确性,因此它为设计算法、调试程序带来很大方便。
**缺点:**递归算法的运行效率较低,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多。
消除递归调用
在递归算法中消除递归调用,使其转化为非递归算法。
- 采用一个用户定义的栈来模拟系统的递归调用工作栈。该方法通用性强,但本质上还是递归,只不过人工做了本来由编译器做的事情,优化效果不明显。
- 用递推来实现递归函数。
- 通过变换能将一些递归转化为尾递归,从而迭代求出结果。
后两种方法在时空复杂度上均有较大改善,但其适用范围有限。
尾递归
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。
当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。
尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。
// 递归
int fibonacci(int n){
if(n==0) return 0;
else if(n==1) return 1;
else return fibonacci(n-1)+fibonacci(n-2)
}
// 尾递归
int fibonacci_tail(int n,int ret1,int ret2){
if(n==0) return ret1;
else return fibonacci_tail(n-1, ret2, ret1+ret2);
}
关键点在于,尾递归每次调用都在收集结果,避免了线性递归不收集结果只能依次展开消耗内存的坏处。
使用尾递归可以带来一个好处:因为进入最后一步后不再需要参考外层函数(caller)的信息,因此没必要保存外层函数的stack,递归需要用的stack只有目前这层函数的,因此避免了栈溢出风险。
2.1.1 递归方程及其求解办法
-
建立递归方程
-
递归方程的求解
-
迭代法
从初始递归方程的开始,反复用右边的函数代替左边的函数,直到初值。 -
代入法
猜测解的形式,用数学归纳法(用于证明计算系数的范围)例: T ( n ) = 4 T ( n 2 ) + n T(n)=4T(\frac{n}{2})+n T(n)=4T(2n)+n
① 猜测 T ( n ) = O ( n 3 ) T(n)=O(n^3) T(n)=O(n3)
② 令 T ( n ) = c n 3 T(n)=cn^3 T(n)=cn3
k < n 时 , T ( k ) = c k 3 k = n 时 , T ( n ) = 4 T ( n 2 ) + n = 4 c ( n 2 ) 3 + n = n 3 − c 2 n 3 + n \begin{aligned} k\lt n\ 时,\ T(k)&=ck^3\\ k = n\ 时,\ T(n)&=4T(\frac{n}{2})+n\\ &=4c(\frac{n}{2})^3+n\\ &=n^3-\frac{c}{2}n^3+n \end{aligned} k<n 时, T(k)k=n 时, T(n)=ck3=4T(2n)+n=4c(2n)3+n=n3−2cn3+n
此时, c = 2 , n = 1 c=2,n=1 c=2,n=1 时, c 2 n 3 − n = 0 ≥ 0 ⇒ c n 3 − c 2 n 3 + n ≤ c n 3 \frac{c}{2}n^3-n=0\ge0\ \Rightarrow cn^3-\frac{c}{2}n^3+n\le cn^3 2cn3−n=0≥0 ⇒cn3−2cn3+n≤cn3
且 k = 1 k=1 k=1 时, T ( k ) = T ( 1 ) = 1 ≤ c ⋅ 1 3 ( c ≥ 1 ) T(k)=T(1)=1\le c·1^3\ (c\ge1) T(k)=T(1)=1≤c⋅13 (c≥1)
⇒ T ( n ) = O ( n 3 ) \Rightarrow T(n)=O(n^3) ⇒T(n)=O(n3) 成立.【进一步推理猜想】
① 猜测 T ( n ) = O ( n 2 ) T(n)=O(n^2) T(n)=O(n2)
② 令 T ( n ) = c n 2 T(n)=cn^2 T(n)=cn2
k < n 时 , T ( k ) = c k 2 k = n 时 , T ( n ) = 4 T ( n 2 ) + n = 4 c ( n 2 ) 2 + n = c n 2 + n ≥ c n 2 \begin{aligned} k\lt n\ 时,\ T(k)&=ck^2\\ k = n\ 时,\ T(n)&=4T(\frac{n}{2})+n\\ &=4c(\frac{n}{2})^2+n\\ &=cn^2+n\ge cn^2 \end{aligned} k<n 时, T(k)k=n 时, T(n)=ck2=4T(2n)+n=4c(2n)2+n=cn2+n≥cn2
⇒ \Rightarrow ⇒ 不成立.
令 T ( n ) = c 1 n 2 + c 2 n T(n)=c_1n^2+c_2n T(n)=c1n2+c2n
k < n 时 , T ( k ) = c 1 k 2 + c 2 n k = n 时 , T ( n ) = 4 T ( n 2 ) + n = 4 c 1 ( n 2 ) 2 + 4 c 2 ⋅ n 2 + n = c 1 n 2 + 2 c 2 n + n ≤ c 1 n 2 + c 2 n ( − c 2 n − n ≥ 0 且 c 2 ≤ − 1 ) \begin{aligned} k\lt n\ 时,\ T(k)&=c_1k^2+c_2n\\ k = n\ 时,\ T(n)&=4T(\frac{n}{2})+n\\ &=4c_1(\frac{n}{2})^2+4c_2·\frac{n}{2}+n\\ &=c_1n^2+2c_2n+n\\ &\le c_1n^2+c_2n(-c_2n-n\ge0\ 且\ c_2\le-1) \end{aligned} k<n 时, T(k)k=n 时, T(n)=c1k2+c2n=4T(2n)+n=4c1(2n)2+4c2⋅2n+n=c1n2+2c2n+n≤c1n2+c2n(−c2n−n≥0 且 c2≤−1)
k = 1 k=1 k=1 时, T ( k ) = T ( 1 ) = 1 ≤ c 1 + c 2 ( c 1 ≥ 1 − c 2 ≥ 2 ) T(k)=T(1)=1\le c_1+c_2\ (c_1\ge1-c_2\ge2) T(k)=T(1)=1≤c1+c2 (c1≥1−c2≥2)
⇒ T ( n ) = c 1 n 2 + c 2 n \Rightarrow T(n)=c_1n^2+c_2n ⇒T(n)=c1n2+c2n 成立. -
递归树法【必考】
用树的形式给出一个递归算法执行的成本模型。
① 展开递归方程,构造对应的递归树。
② 将树中每层中的代价求和,得到每层代价,再将所有层的代价求和,得到总的递归调用代价例:
【一般由递归树进行估计得出结果,用代入法进行验证】
-
主方法{
适用于 T ( n ) = a T ( n b ) + f ( n ) T(n)=aT(\frac{n}{b})+f(n) T(n)=aT(bn)+f(n)
其中 a ≥ 1 , b > 1 a\ge1,b\gt1 a≥1,b>1 为常数, f ( n ) f(n) f(n) 为渐近正函数答案有三种情况:【比较 f ( n ) f(n) f(n) 与 n log b a n^{\log_ba} nlogba 】
① f ( n ) = O ( n log b a − ϵ ) , ϵ > 0 f(n)=O(n^{\log_ba-\epsilon}),\ \epsilon\gt0 f(n)=O(nlogba−ϵ), ϵ>0 即 n log b a n^{\log_ba} nlogba 比 f ( n ) f(n) f(n) 大(大 n ϵ n^\epsilon nϵ 倍)
则 T ( n ) = Θ ( n log b a ) T(n)=\Theta(n^{\log_ba}) T(n)=Θ(nlogba)
② f ( n ) = O ( n log b a ) f(n)=O(n^{\log_ba}) f(n)=O(nlogba) 则 T ( n ) = Θ ( n log b a log 2 n ) T(n)=\Theta(n^{\log_ba}\log_2n) T(n)=Θ(nlogbalog2n)
③ f ( n ) = Ω ( n log b a + ϵ ) , ϵ > 0 f(n)=\Omega(n^{\log_ba+\epsilon}),\ \epsilon\gt0 f(n)=Ω(nlogba+ϵ), ϵ>0 即 f ( n ) f(n) f(n) 比 n log b a n^{\log_ba} nlogba 大(大 n ϵ n^\epsilon nϵ 倍)
且对于某个常数 c < 1 c\lt1 c<1 和所有充分大的 n n n 有 a f ( n b ) ≤ c f ( n ) af(\frac{n}{b})\le cf(n) af(bn)≤cf(n)
那么 T ( n ) = Θ ( f ( n ) ) T(n)=\Theta(f(n)) T(n)=Θ(f(n))
-
2.2 分治法
分治法排序基本思想
将待排序元素分成大小大致相同的2个子集合,分别对2个子集合进行排序,最终将排好序的子集合合并成为所要求的排好序的集合。
分治法的设计思想
将原问题分解成若干个规模较小的子问题,递归地求解这些子问题,然后合并子问题的解得到原问题的解。
分治法的适用条件
- 该问题的规模缩小到一定的程度就可以容易地解决;
- 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质;
- 利用该问题分解出的子问题的解可以合并为该问题的解;
- 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题
如果各子问题是不独立的,则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然也可用分治法,但一般用动态规划较好。
分治法的求解过程
- 划分子问题
将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题。 - 求解子问题
若子问题规模较小而容易被解决则直接求解,否则递归地求解各个子问题。 - 合并子问题
将各个子问题的解合并为原问题的解。
divide-and-conquer(P){
if(|P|<=n0) adhoc(P); // 解决小规模的问题
divide P into smaller subinstances P1,P2,...,Pk; // 分解问题
for(i=1;i<=k;i++)
yi=divide-and-conquer(pi); // 递归地解各子问题
return merge(y1,y2,...,yk); // 将各子问题的解合并为原问题的解
}
人们从大量实践中发现,在用分治法设计算法时,最好使子问题的规模大致相同。即将一个问题分成大小相等的k个子问题的处理方法是行之有效的。
这种使子问题规模大致相等的做法是出自一种平衡子问题的思想,它几乎总是比子问题规模不等的做法要好。
分治法复杂性分析
一个分治法将规模为
n
n
n 的问题分成
k
k
k 个规模为
n
m
\frac{n}{m}
mn 的子问题去解。设分解阀值
n
0
=
1
n_0=1
n0=1,且
a
d
h
o
c
adhoc
adhoc 解规模为
1
1
1 的问题耗费
1
1
1 个单位时间。再设将原问题分解为
k
k
k 个子问题以及用
m
e
r
g
e
merge
merge 将
k
k
k 个子问题的解合并为原问题的解需用
f
(
n
)
f(n)
f(n) 个单位时间。用
T
(
n
)
T(n)
T(n) 表示该分治法解规模为
∣
P
∣
=
n
|P|=n
∣P∣=n 的问题所需的计算时间,则有:
T
(
n
)
=
{
O
(
1
)
,
n
=
1
k
T
(
n
m
)
+
f
(
n
)
,
n
>
1
T(n)=\begin{cases} O(1)\ ,\ n=1\\ \\ kT(\frac{n}{m})+f(n)\ ,\ n\gt1 \end{cases}
T(n)=⎩
⎨
⎧O(1) , n=1kT(mn)+f(n) , n>1
迭代法解得:
T
(
n
)
=
n
log
m
k
+
Σ
j
=
0
log
m
n
−
1
k
j
f
(
n
m
j
)
T(n)=n^{\log_mk}+\Sigma_{j=0}^{\log_mn-1}k^jf(\frac{n}{m^j})
T(n)=nlogmk+Σj=0logmn−1kjf(mjn)
递归方程及其解只给出 n n n 等于 m m m 的方幂时 T ( n ) T(n) T(n) 的值,但是如果认为 T ( n ) T(n) T(n) 足够平滑,那么由 n n n 等于 m m m 的方幂时 T ( n ) T(n) T(n) 的值可以估计 T ( n ) T(n) T(n) 的增长速度。
通常假定 T ( n ) T(n) T(n) 是单调上升的,从而当 m i ≤ n < m i + 1 m^i≤n<m^{i+1} mi≤n<mi+1 时, T ( m i ) ≤ T ( n ) < T ( m i + 1 ) T(m^i)≤T(n)<T(m^{i+1}) T(mi)≤T(n)<T(mi+1) 。
2.2.1 合并排序(归并排序)
void Merge(int *A,int *L,int leftCount,int *R,int rightCount) {
int i=0,j=0,k=0;
// i - to mark the index of left aubarray (L)
// j - to mark the index of right sub-raay (R)
// k - to mark the index of merged subarray (A)
while(i<leftCount && j< rightCount) {
if(L[i] < R[j]) A[k++] = L[i++];
else A[k++] = R[j++];
}
while(i < leftCount) A[k++] = L[i++];
while(j < rightCount) A[k++] = R[j++];
}
void MergeSort(int *A,int n) {
int mid,i, *L, *R;
if(n < 2) return;
// base condition. If the array has less than two element, do nothing.
mid = n/2;
// find the mid index.
// create left and right subarrays
L = new int[mid];
R = new int [n - mid];
// mid elements (from index 0 till mid-1) should be part of left sub-array
// and (n-mid) elements (from mid to n-1) will be part of right sub-array
for(i = 0;i<mid;i++) L[i] = A[i]; // creating left subarray
for(i = mid;i<n;i++) R[i-mid] = A[i]; // creating right subarray
MergeSort(L,mid); // sorting the left subarray
MergeSort(R,n-mid); // sorting the right subarray
Merge(A,L,mid,R,n-mid); // Merging L and R into A as sorted list.
// the delete operations is very important
delete [] R;
delete [] L;
}
复杂度分析
T ( n ) = { O ( 1 ) , n ≤ 1 2 T ( n 2 ) + O ( n ) , n > 1 T(n)=\begin{cases} O(1)\ ,\ n\le1\\ \\ 2T(\frac{n}{2})+O(n)\ ,\ n\gt1 \end{cases} T(n)=⎩ ⎨ ⎧O(1) , n≤12T(2n)+O(n) , n>1
- 最坏时间复杂度:O(nlogn)
- 平均时间复杂度:O(nlogn)
- 辅助空间:O(n)
2.2.2 快速排序
在快速排序中,记录的比较和交换是从两端向中间进行的,关键字较大的记录一次就能交换到后面单 元,关键字较小的记录一次就能交换到前面单元,记录每次移动的距离较大,因而总的比较和移动次数较少。
void Quick_Sort(int *arr, int begin, int end){
if(begin > end) return;
int tmp = arr[begin];
int i = begin;
int j = end;
while(i != j){
while(arr[j] >= tmp && j > i)
j--;
while(arr[i] <= tmp && j > i)
i++;
if(j > i){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
arr[begin] = arr[i];
arr[i] = tmp;
Quick_Sort(arr, begin, i-1);
Quick_Sort(arr, i+1, end);
}
- 最坏时间复杂度:O(n2)
- 平均时间复杂度:O(nlogn)
- 辅助空间:O(n)或O(logn)
2.2.3 二分搜索
给定已按升序排好序的 n n n 个元素 a [ 0 : n − 1 ] a[0:n-1] a[0:n−1] ,现要在这n个元素中找出一特定元素 x x x 。
int BinarySearch(Type a[],const Type &x,int l,int r){
while(r>=l){
int m=(l+r)/2;
if(x==a[m]) return m;
if(x<a[m]) r=m-1;
else l=m+1;
}
return -1;
}
复杂度分析
每执行一次算法的while
循环,待搜索数组的大小减少一半。
因此,在最坏情况下,while
循环被执行了 O ( l o g n ) O(logn) O(logn) 次。循环体内运算需要 O ( 1 ) O(1) O(1) 时间,因此整个算法在最坏情况下的计算时间复杂性为 O ( l o g n ) O(logn) O(logn) 。
2.2.4 大整数乘法
请设计一个有效的算法,可以进行两个 n n n 位大整数的乘法运算。
传统方法时间复杂度: O ( n 2 ) O(n^2) O(n2) .
将 n n n 位二进制整数 X X X 和 Y Y Y 都分成两段( A , B , C , D A,B,C,D A,B,C,D),每段的长为 n 2 \frac{n}{2} 2n .
由此, X = A × 2 n 2 + B X=A×2^{\frac{n}{2}}+B X=A×22n+B , Y = C × 2 n 2 + D Y=C×2^{\frac{n}{2}}+D Y=C×22n+D .
这样,
X
X
X 和
Y
Y
Y 的乘积为
X
Y
=
(
A
×
2
n
2
+
B
)
(
C
×
2
n
2
+
D
)
=
A
C
×
2
n
+
(
A
D
+
B
C
)
×
2
n
2
+
B
D
XY=(A×2^{\frac{n}{2}}+B)(C×2^{\frac{n}{2}}+D)=AC×2^n+(AD+BC)×2^{\frac{n}{2}}+BD
XY=(A×22n+B)(C×22n+D)=AC×2n+(AD+BC)×22n+BD
如果按此式计算
X
Y
XY
XY ,必须进行
4
4
4 次
n
2
\frac{n}{2}
2n 位整数的乘法(
A
C
AC
AC 、
A
D
AD
AD 、
B
C
BC
BC 和
B
D
BD
BD )、
3
3
3 次不超过
2
n
2n
2n 位的整数加法(分别对应式中的 ”+“ ),以及两次移位(分别对应式中的乘
2
n
2^n
2n 和乘
2
n
2
2^{\frac{n}{2}}
22n )。所有这些加法和移位共用
O
(
n
)
O(n)
O(n) 步运算。设
T
(
n
)
T(n)
T(n) 是
2
2
2 个
n
n
n 位整数相乘所需的运算总数,则
T
(
n
)
=
{
O
(
1
)
,
n
=
1
4
T
(
n
2
)
+
O
(
n
)
,
n
>
1
T(n)=\begin{cases} O(1)\ ,\ n=1\\ \\ 4T(\frac{n}{2})+O(n)\ ,\ n\gt1 \end{cases}
T(n)=⎩
⎨
⎧O(1) , n=14T(2n)+O(n) , n>1
由此可得
T
(
n
)
=
O
(
n
2
)
T(n)=O(n^2)
T(n)=O(n2) .
或者,将
X
Y
XY
XY 写成另一种形式:
X
Y
=
A
C
×
2
n
+
(
(
A
−
B
)
(
D
−
C
)
+
A
C
+
B
D
)
×
2
n
2
+
B
D
XY=AC×2^n+((A-B)(D-C)+AC+BD)×2^{\frac{n}{2}}+BD
XY=AC×2n+((A−B)(D−C)+AC+BD)×22n+BD
此式看起来似乎复杂些,但仅需
3
3
3 次
n
2
\frac{n}{2}
2n 位整数的乘法(
A
C
AC
AC 、
B
D
BD
BD 和
(
A
−
B
)
(
D
−
C
)
(A-B)(D-C)
(A−B)(D−C) )、
6
6
6 次加法和
2
2
2 次移位,得
T
(
n
)
=
{
O
(
1
)
,
n
=
1
3
T
(
n
2
)
+
O
(
n
)
,
n
>
1
T(n)=\begin{cases} O(1)\ ,\ n=1\\ \\ 3T(\frac{n}{2})+O(n)\ ,\ n\gt1 \end{cases}
T(n)=⎩
⎨
⎧O(1) , n=13T(2n)+O(n) , n>1
由此可得
T
(
n
)
=
O
(
n
log
3
)
=
O
(
n
1.59
)
T(n)=O(n^{\log3})=O(n^{1.59})
T(n)=O(nlog3)=O(n1.59) .
2.2.5 Strassen矩阵乘法
A A A 和 B B B 是两个 n × n n×n n×n 矩阵,它们得乘机 A B AB AB 同样是一个 n × n n×n n×n 得矩阵。 A A A 和 B B B 得乘积矩阵 C C C 中元素 c i j c_{ij} cij 定义为 c i j = Σ k = 1 n a i k b k j c_{ij}=\Sigma_{k=1}^na_{ik}b_{kj} cij=Σk=1naikbkj .
传统方法时间复杂度: O ( n 3 ) O(n^3) O(n3) .
假设 n n n 是 2 2 2 的幂。将矩阵 A A A 、 B B B 和 C C C 中每个矩阵都分块成 4 4 4 个大小相等的子矩阵,每个子矩阵都是 n 2 × n 2 \frac{n}{2}×\frac{n}{2} 2n×2n 的方阵,由此可将方程 C = A B C=AB C=AB 重写为
[ C 11 C 12 C 21 C 22 ] = [ A 11 A 12 A 21 A 22 ] [ B 11 B 12 B 21 B 22 ] \left[ \begin{matrix} C_{11} & C_{12}\\ C_{21} & C_{22} \end{matrix} \right] =\left[ \begin{matrix} A_{11} & A_{12}\\ A_{21} & A_{22} \end{matrix} \right] \left[ \begin{matrix} B_{11} & B_{12}\\ B_{21} & B_{22} \end{matrix} \right] [C11C21C12C22]=[A11A21A12A22][B11B21B12B22]
由此可得
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
22
B
22
+
A
21
B
12
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_{22}B_{22}+A_{21}B_{12}
C11=A11B11+A12B21C12=A11B12+A12B22C21=A21B11+A22B21C22=A22B22+A21B12
计算
2
2
2 个
n
n
n 阶方程的乘积转化为计算
8
8
8 个
n
2
\frac{n}{2}
2n 阶方程的乘积和
4
4
4 个
n
2
\frac{n}{2}
2n 阶方程的加法,
2
2
2 个
n
2
×
n
2
\frac{n}{2}×\frac{n}{2}
2n×2n 矩阵的加法显然可以在
O
(
n
2
)
O(n^2)
O(n2) 时间内完成。因此,上述分治法的计算时间满足
T
(
n
)
=
{
O
(
1
)
,
n
=
2
8
T
(
n
2
)
+
O
(
n
2
)
,
n
>
2
T(n)=\begin{cases} O(1)\ ,\ n=2\\ \\ 8T(\frac{n}{2})+O(n^2)\ ,\ n\gt2 \end{cases}
T(n)=⎩
⎨
⎧O(1) , n=28T(2n)+O(n2) , n>2
解为
T
(
n
)
=
O
(
n
3
)
T(n)=O(n^3)
T(n)=O(n3) .
改进方法:
M
1
=
A
11
(
B
12
−
B
22
)
M
2
=
(
A
11
+
A
12
)
B
22
M
3
=
(
A
21
+
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
)
\begin{aligned} M_1&=A_{11}(B_{12}-B_{22})\\ M_2&=(A_{11}+A_{12})B_{22}\\ M_3&=(A_{21}+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{aligned}
M1M2M3M4M5M6M7=A11(B12−B22)=(A11+A12)B22=(A21+A22)B11=A22(B21−B11)=(A11+A22)(B11+B22)=(A12−A22)(B21+B22)=(A11−A21)(B11+B12)
就可以得到
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
\begin{aligned} 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{aligned}
C11C12C21C22=M5+M4−M2+M6=M1+M2=M3+M4=M5+M1−M3−M7
其中运用了
7
7
7 次对于
n
2
\frac{n}{2}
2n 阶矩阵乘积的递归调用和
18
18
18 次
n
2
\frac{n}{2}
2n 阶矩阵的加减运算,可得递归方程:
t
(
n
)
=
{
O
(
1
)
,
n
=
1
7
T
(
n
2
)
+
O
(
n
2
)
,
n
>
2
t(n)=\begin{cases} O(1)\ ,\ n=1\\ \\ 7T(\frac{n}{2})+O(n^2)\ ,\ n\gt2 \end{cases}
t(n)=⎩
⎨
⎧O(1) , n=17T(2n)+O(n2) , n>2
解得
T
(
n
)
=
O
(
n
log
7
)
≈
O
(
n
2.81
)
T(n)=O(n^{\log7})\approx O(n^{2.81})
T(n)=O(nlog7)≈O(n2.81)
Hopcroft和Kerr已经证明(1971),计算 2 2 2 个 2 × 2 2×2 2×2 矩阵的乘积, 7 7 7 次乘法是必要的。因此,要想进一步改进矩阵乘法的时间复杂性,就不能再基于计算 2 × 2 2×2 2×2 矩阵的 7 7 7 次乘法这样的方法了。或许应当研究 3 × 3 3×3 3×3 或 5 × 5 5×5 5×5 矩阵的更好算法。
在Strassen之后又有许多算法改进了矩阵乘法的计算时间复杂性。目前最好的计算时间上界是 O ( n 2.376 ) O(n^{2.376}) O(n2.376)
2.2.6 棋盘覆盖
在一个 2 k × 2 k 2^k×2^k 2k×2k 个方格组成的棋盘中,恰有一个方格与其它方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。在棋盘覆盖问题中,要用图示的 4 4 4 种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何 2 2 2 个L型骨牌不得重叠覆盖。
当 k > 0 k>0 k>0 时,将 2 k × 2 k 2^k×2^k 2k×2k 棋盘分割为 4 4 4 个 2 k − 1 × 2 k − 1 2^{k-1}×2^{k-1} 2k−1×2k−1 子棋盘。特殊方格必位于 4 4 4 个较小子棋盘之一中,其余 3 3 3 个子棋盘中无特殊方格。为了将这 3 3 3 个无特殊方格的子棋盘转化为特殊棋盘,可以用一个L型骨牌覆盖这 3 3 3 个较小棋盘的会合处,从而将原问题转化为 4 4 4 个较小规模的棋盘覆盖问题。递归地使用这种分割,直至棋盘简化为棋盘 1 × 1 1×1 1×1 。
#include<stdio.h>
#define MAXSIZE 1025
int k; // 棋盘大小
int x,y; // 特殊方格位置
int board[MAXSIZE][MAXSIZE]; // 棋盘
int tile=1; // 三格骨牌编号
void chessBoard(int tr,int tc,int dr,int dc,int size){
// tr,tc表示棋盘左上角的下标;dr,dc标记当前的特殊方格
if(size==1) return;
int t=tile++; // L型骨牌号
int s=size/2; // 分割棋盘
// 覆盖左上角子棋盘
if(dr<tr+s && dc<tc+s) // 特殊方格在此棋盘中
chessBoard(tr,tc,dr,dc,s);
else{ // 此棋盘中无特殊方格
// 用t号L型骨牌覆盖右下角
board[tr+s-1][tc+s-1]=t;
// 覆盖其余方格
chessBoard(tr,tc,tr+s-1,tc+s-1,s);
}
// 覆盖右上角子棋盘
if(dr<tr+s && dc>=tc+s) // 特殊方格在此棋盘中
chessBoard(tr,tc+S,dr,dc,s);
else{ // 此棋盘中无特殊方格
// 用t号L型骨牌覆盖右下角
board[tr+s-1][tc+s]=t;
// 覆盖其余方格
chessBoard(tr,tc+s,tr+s-1,tc+s,s);
}
// 覆盖左下角子棋盘
if(dr>=tr+s && dc<tc+s) // 特殊方格在此棋盘中
chessBoard(tr+s,tc,dr,dc,s);
else{ // 此棋盘中无特殊方格
// 用t号L型骨牌覆盖右下角
board[tr+s][tc+s-1]=t;
// 覆盖其余方格
chessBoard(tr+s,tc,tr+s,tc+s-1,s);
}
// 覆盖右下角子棋盘
if(dr>=tr+s && dc>=tc+s) // 特殊方格在此棋盘中
chessBoard(tr+s,tc+s,dr,dc,s);
else{ // 此棋盘中无特殊方格
// 用t号L型骨牌覆盖右下角
board[tr+s][tc+s]=t;
// 覆盖其余方格
chessBoard(tr+s,tc+s,tr+s,tc+s,s);
}
}
void main(){
int k;
printf("请输入k值,棋盘的大小为2的k次方:");
scanf("%d",&k);
int size=1<<k;
int x,y; // (x,y)为特殊方格的下标
scanf("%d%d",&x,&y);
Board[x][y]=0; // 初始特殊方格标号为0
TileBoard(0,0,x,y,size);
for(int i=0;i<size;i+){
for(int j=0;k<size;j++)
printf("%4d",board[i][j]);
printf("\n");
}
}
2.2.7 *芯片测试
测试方法:将2片芯片(A和B)至于测试台上,互相进行测试没测试报告是“好”或“坏”,只取其一。好芯片的报告一定是正确的,坏芯片的报告是不确定的(可能会出错)。
输入:n片芯片,其中好芯片至少比坏芯片多1片。
问题:设计一种测试方法,通过测试从n片芯片中跳出1片好芯片。
要求:使用最少的测试次数。
判定芯片A的好坏
用其他n-1片芯片对A测试。
n是奇数:好芯片数
≥
(
n
+
1
)
/
2
\ge(n+1)/2
≥(n+1)/2 .
A好,至少有
(
n
−
1
)
/
2
(n-1)/2
(n−1)/2 个报“好”;
A坏,至少有
(
n
+
1
)
/
2
(n+1)/2
(n+1)/2 个报“坏”.
n是偶数:好芯片数
≥
n
/
2
+
1
\ge n/2+1
≥n/2+1 .
A好,至少有
n
/
2
n/2
n/2 个报“好”;
A坏,至少有
n
/
2
+
1
n/2+1
n/2+1 个报“坏”.
结论:至少一半报“好”,则A为好芯片;超过一半报“坏”,则A为坏芯片。
假设n为偶数,将n片芯片两两一组做测试淘汰,剩下芯片构成子问题,进入下一轮分组淘汰。
截止条件:
n
≤
3
n\le3
n≤3 ,【3片芯片,1次测试得到好芯片;1或2片芯片,不在需要测试】
// 伪码
Test(int n){
int k=n;
while(k>3){
for(int i=1;i<k/2;k++){
if(2片好) 任取1片留下;
else 2片同时丢掉;
}
k=剩下的芯片数;
}
if(k==3){
任取2片芯片测试;
if(1好1坏) 取没测的芯片;
else 任取1片被测芯片;
}
if(k==2||k==1) 任取1片;
}
算法复杂度
W ( n ) = { W ( n 2 ) + O ( n ) , n > 3 1 , n = 3 0 , n = 2 或 n = 1 W(n)=\begin{cases} W(\frac{n}{2})+O(n)\ ,\ n\gt3\\ 1\ ,\ n=3\\ 0\ ,\ n=2\ 或\ n=1 \end{cases} W(n)=⎩ ⎨ ⎧W(2n)+O(n) , n>31 , n=30 , n=2 或 n=1
即 W ( n ) = O ( n ) W(n)=O(n) W(n)=O(n)
2.2.8 *幂乘算法及应用
输入:a 为给定实数,n 为自然数。
输出:
a
n
a^n
an
a
n
=
{
a
n
2
×
a
n
2
,
n
为偶数
a
n
−
1
2
×
a
n
−
1
2
×
a
,
n
为奇数
a^n=\begin{cases} a^\frac{n}{2}×a^\frac{n}{2}\ ,\ n为偶数\\ a^\frac{n-1}{2}×a^\frac{n-1}{2}×a\ ,\ n为奇数 \end{cases}
an={a2n×a2n , n为偶数a2n−1×a2n−1×a , n为奇数
可得
W
(
n
)
=
W
(
n
2
)
+
Θ
(
1
)
W(n)=W(\frac{n}{2})+\Theta(1)
W(n)=W(2n)+Θ(1) ,解得
W
(
n
)
=
Θ
(
log
n
)
W(n)=\Theta(\log n)
W(n)=Θ(logn) .
应用 计算Fibonacci数
设
{
F
n
}
\{F_n\}
{Fn} 为 Fibonacci 数构成的数列,那么
[
F
n
+
1
F
n
F
n
F
n
−
1
]
=
[
1
1
1
0
]
n
\left[ \begin{matrix} F_{n+1} & F_n\\ F_n & F_{n-1} \end{matrix} \right] =\left[ \begin{matrix} 1 & 1\\ 1 & 0 \end{matrix} \right]^n
[Fn+1FnFnFn−1]=[1110]n
那么
[
F
n
+
2
F
n
+
1
F
n
+
1
F
n
]
=
[
F
n
+
1
F
n
F
n
F
n
−
1
]
[
1
1
1
0
]
=
[
1
1
1
0
]
n
[
1
1
1
0
]
=
[
1
1
1
0
]
n
+
1
\begin{aligned} \left[ \begin{matrix} F_{n+2} & F_{n+1}\\ F_{n+1} & F_{n} \end{matrix} \right] &= \left[ \begin{matrix} F_{n+1} & F_n\\ F_n & F_{n-1} \end{matrix} \right] \left[ \begin{matrix} 1 & 1\\ 1 & 0 \end{matrix} \right]\\ &= \left[ \begin{matrix} 1 & 1\\ 1 & 0 \end{matrix} \right]^n \left[ \begin{matrix} 1 & 1\\ 1 & 0 \end{matrix} \right] =\left[ \begin{matrix} 1 & 1\\ 1 & 0 \end{matrix} \right]^{n+1} \end{aligned}
[Fn+2Fn+1Fn+1Fn]=[Fn+1FnFnFn−1][1110]=[1110]n[1110]=[1110]n+1
用幂乘计算n次矩阵乘法,则时间复杂度为
T
(
n
)
=
Θ
(
log
n
)
T(n)=\Theta(\log n)
T(n)=Θ(logn) .
2.2.9 平面点对问题
输入:平面点集P有n个点,
n
≥
1
n\ge1
n≥1
输出:P中的两个点,其距离最小
蛮力法
C(n,2)个点对,计算最小距离,
O
(
n
2
)
O(n^2)
O(n2) .
分治策略 P划为大小相等的
P
L
P_L
PL 和
P
R
P_R
PR
1、分别计算
P
L
P_L
PL 、
P
R
P_R
PR 中最近点对
2、计算
P
L
P_L
PL 与
P
R
P_R
PR 中各一个点的最近点对
3、上述情况下的最近点对是解
// 伪码
// 输入:点集P、X和Y为横、纵坐标数组
// 输出:最近的两个点及距离
MinDistance(P,X,Y){
若|p|<=3,直接计算其最小距离; // O(1)
排序X,Y; // O(nlogn)
做中垂线l将P划分为PL和PR; // O(1)
MinDidtance(PL,XL,YL); // 2T(n/2)
MinDidtance(PR,XR,YR);
d=min(dL,dR); //dL、dR为子问题的距离 // O(1)
检查距l不超过d两侧各一个点的距离,若小于d,修改d为这个值; // O(n)
}
跨边界处理
算法复杂度
T ( n ) = { O ( 1 ) , n ≤ 3 2 T ( n 2 ) + O ( n l o g n ) , n > 3 T(n)=\begin{cases} O(1)\ ,\ n\le3\\ \\ 2T(\frac{n}{2})+O(nlogn)\ ,\ n\gt3 \end{cases} T(n)=⎩ ⎨ ⎧O(1) , n≤32T(2n)+O(nlogn) , n>3
解得 T ( n ) = O ( n l o g 2 n ) T(n)=O(nlog^2n) T(n)=O(nlog2n) .
增加预处理
1、在递归前对X,Y排序,作为预处理
2、划分时对排序的数组X,Y进行拆分,得到针对子问题
P
L
P_L
PL 的数组
X
L
,
Y
L
X_L,Y_L
XL,YL 及针对子问题
P
R
P_R
PR 的数组
X
R
,
Y
R
X_R,Y_R
XR,YR .
算法复杂度
W ( n ) = T ( n ) + O ( n l o g n ) T ( n ) = { O ( 1 ) , n ≤ 3 2 T ( n 2 ) + O ( n ) , n > 3 \begin{aligned} W(n)&=T(n)+O(nlogn)\\ T(n)&=\begin{cases} O(1)\ ,\ n\le3\\ \\ 2T(\frac{n}{2})+O(n)\ ,\ n\gt3 \end{cases} \end{aligned} W(n)T(n)=T(n)+O(nlogn)=⎩ ⎨ ⎧O(1) , n≤32T(2n)+O(n) , n>3
解得 T ( n ) = O ( n l o g n ) , W ( n ) = O ( n l o g n ) T(n)=O(nlogn),W(n)=O(nlogn) T(n)=O(nlogn),W(n)=O(nlogn) .
2.2.10 *选最大与最小
顺序比较
最坏情况下,
W
(
n
)
=
n
−
1
+
n
−
2
=
2
n
−
3
W(n)=n-1+n-2=2n-3
W(n)=n−1+n−2=2n−3 .
分组算法
// 伪码
// 输入:n个数的数组L
// 输出:max,min
FindMaxMin(int *L){
将n个元素两两分成n/2组(多余轮空);
每组比较得到n/2个较小和较大; // O(n/2)
在n/2个较大(含轮空元素)中找max; // O(2(n/2-1))
在n/2个较小(含轮空元素)中找min;
return max,min;
}
算法复杂度
W ( n ) = ⌊ n 2 ⌋ + 2 ⌈ n 2 ⌉ − 2 = n + ⌈ n 2 ⌉ − 2 = ⌈ 3 n 2 ⌉ − 2 \begin{aligned} W(n)&=\lfloor {\frac{n}{2}} \rfloor+2\lceil{\frac{n}{2}}\rceil-2\\ &=n+\lceil{\frac{n}{2}}\rceil-2\\ &=\lceil{\frac{3n}{2}}\rceil-2 \end{aligned} W(n)=⌊2n⌋+2⌈2n⌉−2=n+⌈2n⌉−2=⌈23n⌉−2
分治算法
将数组L从中间划分为两个子数组L1和L2
递归地在L1中球最大max1和最小min1
递归地在L2中球最大max2和最小min2
max=max{max1,max2}
min=min{min1,min2}
算法复杂度
令 n = 2 k W ( n ) = 2 W ( n 2 ) + 3 W ( 2 ) = 1 则 W ( 2 k ) = 2 W ( 2 k − 1 ) + 2 = 2 ( W ( 2 k − 2 ) + 2 ) + 2 = 2 2 W ( 2 k − 2 ) + 2 2 + 2 = 2 k − 1 + 2 k − 2 + . . . + 2 2 + 2 = 3 ⋅ 2 k − 1 − 2 = 3 n 2 − 2 \begin{aligned} &令n=2^k\\ &W(n)=2W(\frac{n}{2})+3\\ &W(2)=1\\ &则\ W(2^k)=2W(2^{k-1})+2\\ &\ \ \ =2(W(2^{k-2})+2)+2\\ &\ \ \ =2^2W(2^{k-2})+2^2+2\\ &\ \ \ =2^{k-1}+2^{k-2}+...+2^2+2\\ &\ \ \ =3·2^{k-1}-2=\frac{3n}{2}-2\\ \end{aligned} 令n=2kW(n)=2W(2n)+3W(2)=1则 W(2k)=2W(2k−1)+2 =2(W(2k−2)+2)+2 =22W(2k−2)+22+2 =2k−1+2k−2+...+22+2 =3⋅2k−1−2=23n−2
2.2.11 *选第二大
输入:n个数的数组L
输出:第二大的数second
顺序比较
先找到最大max,再从剩下的n-1个数中找到第二大second
算法复杂度
W ( n ) = n − 1 + n − 2 = 2 n − 3 W(n)=n-1+n-2=2n-3 W(n)=n−1+n−2=2n−3
锦标赛算法
- 两两分组比较,大者进入下一轮,直到剩下1个元素max为止
- 在每次比较中淘汰较小元素,将被淘汰元素记录在淘汰它的元素的链表上
- 检查max的链表,从中找到最大元,即second
// 伪码
// 输入:n个数的数组L
// 输出;second
FindSecond(int *L){
k=n; // 参与淘汰的元素数
a:将k个元素两两一组,分成k/2组;
每组的2个数比较,找到较大数;
将被淘汰数计入较大数的链表;
if(k%2==1) k=k/2+1;
else k=k/2;
if(k>1) goto a;
max=最大数;
second=max的链表中的最大;
return second;
}
算法复杂度
W ( n ) = n − 1 + ⌈ log n ⌉ − 1 = n + ⌈ log n ⌉ − 2 W(n)=n-1+\lceil{\log n}\rceil-1=n+\lceil{\log n}\rceil-2 W(n)=n−1+⌈logn⌉−1=n+⌈logn⌉−2
2.2.12 *一般选择问题(选第k小)
输入:n个数的数组L,正整数k
输出:第k大的数
简短算法
- 调用k次选最小算法,时间复杂度 O ( k n ) O(kn) O(kn)
- 先排序,然后选出第k大的数,时间复杂度 O ( n log n ) O(n\log n) O(nlogn)
分治算法
假设元素彼此不等,
- 用某个元素m*,作为标准将S划分成 S 1 S_1 S1 与 S 2 S_2 S2 ,其中 S 1 S_1 S1 的元素小于m*, S 2 S_2 S2 的元素大于等于m*
- 如果
k
≤
∣
S
1
∣
k\le|S_1|
k≤∣S1∣ ,则在
S
1
S_1
S1 中找第
k
k
k 小
如果 k = ∣ S 1 ∣ + 1 k=|S_1|+1 k=∣S1∣+1 ,则在m*是第 k k k 小
如果 k > ∣ S 1 ∣ k\gt|S_1| k>∣S1∣ ,则在 S 2 S_2 S2 中找第 k − ∣ S 1 ∣ − 1 k-|S_1|-1 k−∣S1∣−1 小
// 伪码
Select(int *S,int k){
将S分成5个一组,共(n/5+1)组;
每组排序,中位数放到集合M;
m=Select(M,|M|/2+1); //S分A,B,C,D
A,D元素小于m放S1,大于m放S2;
S1=S1+C;
S2=S2+B;
if(k==|S1|+1) return m;
else if(k<=|S1|) return Select(S1,k);
else return Select(S2,k-|S1|-1);
}
算法复杂度
2.2.13 *平面点集的凸包
问题
给定大量离散点的集合Q,求一个最小的凸多边形,使得Q中的点在该多边形内或者边上。
应用背景
图形处理中用于形状识别:字形识别、碰撞检测等。
分治算法
- 以连接最大纵坐标点 y m a x y_{max} ymax 和最小纵坐标点 y m i n y_{min} ymin 的线段 d = { y m a x , y m i n } d=\{y_{max},y_{min}\} d={ymax,ymin} 划分 Q Q Q 为左点集 Q l e f t Q_{left} Qleft 和右点集 Q r i g h t Q_{right} Qright .
- 分别考虑
Q
l
e
f
t
Q_{left}
Qleft 和
Q
r
i
g
h
t
Q_{right}
Qright :确定距离
d
d
d 最远的点
P
P
P
P P P 、 y m a x y_{max} ymax 和 y m i n y_{min} ymin 构成的三角形中的点,删除;
P P P 和 y m a x y_{max} ymax 构成的线段 a a a 之外的点和 a a a 构成子问题;
P P P 和 y m i n y_{min} ymin 构成的线段 b b b 之外的点和 b b b 构成子问题.
// 伪码
Deal(int *Q){
以d和距离d最远的点P构成三角形,P加入凸包,另外两条边分别记作a和b;
检查Q中其他店是否在三角形内,若在则从Q中删除,否则根据在a或b边的外侧划分在两个子问题中;
Deal(a);Deal(b);
}
算法复杂度
2.2.14 循环赛日程表
设计一个满足以下要求的比赛日程表:
(1)每个选手必须与其他n-1个选手各赛一次;
(2)每个选手一天只能赛一次;
(3)循环赛一共进行n-1天.
分治策略
将所有的选手分为两半,n个选手的比赛日程表就可以通过为n/2个选手设计的比赛日程表来决定。
递归地用对选手进行分割,直到只剩下2个选手时,只要让这2个选手进行比赛就可以了。
2.3 改进分治算法的途经
2.3.1 减少子问题数
分治算法的时间复杂度方程
W
(
n
)
=
a
W
(
n
b
)
+
d
(
n
)
W(n)=aW(\frac{n}{b})+d(n)
W(n)=aW(bn)+d(n) .
a:子问题数 ,
n
b
\frac{n}{b}
bn :子问题规模 ,d(n):划分与综合工作量
当 a 较大,b 较小,d(n) 不大时,方程的解:
W
(
n
)
=
Θ
(
n
log
b
a
)
W(n)=\Theta(n^{\log_ba})
W(n)=Θ(nlogba) .
例子:整数位乘问题、矩阵相乘问题
2.3.2 增加预处理
例子:平面点对问题