参考摘自:计算机算法设计与分析(第五版)王晓东
一、简介
分治法的设计思想:将一个难以解决的大问题分割成一些规模较小的相同问题,分而治之。如果原来n个问题可以分成k个子问题,且1<k<n,且这些子问题都可解,并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。
分治与递归经常同时应用在算法设计中,并由此产生许多高效算法。
二、递归的概念
递归算法:直接或间接地调用自身的算法称为递归算法。
1、阶乘函数
递归方程:
改方程的自变量n的定义域是非负整数。递归式的第一式给出了这个函数的初始值,是非递归定义的。每个递归函数都必须有非递归定义的初始值,否则递归函数无法计算。递归式的第二式用较小的自变量的函数值来表示较大自变量的函数值的方式来定义n的阶乘。
int factorial(int n) {
if(n == 0)
return 1;
return n*factorial(n-1);
}
非递归定义:n! = 1*2*3*...*(n-1)*n
2、斐波那契数列
无穷数列1,1,2,3,5,8,13...称为斐波那契数列。
递归方程:
int fibonacci(int n) {
if(n <= 1)
return 1;
return fibonacci(n-1) + fibonacci(n-2);
}
非递归定义:
3、Ackerman函数
并非一切递归函数都能用非递归方式定义。Ackerman函数。当一个函数及它的一个变量由函数自身定义时,称这个函数时双递归函数。
Ackerman函数A(n,m)有两个独立的整型变量m>=0和n>=0,其定义如下:

A(n,m)的自变量m的每个值都定义了一个单变量函数。m=0定义了函数“加2”。当m=1时,由于A(1,1) = A(A(0,1),0) = A(1,0) = 2 且 A(n,1) = A(A(n-1,1),0) = A(n-1,1)+2,因此A(n,1)=2n (n >=1),即A(n,1)是函数“乘2”。当m=2时,A(1,2) = A(A(0,2),1) = A(1,1) = A(A(0,1),0) = A(1,0) = 2 且 A(n,2) = A(A(n-1,2),1) = 2*A(n-1,2),故A(n,2) = 2^n。
类似的,我们也可以推出A(n,3)。A(n,4)的增长速度非常快,以至于没有适当的数学式子来表示这一函数。所以Ackerman函数就没有非递归定义。
4、排列问题
设R = {r1,r2,...,rn}是要进行排列的n个元素,Ri = R - {ri}。集合X中元素的全排列记为Perm(X)。(ri)Perm(X)表示在全排列Perm(X)的每个排列前加上前缀ri得到的排列。R的全排列可归纳定义如下:
当n=1时,Perm(R) = (r),其中r是集合R中唯一的元素;
当n>1时,Perm(R)由(r1)Perm(R1),(r2)Perm(R2),...,(rn)Perm(Rn)构成。
依此递归定义,可设计产生Perm(R)的递归算法如下:
template<class Type>
void Perm(Type list[], int k, int m)
//产生list[k:m]的所有排列
{
if(k==m) // 只剩下1个元素
{
for(int i=0; i<m; i++)
{
cout << list[i];
}
cout << endl;
}
else // 还有多个元素待排列,递归产生排列
{
for(int i=k; i<=m; i++)
{
Swap(list[k], list[i]); // 所有元素依此被放到第一个位置
Perm(list, k+1, m);
Swap(list[k], list[i]); // 换回来
}
}
}
template<class Type>
inline void Swap(Type & a, Type & b) {
Type temp = a;
a = b;
b = temp;
}
在一般情况下,k<m。算法将list[k:m]中的每个元素分别于list[k]中的元素交换,然后递归地计算list[k+1:m]的全排列,并将计算结果作为list[0:k]的后缀。算法中Swap()适用于交换两个变量值的内联函数。
5、整数划分问题
将正整数n表示成一系列正整数之和,n=n1+n2+...+nk(n1>=n2>=...>=nk>=1,k>=1)。正整数n的不同的划分个数成为正整数n的划分数记为p(n)。
例如,p(6) = 11。
递归式: 
int q(int n, int m) {
if((n<1) || (m<1))
return 0;
if((n==1) || (m==1))
return 1;
if(n<m)
return q(n,n);
if(n==m)
return q(n,m-1)+1;
return q(n,m-1) + q(n-m,m);
}
正整数n的划分数p(n) = q(n,n)。
6、Hanoi塔问题

设a、b、c是三个塔座。开始时,在塔座a上有一叠共n个圆盘,这些圆盘自下而上,由大到小地叠放在一起,各圆盘从小到大编号为1,2,…,n,如图所示。现要求将塔座a上的这一叠圆盘移到塔座b上,并仍按同样顺序叠置。在移动圆盘时应遵守以下移动规则:
(1)每次只能移动一个圆盘;
(2)任何时刻都不允许将较大的圆盘压在较小的圆盘之上;
(3)在满足移动规则I和Ⅱ的前提下,可将圆盘移至a、b、c中任一塔座上。
当n=1时,问题比较简单。此时,只要将编号为1的圆盘从塔座a直接移至塔座b上即可。当n>1时,需要利用塔座c作为辅助塔座。此时要设法将n-1个较小的圆盘依照移动规则从塔座a移至塔座c上,然后将剩下的最大圆盘从塔座a移至塔座b上,最后设法将n-1个较小的圆盘依照移动规则从塔座c移至塔座b上。由此可见,n个圆盘的移动问题就可分解为两次n-1个圆盘的移动问题,这又可以递归地用上述方法来做。由此可以设计出解Hanoi塔问题的递归算法如下:
void hanoi(int n, int a, int b, int c)
{
if(n > 0)
{
hanoi(n-1,a,c,b);// 先把n-1个盘移动到c上
move(a,b);// 把最大的盘移到b上
hanoi(n-1,c,b,a);//再把c上的n-1个盘移到b上
}
}
三、分治策略设计范例
1、二分搜索技术
template<class Type>
int BinarySearch(Type a[], const Type& x, int n) // a是从小到大已经排好序的数列,搜索x
{
//找到x时返回其在数组中的位置,否则返回-1
int left = 0;
int right = n-1;
while(left<=right)
{
int middle = (left + right)/2;
if(x == a[middle]) // 找到了
return middle;
if(x > a[middle]) // 在右半部
left = middle+1;
else // 在左半部
right = middle-1;
}
return -1; // 未找到
}
2、棋盘覆盖
在一个2^k×2^k个方格组成的棋盘中,若恰有一个方格与其他方格不同,则称该方格为特殊方格,且称该棋盘为一特殊棋盘。显然,特殊方格在棋盘上出现的位置有4^k种情形。因而对任何k≥0,有4^k种特殊棋盘。图中的特殊棋盘是k=2时16个特殊棋盘中的一个。



void ChessBoard(int tr, int tc, int dr, int dc, int size)
// tr:左上角格子行号,tc:左上角格子列号
// dr:特殊方格行号,dc:特殊方格列号
// size:2^k
// Board:二位整型数组,表示棋盘
// tile:全局变量,表示L型骨牌编号,起初始值是0
{
if(size == 1) // 直至棋盘简化为1*1的棋盘
return;
int t = tile++ , s = size/2; // 分割棋盘
// 覆盖左上角子棋盘
if(dr < tr+s && dc < tc+s) // 特殊方格在此棋盘中
ChessBoard(tr, tc, dr, dc, s); // 递归
else { // 特殊方格不在此棋盘中
Board[tr+s-1][tc+s-1] = t; // 用t号L型骨牌覆盖右下角
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 {
Board[tr+s-1][tc+s] = t; // 用t号L型骨牌覆盖左下角
ChessBoard(tr, tr+s, tr+s-1, tc+s, s); // 递归
}
// 覆盖左下角棋盘
if(dr >= tr+s && dc < tc+s) // 特殊方格在此棋盘中
ChessBoard(tr+s, tc, dr, dc, s); // 递归
else {
Board[tr+s][tc+s-1] = t; // 用t号L型骨牌覆盖右上角
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 {
Board[tr+s][tc+s] = t; // 用t号L型骨牌覆盖左上角
ChessBoard(tr+s, tc+s, tr+s, tc+s, s); // 递归
}
}
解得T(k) = O(4^k)
3、合并排序
template<class Type>
void MergeSort(Type a[], int left, int right)
{
if(left < right) {
int i = (left+right)/2; // 取中点
MergeSort(a, left, i); // 排左边
MergeSort(a, i+1, right); // 排右边
MergeSort(a, b, left, i, right); // 合并到数组b
Copy(a, b, left, right); // 复制回数组a
}
}
解得T(n) = O(nlogn)
template<class Type>
void MergeSort(Type a[], int n)
{
Type *b = new Type [n]; // 临时存储中间排序结果
int s = 1; // 当前归并的子序列长度
while(s < n) // s>=n时,整个数组变成一个有序序列
{
//MergePass:归并函数,负责将长度为s的子序列两两合并成更长的有序子序列。
MergePass(a,b,s,n); // 将数组a中的有序子序列两两归并到b
s+=s;
MergePass(b,a,s,n); // 将b中的有序子序列两两归并回a
s+=s;
}
}
特别地,如果Type是自定义的,则必须重载运算“<=”。
4、快速排序
快速排序算法基本思想是,对于输入的子数组a[p:r],按以下三个步骤进行排序。
①分解:以a[p]为基准元素将a[p:r]划分成3段a[p:q-1],a[q]和a[q+1:r],使a[p:q-1]中任何一个元素小于等于a[q],而a[q+1:r]中任何一个元素大于等于a[q]。下标q在划分过程中确定。
②递归求解:通过递归调用快速排序算法,分别对a[p:q-1]和a[q+1:r]进行排序。
③合并:由于对a[p:q-1]和a[q+1:r]的排序是就地进行的,因此在a[p:q-1]和a[q+1:r]都已排好的序后,不需要执行任何计算,a[p:r]则已排好序。
int Partition(Type a[], int p, int r)
{
int i = p, j = r+1;
Type x = a[p]; // 选择数组的第一个元素作为基准值。
// 将小于x的元素交换到左边区域,将大于x的元素交换到右边区域
while(true) {
while(a[++i] < x && i < r); // 从左向右找到第一个大于等于基准值的元素
while(a[--j] > x); // 从右向左找到第一个小于等于基准值的元素
if(i >= j)
break; // 左右两部分已经完全划分,跳出循环。
Swap(a[i], a[j])
}
// 基准值归位
a[p] = a[j];
a[j] = x;
return j;
}
void QuickSort(Type a[], int p, int r)
{
if(p < r)
{
int q = Partition(a, p, r);
Quicksort(a, p, q-1); // 对左半段排序
Quicksort(a, q+1, r); // 对右半段排序
}
}
快速算法在平均情况下的时间复杂度也是O(logn)。
5、循环赛日程表
设有n=2*个运动员要进行网球循环赛。现要设计一个满足以下要求的比赛日程表:
①每个选手必须与其他n-1个选手各赛一次;
②每个选手一天只能赛一次;
③循环赛一共进行n-1天。
按此要求可将比赛日程表设计成有n行和n-1列的表。在表中第i行和第j列处填入第i个选手在第j天所遇到的选手。
按分治策略,可以将所有选手对分为两半,n个选手的比赛日程表就可以通过为n/2个选手设计的比赛日程表来决定。递归地用这种一分为二的策略对选手进行分割,直到只剩下两个选手时,比赛日程表的制定就变得简单了。这时只要让这两个选手进行比赛就可以了。

被折叠的 条评论
为什么被折叠?



