张三木教你理解分治法
分治策略(Divide and Conquer)
- 将原始问题划分或者归结为规模较小的子问题
- 递归或者迭代求解每个子问题
- 将子问题的解综合得到原问题的解
注意:
- 子问题和原始问题性质完全一样
- 子问题之间可以彼此独立的求解
- 递归停止时子问题可以直接求解
分治算法设计模式的一般描述
devide-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);
};
rerturn merge(y1,y2,.....,yk);
}
其中,|p|表示问题P的规模;n0为不需要再分解问题的阈值;adhoc是基本子算法,可以直接求解;merge用于将子问题的解合并为原问题的解。
分治策略的特点
- 将原问题规约为规模小的子问题,子问题与原问题具有相同的性质
- 子问题规模足够小时,可以直接求解
- 子问题的解综合得到原问题的解
- 算法可以递归,也可以迭代实现
- 算法的分析方法:递推方程
- 算法实现:递归或者迭代
分治算法的时间分析
用主定理求解型为T(n)=aT(n/b)+O(n^d)的递归方程
设a>=1和b>1是常数,f(n)是一个函数,T(n)是定义在非负整数集上的函数:T(n)=aT(n/b)+ O(n^d) 。
即有:
分治法改进的方法
- 减少子问题个数
- 增加预处理
例子
二分查找
二分查找的设计思想:
- 通过x和a[mid]的比较。将原问题归结为规模减半的子问题。
- 对子问题进行二分检索
- 当子问题规模为1的时候,直接比较x与a[mid],若相等则返回m
- 时间复杂度:O(logN)
/*
*二分查找(数组实现)
*@param {array} a 需要查找的已排好序的数组
*@param {int} low 数组的最低下标
*@param {int} high 数组的最高下标
*@param {int} x 被查找的数
*@return {array} a 排好序的数组
*/
void binarySearch(int a[] , int low , int high , int x ){
if(low > high){
return ;
};
int mid = (low+high)/2;
if(a[mid] == x){
return mid;
};
if(a[mid] > x){
return binarySearch(a , low , mid , x );
}else if( a[mid] < x ){
return binarySearch(a , mid , high , x );
};
}
二分归并排序
二分归并排序设计思想:
- 划分将原问题归结为规模为n/2的2个子问题
- 继续划分,将原问题归结为规模为n/4的4个子问题。继续....,当子问题的规模为1时,划分结束。
- 从规模1到n/2,陆续归并被排好序的两个数组。每归并一次,数组规模扩大一倍,直到原始数组。
- 时间复杂度分析:O(n)=nlogn
/*
*二分归并数组排序
*
*/
void merge ( int a[ ], int low , int mid , int high ){
int b[1000];
int i = low;
int j = mid;
int k = 0;
while(i <= mid && j<=high ){
if( a[i] > a[j]){
b[k]=a[j];
j++;
k++;
} else {
b[k]=a[i];
i++;
k++;
};
if(i <= m){
for( int p = i; p<=m ;p++){
b[k++]=a[p];
};
} else {
for(int p=j;p<=high;p++){
b[k++]=a[p];
}
}
}
for(int p=low;p<=high;p++){
a[p]=b[p-low];
};
/*
*
*@param {array} a 乱序数组
*@param {int} low 数组的最低下标
*@param {int} high 数组的最高下标
*/
void MergeSort( int a[ ], int low , int high ) {
if(low<high){
int mid = (low+high)/2; //对半划分
MergeSort(a , low , mid); //子问题一
MergeSort(a, mid+1, high); //子问题二
Merge(a,low,mid,high);
}
}
快速排序
用首元素x作为划分标准,将输入数组A划分成不超过x的元素构成的数组Al,大于x的元素构成的数组Ar。其中Al和Ar从左到右存放在数组A的位置。
递归地对子问题Al和Ar进行排序,直到子问题规模为1。
int partition(int a[], int left, int right) {
int x = a[left];
while (left < right) {
while (left < right&&a[right] >= x) {
right--;
};
a[left] = a[right];
while (left < right&&a[left] <= x) {
left++;
};
a[right] = a[left];
};
a[left] = x ;
return left ;
}
void QSort(int a[],int low,int high){
if(low<high){
pivotloc=partition(a,low,high);
QSort(L,low,pivotloc-1);
QSort(L,pivotloc+1,high);
}
}
void QuickSort(int a,int n){
QSort(a,0,n-1);
}
小总结
分治法(尤其是二分法)非常适合用于存在某一个数并且查找该目标数的题目,同时也注意几个点:
- 边界值的等号是否需要考虑
- 中间值是否需要去掉
- 二分后的对象是否与原对象性质一样。
简单来说,分治法的设计思想就是:将一个大的问题分解为性质相同规模较小的小问题,逐个击破,分而治之。
参考:
《计算机算法设计与分析》(第五版)王晓东著
郑琪老师提供的PPT
如有错漏,恳请指出。
结对编程感想:
本次结对编程实验中,我们一开始就遇到了难题(没有理解题意导致A不过去),我们两个都写出了自己的实现方法,在这上面花了很多时间。
后来知道错误之处后,答题就顺利了很多。结对编程可以互相找出对方的不足之处,也可以齐心协力想出一个更好的算法。队友逻辑思维十分清晰,我也在此次上机中学到了很多东西。