《算法设计与问题求解》公开课学习笔记3

第三章 递归与分治

递归的基本思想

1. 引子-故事中的故事

老和尚讲故事:“从前有个庙,庙里有个老和尚,老和尚给小和尚讲故事,讲的什么故事呢?故事是:从前有个庙,庙里有个老和尚,老和尚给小和尚将故事,讲的什么故事呢?故事是......”

可以将上述描述用递归程序的方式表示出来:

void Story()
{    
    printf("从前有个庙,庙里有个老和尚,老和尚给小和尚讲故事, \r\n");
    printf("讲的什么故事呢?故事是:\r\n");
    Story();
}

递归是常用的一种求解问题的思想,包括三个基本的要素:

1. 递归终止条件

2. 终止处理办法

3. 递归处理方法

举例:Fibonacci数列:意大利数学家Fibonacci在年写成的《计算之书》提出这样一个有趣的问题:每一对兔子每个月不多不少恰好能生一对(一雌一雄)新兔子,而且每对新生兔子出生两个月后就成熟并具备繁殖能力;另外,假定所有的兔子都不会死亡。假如养了初生的小兔一对,试问 n 个月后共有多少对兔子?

分析:

第 n 个月的兔子对数为 F(n),它们按照兔子的成熟属性可以分为两类:

成熟兔子对 = ?  第 n-1 个月的兔子数 F(n-1)

新生兔子对 = ?  第 n-2 个月的兔子书 F(n-2)

则递归方程为F(n) = \left\{\begin{matrix} 1, n = 0, 1 & \\ F(n-1) + F(n-2), n>1& \end{matrix}\right. ,根据递归方程,写出递归程序:

long fib(int n)
{
    if (n <= 1) return 1;
    return fib(n-1) + fib(n-2)
}

总结:

1. 递归是一种特殊的迭代,但是在迭代前不知道还要迭代多少次。

2. 递归函数一定有参数,且参数会在迭代的过程中逐步逼近某个值。

3. 递归函数中一定有处理终点,而这个点就是递归出口。

2. 递归算法的实例

字符串的全排列:给定一组互不相同的字符,求这组字符的全排列。输入:一个字符串;输出:输出该字符串的全排列,排列的先后顺序不影响结果。

例:输入 ABC;输出 ABC ACB BAC BCA CAB CBA

问题分析:

简单情景,当输入字符串长度为 1 时,可以直接输出;

当输入的字符串长度超过 1 时,只需要从字符串中选一个字符作为输出字符串的首字符,对其余的字符进行递归处理(全排列)即可。递归程序如下,时间复杂度为 O(n!)。

void permutations(string str, int i, int n)
{
    if (i == n-1)
    {//递归出口
        cout << str << endl;
        return;
    }
    //递归长度大于 1 的字符串
    for (int j = i; j < n; j++)
    { //交换当前第一个字符与其他位置字符
        swap(str[i], str[j]);  // STL 函数
      //递归处理子串 str[i+1, n-2]
        permutations(str, i+1, n);
      // 还原到输入宗富川 str 的顺序
        swap(str[i], str[j]);
}

             

分治策略的基本原理

分:将规模比较大的问题分解为若干个规模比较小的问题,若分一次不够,则可以递归处理,将分解的子问题的继续分解,直至分解的子问题为规模足够小基础问题。

治:求解规模足够小的基础问题

合:将求出的小规模问题的解合并为一个更大规模的问题的解,自底向上逐步求出原来问题的解。

因此分治算法可以细分为三个阶段:Divide、Conquer、Combine。Divide 阶段是把原问题分割成小问题;Conquer阶段是递归处理流程;Combine阶段是运用小问题的答案合成出原问题的解答。

分治算法的框架程序如下:

divide_and_conquer(P){
(1) if (|p| <= n0) adhoc(P); // 递归出口,用特定的程序解决基础问题
(2) divide P into smaller subinstances P1, P2, ..., PK  // 分解出子问题
(3) for (i = 1, i<= k, i++)
        yi = divide_and_conquer(Pi);  // 递归求解各个子问题
(4) return merge(y1, y2, ..., yk);  // 将各个子问题的解合并为原问题的解}

设计分治策略,把原问题分解成 k 个规模较小的子问题,这个步骤是分治算法的基础和关键,人们往往遵循两个原则:

1. 平衡子问题原则,分割出的 k 个子问题其规模最好大致相当;

2. 独立子问题原则,分割出的 k 个子问题之间重叠越少越好,最好 k 个子问题是相互独立,不存在重叠在问题。

1. 分治策略-Master定理

【Master定理】:假设 a >= 1 和 b >= 1 是常数,f(n) 是定义在非负整数上的一个确定的非负函数,T(n) 也是定义在非负整数上的一个非负函数,且满足递归方程:T(n) = aT(n/b) + f(n),如果 f(n) 符合下述三类条件,T(n)的渐近复杂度为:

(1)若对于某常数 \varepsilon > 0,,有 f(n) = O(n^{log_{b}a-\varepsilon })(f(n)的上界),则 T(n) = O(n^{log_{b}a})

(2)若 f(n) = \Theta (n^{log_{b}a}),则有 T(n) =O(n^{log_{b}a}\cdot logn)

(3)若存在常数 \varepsilon > 0,,有 f(n) = \Omega (n^{log_{b}a+\varepsilon })(f(n)的下界),且对于某常数 c>1 和所有充分大的正整数 n 有 a\cdot f(n/b) \leq c\cdot f(n),则有 T(n) = O(f(n))

例1:求 T(n) = 9T(n/3) + n的渐近阶。

利用 Mater 定理,其中 a = 9, b = 3, f(n) = n,则 n^{log_{b}a} = n^2,则时间复杂度为 O( n^2)。

例2:求 T(n) = T(2n/3) + 1 的渐近阶。

利用 Master 定理,a = 1, b = 3/2, f(n) = 1,则 n^{log_{b}a} = n^0 = 1 = f(n),则时间复杂度为 O(logn)。

例3:求 T(n) = 2\cdot T(n/2) + n^2 的渐近阶。

利用 Master 定理,其中 a = 2, b = 2, f(n) = n^2,则 n^{log_{b}a} = n^1 = n,则时间复杂度为 O(n^2)

例4:求 T(n) = 2T(n/2) + nlogn 的渐近阶。

利用 Master 定理,a = 2, b = 2, f(n) = nlog(n),则 n^{log_{b}a} = n^1 = n,可以验证 n^{log_{b}a}是 f(n)的下界,但是不满足 f(n) = \Omega (n^{log_{b}a+\varepsilon }),因此无法满足 Master 定理,需要使用推导的方法得到最终的时间复杂性函数。

2. 合并排序

任意给定一个包含 n 个整数的集合把 n 个整数按升序排列。

输入:每个测试用例包括两行,第一行输入整数个数,第二行输入 n 个整数,数与数之间用空格隔开。最后一行包含-1,表示输入结束。

输出:每组测试数据的结果输出占一行,输出按照升序排列的 n 个整数。

样例输入:

7

49 38 65 97 13 27

-1

样例输出:

13 27 38 49 65 76 97

合并排序基本思想:

分:根据整数集合的规模把原始的整数集合(记为A = {a[l], ..., a[r]})平均分成两部分:A1 = {a[l], ..., a[(l+r)/2]} 元素为第一部分,A2 = {a[(l+r)/2 + 1, ..., a[r]} 为第二部分。

治:如果划分后的子集 A1 和 A2 只包含一个整数,则不需要任何操作,把这单个整数当成以排好序的集合,否则,把该子集继续分割,然后递归调用。

合:设计子程序 merge (合并程序)把两个已经升序排列的子集B1,B2合并为一个整体升序排列的集合 B。

void mergeSort(int iDatas[], int iBuffer[], int iLow, int iHigh){
    if (iHigh > iLow){
        int iMid = (iLow + iHigh)/2;
        mergeDort(iDatas, iBuffer, iLow, iMid);
        mergeSort(iDatas, iBuffer, iMid, iHigh);
        merge(iDatas, iBufferm, iLow, iMid, iHigh);
        for (int i == iLow; i<= iHigh; i++)
            iDatas[i] = iBuffer[i];
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值