分治策略
求解一个复杂问题,可以将其分解成若干个子问题,子问题可以进一步分解为更小的问题,直到所得的小问题可以分解为基本问题,并且其求解方法是已知的,可以直接求解为止。
分治法是一种设计算法策略,要求分解所得的子问题是同类问题,并且原问题的解可以通过组合子问题的解来获取。
一般方法
分治法的思想就是:分而治之。一个问题能够用分解法的要素为:
- 问题能够分解为相对规模较小、相互独立且与原问题类型相同的子问题
- 子问题足够小时可以直接求解
- 能够将子问题的解组合为原问题的解
- 由于分治法要求分解同类子问题,并允许不断分解,使问题规模逐渐减小,最终可以用已知的方法求解足够小的子问题故其很自然的就要使用递归算法。
分治策略的控制抽象;
- 分治法:
SolutionType DandC(ProblemType P) {
ProblemType P1, P2, P3, Pk;
if(Small(P)) {
}
return S(P);
else {
Divide(P, P1, P2, P3, Pk);
return Combine(DandC(P1), DandC(P2), DandC(Pk));
}
}
//Small()为一个布尔类型的函数判断P是否足够小
//Combine()函数将各子问题的解组合成原始问题的解
//Divide()函数以某种形式将问题P分解’
//S()函数针对足够小的问题进行求解
- 一分为二的分治法
SolutionType DandC(int left, int right) {
if(Small(left, right))
return S(left, right);
else {
int m = Divide(left, right);
return Combine(DandC(left, m), DandC(m + 1, right));
}
}
求最大最小元
运用分治法在一个元素集合中寻找最大元和最小元问题。
template<class>
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;
}
}
/*****
分析:
子问题:各个分组求最大最小
Combine:将各个分组的的最大最小进行比较
足够小:i == j 或着 i = j - 1
*****/
二分搜索
在表中搜索确定一个关键字值为给定的元素是一种是一种常见的运算。
用分治法求解
有一个长度为n的有序表,要求在表中搜索与给定元素x有相同关键字值的元素。若n=0,搜索失败;若n>0,则可将有序表分解成若干个子表。最简单的做法是分解成两个子表。假定以元素 a m a_{m} am为划分点。则将其与给定元素x进行比较。比较结果有三种情况:
- x < a m a_{m} am,在前半部分子表中寻找
- x = a m a_{m} am,搜索成功
- x > a m a_{m} am,在后半部分子表中搜索
二分搜索框架:
template<class T>
int SortableList<T>::BSearch(const T& x, int left, int right) const
{
if(left <= right) {
int m = Divide(left, right);
if(x < l[m) return BSearch(x, left, m-1);
else if(x > l[m]) return BSearch(x, m+1, right);
else return m;
}
return -1;
}
对半搜索
对半搜索是一种二分搜索。
设当前搜索子表为:
a
l
e
f
t
,
.
.
.
.
.
.
,
a
r
i
g
h
t
a_{left},......,a_{right}
aleft,......,aright,令
m
=
(
l
e
f
t
+
r
i
g
h
t
)
/
2
m = (left + right) / 2
m=(left+right)/2
这种二分搜索被称为对半搜索,将表划分成几乎等大小的两个子表
对半搜索递归算法:
template<class T>
int SortableList<T>::BSearch(const T& x, int left, int right) const
{
if(left <= right){
int m = (left+right)/2;
if(x < l[m])
return BSearch(x, left, m-1);
else if(x > l[m])
return BSearch(x, m+1, right);
else
return m;
}
return 1;
}
定理:对于 n ≥ 0 n \geq 0 n≥0,对半搜索递归函数是正确的。
对半搜索的迭代算法:
template<class T>
int SortacleList<T>::BSearch(const T& x)const
{
int m, left = 0, right = n - 1;
while(left <= right) {
m = (left+right)/2;
if(x < l[m])
right = m - 1;
else if(x > l[m])
left = m + 1;
else
return m
}
return -1;
}
二叉判定树
二分搜索时的算法行为可以用一棵二叉树来描述。
我们称这棵描述搜索算法执行过程的二叉树为二叉判定树。
略
排序问题
排序又称为分类。
分治法求解排序问题的思想很简单:按某种方式将序列分成两个或多个子序列,再将已排序的子序列合并成一个有序序列即可。
合并排序和快速排序是两种典型的符合分治策略的排序算法。
合并排序
merge sort 的基本运算就是把两个或者多个有序序列合并成一个有序序列,下面介绍最基本的合并算法;两路合并运算:
- 合并两个有序序列
实现这种合并的方法十分简单:比较两个有序序列的最小值,输出其中较小者,然后重复此过程,直到其中一个序列为空,如果另一个还有元素未输出则将剩余依次输出即可。
template<class T>
void SortableList<T>::Merge(int left, int mid, int right)
{
T* temp = new T[right - left + 1];
int i = left, j = mid+1, k = 0;
while((i <= mid) && (j <= right)){
if(l[i] <= l[j])
temp[k++] = l[i++];
else
temp[k++] = l[j++];
}
while(i <= mid)
temp[k++] = l[i++];
while(j <= right)
temp[k++] = l[j++];
for(i = 0, k = left;l<= right;)
l[k++] = temo[i++];
}
-
分治法求解
算法描述:将待排序的元素序列一分为二,得到两个长度基本相等的子序列,类似对半搜索的做法:然后最两个子序列分别排序,如果子序列较长,还可以继续细分,直到子序列的长度不超过1为止;当分解所得的子序列已排列有序时,可以采用上面的方法进行合并,实现将子问题的解组成成原问题的解。
分治法审视问题的视角:元素一分为二,分别进行排序,当序列为空或只有一个元素时别人为子问题足够小以至于可以直接解决。 -
合并排序算法
template<class T>
void SortableList<T>::MerageSort()
{
MergeSort(0, n-1);
}
template<class T>
void SortableList<T>::MerageSort(int left, int right)
{
if(left < right) {
int mid = (left+right)/2;
MerageSort(left, mid);
MerageSort(mid+1, right);
Merge(left, mid, left);
}
}
快速排序
quick sort 又被称为 分划交换排序。
采用一种特殊的分划操作对排序问题进行求解,其分解方法为:
- 在待排序的序列中( K 0 , K 1 , . . . K n − 1 K_0, K_1,...K_{n-1} K0,K1,...Kn−1)中选择一个元素作为分划元素,即主元。
- 不妨假定 K a K_a Ka为主元,经过一趟特殊的分划处理将原序列中的元素重新排列,值得以主元为轴心,将序列分为左右两个子序列。
- 使得主元左侧子序列中所有的元素都不大于主元,右侧都不小于。
- 在新的序列中 K a K_a Ka处于位置 j j j处,则新序列应满足上述条件。这样新序列就被分为了三部分:主元和左右两个子序列。 ( K 0 , . . . K j − 1 ) K j ( K j + 1 , . . . , K n − 1 ) (K_0,...K_{j-1})K_j(K_{j+1},...,K_{n-1}) (K0,...Kj−1)Kj(Kj+1,...,Kn−1)
分划:以主元为轴心,对一个序列按上述要求重新排列,并分解为两个子序列的国车过。
实质:分划就是对原序列排序问题分解成两个待解决的、性质相同的子问题。
快速排序中,使用分划操作将一个问题分解成两个相互独立的子问题。当子序列为一个元素或为空序列时我们认为其足够小。无需进行任何处理。
分治法要求分别求解子问题后,设法将子问题的解组合成原问题的解,这一点在快速排序下变得非常简单:由于划分操作,左边序列均不大于主元,右边序列均不小于主元。
合并排序 | 快速排序 |
---|---|
分治策略 | 分治策略} |
问题分解简单:一分为二 | 问题分解困难:分化操作 |
子问题解合并原问题解较难 | 子问题解合并原问题相对简单 |
分划操作:
快速排序的核心。
每趟划分选择序列中哪个元素是需要考虑的,最简单的做法就是选择序列中的第一个元素。
注:此图为以末尾元素为主元
算法要求在待排序序列尾部设置一个大值
∞
\infty
∞作为哨兵防止指针右移过程中移出序列之外,不能终止。这种情况当初始序列以递减次序列排序时就会发生。
template<class T>
int SortableList<T>::Partition(int left, int right)
{//前置条件:left <= right
int i = left;
int j = right + 1;
do{
do{
i++;
}while(l[i] < l[left]);
do{
j--;
}while(l[j] > l[left]);
if(i < j){
Swap(i,j); //交换两个元素l[i] 和 l[j]
}
}while(i < j);
Swap(left, j);
return j;
}
- 快速排序算法
调用分划函数,以主元为轴心分成两个子序列。然后分别用递归调用自身对这两个子序列实施快速排序,将他们排成有序序列。
template<class T>
void SortableList<T>::QuickSort(){
QuickSort(0, n-1);
}
template<class T>
void SortableList<T>::QuickSort(int left, int right)
{
if(left < right) {
int j = Partition(left, right);
QuickSort(left, j-1);
QuickSort(j+1,right);
}
}
- 改善快速排序算法
- 改进主元选取方法:一是选取 K ( l e f t + r i g h t ) / 2 K_{(left+right)/2} K(left+right)/2、二是选取 l e f t r i g h t left ~ right left right间的随机整数、三是选取 K l e f t K_{left} Kleft、 K ( l e f t + r i g h t ) / 2 K_{(left+right)/2} K(left+right)/2和 K r i g h t K_{right} Kright中的一个。
- 快速排序中子序列会变得越来越小,当子序列的长度小到一定程度时,快速排序的速度反而不如一些简单的排序,如直接插入法。
- 递归算法的效率往往不如非递归算法,可以设计非递归的排序算法:使用一个堆栈,在一次分划操作后,将其中一个子序列范围的上下界进栈保存,而对另一个序列继续进行分划排序。当对此子序列排序时,仍将分划的到的一个子序列的上下界进栈保存,对另一个子序列继续进行排序。直到足够小为止,再从栈中取出保存的某个尚未排序的子序列上下界,对该子序列进行快速排序。
选择问题
选择问题:是指在n个元素的集合中,选出某个元素值大小在集合中处于第k位的元素。
当k等于1时,是求最小元素;当k等于n时,是求最大元素;当k = (n+1)/2时,是求其中位数。
分治法求解
使用快速排序中的分划方法,以主元为基准,将一个表划分成左右两子表。
设原表的长度为
n
n
n,假定经过一趟划分,分成左右两个子表,其中左子表是主元及其左边元素,设其长度为
p
p
p。
那么,若k=p,则主元就是第k元素;
否则,若
k
<
p
k<p
k<p,第k小元素必定在左子表,需求解的问题的子问题为:在左子表中求第k小元素。
若
k
>
p
k>p
k>p,第k小元素必定在右子表中,所求解问题的子问题为:在右子表中求第
k
−
p
k-p
k−p小元素。
随机选择主元
假定表中的元素各不相同,并且随机选取主元,即在下标区间[left,right]中随机选取一个下标r,以该下标的元素为主元。
template<class T>
ResultCode SortableList<T>::Select(T& x, int k) {
if(n <= 0 || k > n || k <= 0) {
return OutOfBounds;
}
int left = 0,right = n;
l[n] = INFTY; //无穷大
do{
int j = rand()%(right - left + 1) + left; //随机选取主元
Swap(left, j); //将主元交换至left处
j = Partition(left, right); //划分操作并返回主元的位置j
if(k == j + 1){ // k与主元下标加1相等
x = l[j];
return Success;
}
else if(k < j + 1){
right = j; //此处为j而不是j-1
}
else{
left = j + 1;
}
}while(true);
}
该程序的平均时间复杂度是线性的,即 O ( n ) O(n) O(n)。
线性时间选择算法
通过精心挑选分划元素,可以使分划所得的两个子集合大小相近,从而避免最坏情况的发生,使得求解第k小元素的最坏情况具有线性时间复杂大 O ( n ) O(n) O(n)。
改进的算法使用二次取中法确定主元。
选择规则:
假定有n = 35个元素,每7个为一组,共5组。
设同组元素按递增次序排列。
先从每组中选取一个中间值,共5个中间值。
然后再从这5个中间值中求得中间值,设二次求得的中间值为mm将其与当前进行分划处理的子表中最左边元素进行交换,使mm为本趟分划的主元。
ResultCode SortableList<T>::Select(T &x, int k)
{
if(n <= 0 || k >= n || k <= 0){
return OutOfBounds;
}
int j = Select(k, 0, n-1, 5);
x = l[j];
return Success;
}
template<class T>
int SoratbleList<T>::Select(int k, int left, int right, int r)
{
int n = right - left + 1;
if(n <= r){
//若问题足够小,使用插入排序,取其中的第k小元素,其下标为left = k - 1
InsertSort(left, right;
return left + k - 1;
}
//二次取中规则求每组中间值,并将每组中间值存放在子表前部
for(int i = 1; j <= n/r; i++) {
InsertSort(left + (i-1)*r, left+r*i-1);
Swap(left + i - 1, left + (i-1)*r + Ceil(r,2) - 1 )
}
//求二次中间值,其下标为j
int j = Select( Ceil(n/r, 2), left, left + n/r - 1, r);
//二次中间值为主元将其放置表的首位
Swap(left, j);
//对子表进行分划操作放回主元的位置j
j = Partition(left, right);
if(k == j - left + 1)
return j;
else if(k < j - left + 1 {
return Select(k, left, j-1, r);
}
else
return Select(k - (j - left + 1), j+1, right, r);
}
斯特拉森矩阵乘法
分治法求解矩阵乘法
int j = Select( Ceil(n/r, 2), left, left + n/r - 1, r);
//二次中间值为主元将其放置表的首位
Swap(left, j);
//对子表进行分划操作放回主元的位置j
j = Partition(left, right);
if(k == j - left + 1)
return j;
else if(k < j - left + 1 {
return Select(k, left, j-1, r);
}
else
return Select(k - (j - left + 1), j+1, right, r);
}