递归与分治策略

递归与分治策略

文章目录

    • 递归与分治策略
  • 一、递归
    • 1.区别递归与循环
  • 二、分治
    • 1.基本思想
    • 2.适用情况
    • 2.基本步骤
    • 3.递推方程的求解方法
    • 4.算法复杂度分析
    • 三、排序问题
    • 1.合并排序
    • 2.快速排序
    • 四、经典递归分治问题
    • 1.平面最接近点对问题
    • 2.棋盘覆盖问题
    • 3.选择问题
    • 4.循环赛日程表
  • 总结


一、递归

1.区别递归与循环

递归:你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开它。若干次之后,你打开面前的门后,发现只有一间屋子,没有门了。然后,你开始原路返回,每走回一间屋子,你数一次,走到入口的时候,你可以回答出你到底用这你把钥匙打开了几扇门。

循环:你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门(若前面两扇门都一样,那么这扇门和前两扇门也一样;如果第二扇门比第一扇门小,那么这扇门也比第二扇门小,你继续打开这扇门,一直这样继续下去直到打开所有的门。但是,入口处的人始终等不到你回去告诉他答案。

二、分治

1.基本思想

分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。

分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。

如果原问题可分割成k个子问题,1

2.适用情况

分治法所能解决的问题一般具有以下几个特征:

1) 该问题的规模缩小到一定的程度就可以容易地解决

2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。

3) 利用该问题分解出的子问题的解可以合并为该问题的解;

4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。

第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;

第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、

第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。

第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。

2.基本步骤

分治法在每一层递归上都有三个步骤:

step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;

step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题

step3 合并:将各个子问题的解合并为原问题的解。

它的一般的算法设计模式如下:

Divide-and-Conquer(P)

1. if |P|≤n0

2. then return(ADHOC(P))

3. 将P分解为较小的子问题 P1 ,P2 ,...,Pk

4. for i←1 to k

5. do yi ← Divide-and-Conquer(Pi) △ 递归解决Pi

6. T ← MERGE(y1,y2,...,yk) △ 合并子问题

7. return(T)

其中|P|表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时直接用算法ADHOC(P)求解。算法MERGE(y1,y2,...,yk)是该分治法中的合并子算法,用于将P的子问题P1 ,P2 ,...,Pk的相应的解y1,y2,...,yk合并为P的解。

3.递推方程的求解方法

1、迭代法

不断用递推方程的右部替换左部,下面以汉诺塔为例进行求解。
在这里插入图片描述

递归与分治策略_第1张图片

有时候直接迭代可能不太方便,可以使用换元迭代。下面以二分归并排序迭代方程为例进行求解。

在这里插入图片描述
递归与分治策略_第2张图片

2、差消法

差消法一般应用在递归方程右边不仅仅只依赖于当前项的前一项,而是前很多项,这种递归方程直接用迭代法很麻烦。属于高阶递归方程,因此要先把高阶递归方程进行差消,再进行迭代。以快速排序的递归方程为例。

在这里插入图片描述
递归与分治策略_第3张图片

3、递归树

建立递归树,每次迭代将函数项作为儿子,非函数项作为根的值。以二分归并排序递归方程为例。

在这里插入图片描述
递归与分治策略_第4张图片

4、主定理

在这里插入图片描述
递归与分治策略_第5张图片

下面举一个例子:

递归与分治策略_第6张图片

4.算法复杂度分析

一个分治法将规模为n的问题分成k个规模为n/m的子问题去解。设分解阀值n0=1,且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。用T(n)表示该分治法解规模为|P|=n的问题所需的计算时间,则有:

T(n)= k T(n/m)+f(n)

三、排序问题

1.合并排序

1.概念
归并排序(Merge Sort)是一种高效的、通用的、基于比较的分治排序算法。大多数实现都产生了稳定的排序,意味着实现保留了排序输出中想等元素的输入顺序。归并排序是一种分治算法,由冯.诺伊曼于1945年发明。

2.基本思想
将待排序元素分成大小大致相同的两个相同子集合,分别对两个子集合进行排序,最终将排好序的子集合合并成要求的排好序的集合。

输入: 一系列的无序元素(eg.数字)组成的输入数组A。

经过: 分两步。第一步、先将整个问题递归分解成一个个只包含一个元素的子问题/子数组(eg,刚开始问题是{2,1,5,6,4,10},直接分成2,1,5,6,4,10即可)

第二步、一步步将分解出来的子数组合并成一个排好序的数列,向两两对比每个自数组(只有1个元素),按照大小顺序合并成一个子数组(有2个元素);再

两两对比现在的子数组,按照大小顺序合并成一个新的子数组…依此类推,知道最后两个子数组进行对比,里面元素按照大小顺序合并成最终的有序序列。

输出: 输出数组B,里面包含的元素都是A中的但是已经按照要求排好了顺序。

当数组元素个数为奇数时:
递归与分治策略_第7张图片
当数组元素个数为偶数时:
递归与分治策略_第8张图片
3.工作原理
3.1.工作流程
1.将未排序的列表划分成为n个子列表,每个子列表包含一个元素(一个元素的列表被认为是排序的)。

2.反复合并子列表以产生新的排序子列表,直到只剩一个子列表。

3.2.划分的方式
自顶向下

自顶向下的实现是使用递归的原理。它从书的顶端开始,然后向下操作,每次操作都问同样的问题(我需要做什么来排序这个数组?),并且回答它(分成两个子数组,进行递归调用,合并结果),知道我们到达树的底部。

自底向上

自底向上的实现则不需要递归。它直接从树的底部开始,然后通过遍历这些片段再将它们合并起来。

其实,任何的合并排序都可以被可视化为树上的操作。树中的叶子代表数组中的各个元素。树的每个内部节点对应于将两个较小的数组合并成一个更大数组。
4.代码实现
递归版

#include
#define Maxn 10001
using namespace std;
void Merge(int arr[], int l, int mid, int r);
void MergeSort(int arr[], int l, int r);
void displayArray(int arr[], int n);
int main()
{
    int arr[Maxn];
    cout << "请输入数组元素的个数: " << endl;
    int n;
    cin >> n;
    cout << "请在" << n << "行内分别输入一个数组元素" << endl;
    for(int i = 0; i < n; i++)
        cin >> arr[i];
    cout << "数组初始状态为: " << endl;
    displayArray(arr, n);
    MergeSort(arr, 0, n-1);
    cout << "数组经过归并排序后的状态为: " << endl;
    displayArray(arr, n);
}

void Merge(int arr[], int l, int mid, int r)
{
    int i, j, k;
    int n1 = mid - l + 1;            //左半边的长度
    int n2 = r - mid;                //右半边的长度
    int *L = new int[n1+1];          //开辟临时空间
    int *R = new int[n2+1];
    for(i = 0; i < n1; i++)
        L[i] = arr[i+l];              //下标从0开始  将数组a的元素填入
    for(j = 0; j < n2; j++)
        R[j] = arr[j+mid+1];
    L[n1] = INT_MAX;
    R[n2] = INT_MAX;
    i = 0;
    j = 0;
    for(k = l; k <= r; k++)     //合并
    {
        if(L[i] <= R[j])
            //注意这里是先把L[i]的值赋给了a[k],之后再i++
            //这样是为了方便操作
            //下面else语句同理
            arr[k] = L[i++];
        else
            arr[k] = R[j++];
    }
}

void MergeSort(int arr[], int l, int r)   //p->left r->right
{
    if(l < r)
    {
        int mid = (l + r) / 2;        //q为中点
        MergeSort(arr, l, mid);         //完成左半部的sort
        MergeSort(arr, mid+1, r);       //完成右半部的sort
        Merge(arr, l, mid, r);          //将两半部的sorted的数列整合成一个数组排序
    }
}

void displayArray(int arr[], int n)
{
    for(int i = 0; i < n-1; i++)
        cout << arr[i] << " ";
    cout << arr[n-1] << endl;
}

非递归版(之后有空再补)
5.算法分析
分类:排序算法

算法种类:分治算法

数据结构:数组

最优时间复杂度:O(nlog(n))

最坏时间复杂度:O(nlog(n))

平均时间复杂度:O(nlog(n))

最坏空间复杂度:共O(n),辅助O(n);当使用linked list,辅助空间为O(1)

稳定性:稳定

复杂性:较复杂

2.快速排序

四、经典递归分治问题

1.平面最接近点对问题

2.棋盘覆盖问题

3.选择问题

4.循环赛日程表

总结

你可能感兴趣的:(算法设计与分析,算法,递归法,分治算法,c++)