参考书籍:算法设计与分析——C++语言描述(第二版)
算法设计策略-分治法
分治法
分治法的基本思想
分治法就是分而治之,一个问题能够用分治法求解的要素是:第一,问题能够按照某种方法分解成若干个规模较小,相互独立且与原问题类型相同的子问题;第二,子问题足够小时可以直接求解;第三,能够将子问题的解组合成原问题的解。
由于分治法要求分解成同类子问题,并允许不断分解,使问题的规模逐步减小,最终可用已知的方法求解足够小的问题,因此分治法求解很自然导致一个递归算法。
分治法求解框架:
SolutionType DandC(ProblemType P)
{
ProblemType P1,P2,...,Pk;
if(Small(P)) //子问题P足够小,用S(P)直接求解
return S(P);
else{
Divide(P,P1,P2,...,Pk); //将问题P分解成子问题P1,P2,...,Pk
return Combine(DandC(P1),DandC(P2),...,DandC(Pk)); //求解子问题,合并求解
}
}
其中Small(P)
是一个布尔值函数,它判定问题P是否足够小。Divide
函数以某种方式,将问题P分解成规模较小,相互独立的若干同类型子问题。Combine
函数将各子问题的解组合成原始问题的解。特殊的,一分为二的分治法:
SolutionType DandC(int left, int right)
{
if(Small(left, right))
return S(left,right);
else{
int m = Divide(left, right); //以m为界将问题分解成两个子问题
return Combine(DandC(left, m),DandC(m+1, right)); //分别求解子问题,合并求解
}
}
如果较大的问题可以分为几个同样大小的问题,那么往往可以得到以下的递推公式:
定理:设,,和为常数,,则
分治法求最大元最小元
问题描述
在一个元素集合中寻找最大元素和最小元素的问题,即在互不相同的个数中,找出最大和最小的数。
分治法求解
用分治法求最大最小元。可以将原问题分解成大小基本相等的两个子问题。显然在一个或两个元素的表中求最大、最小元是容易的,可以直接求得;如果已经求得了由分解所得的两个字表中的最大、最小元,则原表的最大元是两个字表中的最大元之较大者,原表的最小元是两子表中的最小元之较小者。
template <class T>
void SortableList<T>::MaxMin(int i, int j, T& max, T& min)const
//前置条件:i和j,0<=i<=j<表长,是表的下标范围的界
{
T min1, max1;
if(i == j){
max=min=l[i]; //表中只有一个元素
} else{ //表中有两个元素
if(i == j-1){
if(l[i] < l[j]){
max = l[j];
min = l[i];
} else{
max = l[i];
min = l[j];
}
} else{ //表中多于两个元素时
int m = (i+j)/2; //对半分割
MaxMin(i,m,max,min); //求前一半部分子表中的最大最小元素
MaxMin(m+1,j,max1,min1); //求后一半部分子表中的最大最小元素
if(max < max1){ //两子表最大元的大者为原表最大元
max = max1;
}
if(min > min1){ //两子表最小元的小者为原表最小元
min = min1;
}
}
}
}
使用归纳法可以证明算法的正确性。
用C语言实验如下:
//#include <stdio.h>
//
//int gcd(int m, int n)
//{
// //欧几里得辗转相除法
// //假定m<n
// if(m == 0) {
// return n;
// } else {
// int tmp;
// while(m>0) {
// tmp = n%m;
// n = m;
// m = tmp;
// }
// return n;
// }
//}
//
//int Rgcd(int m, int n)
//{
// //递归形式
// if(m == 0) {
// return n;
// } else {
// return Rgcd(n%m, m);
// }
//}
//
//int main()
//{
// int m, n, r1, r2;
// scanf("%d%d", &m, &n);
//
// if(n>m) {
// r1 = gcd(m,n);
// r2 = Rgcd(m,n);
// } else {
// r1 = gcd(n,m);
// r2 = Rgcd(n,m);
// }
//
// printf("GCD of m and n: %d
", r1);
// printf("GCD of m and n: %d
", r2);
//
//
// return 0;
//}
#include <stdio.h>
void MaxMin(int l[], int i, int j, int *max, int *min)
//前置条件:i和j,0<=i<=j<表长,是表的下标范围的界
{
int min1=l[i], max1=l[i];
if(i == j){
*max=*min=l[i]; //表中只有一个元素
} else{ //表中有两个元素
if(i == j-1){
if(l[i] < l[j]){
*max = l[j];
*min = l[i];
} else{
*max = l[i];
*min = l[j];
}
} else{ //表中多于两个元素时
int m = (i+j)/2; //对半分割
MaxMin(l,i,m,max,min); //求前一半部分子表中的最大最小元素
MaxMin(l,m+1,j,&max1,&min1); //求后一半部分子表中的最大最小元素
if(*max < max1){ //两子表最大元的大者为原表最大元
*max = max1;
}
if(*min > min1){ //两子表最小元的小者为原表最小元
*min = min1;
}
}
}
printf("i= %d; j= %d; Max = %d; Min = %d
", i, j, *max, *min);
}
int main()
{
int P[10] = {1, 3, 5, 2, 3 ,6 ,7, 1, 10, -11};
int max = 0, min = 0;
printf("array P:");
for(int i = 0; i<10; i++)
printf(" %d", P[i]);
printf("
");
MaxMin(P, 0, 9, &max, &min);
return 0;
}
实验结果:
array P: 1 3 5 2 3 6 7 1 10 -11
i= 0; j= 1; Max = 3; Min = 1
i= 2; j= 2; Max = 5; Min = 5
i= 0; j= 2; Max = 5; Min = 1
i= 3; j= 4; Max = 3; Min = 2
i= 0; j= 4; Max = 5; Min = 1
i= 5; j= 6; Max = 7; Min = 6
i= 7; j= 7; Max = 1; Min = 1
i= 5; j= 7; Max = 7; Min = 1
i= 8; j= 9; Max = 10; Min = -11
i= 5; j= 9; Max = 10; Min = -11
i= 0; j= 9; Max = 10; Min = -11
Press any key to continue.
时间分析
分治法求最大元最小元程序在最好、平均和最坏情况下的比较次数都为。
小结
分治法的思想:
对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。
分治法在每一层递归上都有三个步骤:
- 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
- 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;
- 合并:将各个子问题的解合并为原问题的解。
分治法所能解决的问题一般具有以下几个特征:
- 该问题的规模缩小到一定的程度就可以容易地解决;
- 因为问题的计算复杂性一般是随着问题规模的增加而增加,因此大部分问题满足这个特征
- 该问题可以分解为若干个规模较小的相同问题;
- 这条特征是应用分治法的前提,它也是大多数问题可以满足的,此特征反映了递归思想的应用
- 利用该问题分解出的子问题的解可以合并为该问题的解;
- 能否利用分治法完全取决于问题是否具有这条特征,如果具备了前两条特征,而不具备第三条特征,则可以考虑贪心算法或动态规划算法
- 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
- 这条特征涉及到分治法的效率,如果各子问题是不独立的,则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然也可用分治法,但一般用动态规划较好