递推递归分治基本讲解

递推

1.基本概念:

是一种简单的算法,即通过已知条件,利用特定的关系得出中间推论,直至得到结果的一个算法

分为顺推法和逆推法两种

顺推法:由已知条件出发,逐步推算出要解决的问题的方法,例如斐波那契数列

逆推法:由已知结果出发,用迭代表达式逐步推算出问题开始的条件,即顺推法的逆过程

2.几个经典样例

斐波那契数列

又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,

兔子繁殖问题:

有一对兔子,从出生后第三个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔。假设所有兔子都不死,问每个月的兔子总数为多少?

设他的函数为f(n),已知f(n)=1,f(2)=1,f(n)=f(n-1)+f(n-2)(n>=3,n∈N),则我们可以推断出f(3)=2,f(4)=3……

#include<stdio.h>
int main()
{
    int n , i , f[1000] ;
    scanf("%d",&n) ;
    f[0] = 0 ;
    f[1] = 1 ;
    for(i = 2 ; i <= n ; ++i)
    {
        f[i] = f[i - 1] + f[i - 2] ;
    }
    printf("%d\n",f[n]) ;
    return 0;
}
​
 
汉诺塔问题

题意:

Hanoi塔由n个大小不同的圆盘和三根木柱a,b,c组成。开始时,这n个圆盘由大到小依次套在a柱上,如图所示。

要求把a柱上n个圆盘按下述规则移到c柱上: (1)一次只能移一个圆盘; (2)圆盘只能在三个柱上存放; (3)在移动过程中,不允许大盘压小盘。

问将这n个盘子从a柱移动到c柱上,总计需要移动多少个盘次?

解析:

设Hn为n个盘子从a柱移到c柱所需移动的盘次。

显然,当n=1时,只需把a 柱上的盘子直接移动到c柱就可以了,故H1=1。

当n=2时,先将a柱上面的小盘子移动到b柱上去,然后将大盘子从a柱移到c柱,最后,将b柱上的小盘子移到c柱上,共记3个盘次,故:H2=3。

以此类推,当a柱上有n(n>=2)个盘子时,总是先借助c柱把上面的n-1个盘子移动到b柱上,然后把a柱最下面的盘子移动到c柱上,再借助a柱把b柱上的n-1个盘子移动到c柱上,总共移动H(n-1)+1+H(n-1)个盘次。

∴Hn=2H(n-1)+1,边界条件:H1=1

#include<stdio.h>
int main()
{
    long long int s[105] ;
    int n , i ;
    scanf("%d" , &n) ;  //n为盘子的个数
    s[1] = 1 ;
    for(i = 2 ; i < n + 1 ; ++ i)
    {
        s[i] = 2 * s[i - 1] + 1 ;
    }
    printf("%lld\n" , s[n]) ;  //需要移动的次数
    return 0 ;
}

 

各种平面分割问题

· 直线分割平面问题

题意:

n条直线,最多可以把平面分割成多少个区域

解析:

第n条线与前面的n-1条线均相交,而且交点不重叠

如下图所示,第四条直线满足的一条件是与前面三条直线相交而且交点不重叠

令第n条直线分割的平面数是f(n),则f(1)=2

我们再来考虑第n条直线,第n条直线与n-1条直线相交,交点不重叠,那么第n条直线被分成了n段。如上面的图可以看出此规律。这n段线段或者射线参与了平面的分割任务,而且他们分别位于n-1条直线分割出来的不同的平面区域内。所以第n条直线加入之后,多出来的平面数量是n。

故有 f(n)=f(n-1)+n.

递归式求出来了,可知f(n)=f(1)+2+3+...+n=1+1+2+3+...+n = n*(n + 1)/2 + 1

#include<stdio.h>
int main()
{
    int n , i ;
    scanf("%d" , &n) ;
    printf("%d\n" , n *(n + 1)/2 + 1) ;
    return 0 ;
}
 

· 折线分割平面(hdu2050)

根据直线分平面可知,由交点决定了射线和线段的条数,进而决定了新增的区域数。当n-1条折线时,区域数为f(n-1)。为了使增加的区域最多,则折线的两边的线段要和n-1条折线的边,即2 *(n-1)条线段相交。那么新增的线段数为4*(n-1),射线数为2。但要注意的是,折线本身相邻的两线段只能增加一个区域。 由此可得:

f(n) = f(n - 1) + 4(n - 1) + 2 - 1

= f(n - 1) + 4(n - 1) + 1

= f(n - 2) + 4(n - 2) + 4(n - 1) + 2

= f(1) + 4 + 4 * 2 + …… + 4(n - 1) + (n - 1)

= 2n ^ 2 - n + 1

#include<stdio.h>
int main ()
{
    int t , n ;
    scanf("%d" , &t) ;
    while (t --)
    {
        scanf("%d" , &n) ;
        printf("%d\n" , 2 * n * n - n + 1) ;
    }
    return 0 ;
}
 

· 椭圆切割平面

!!该问题只讨论两两椭圆只有两个交点的情况

题意:

有n条封闭曲线画在平面上,而任何两条封闭曲线恰好相交于两点,且任何三条封闭曲线不相交于同一点,问这些封闭曲线把平面分割成的区域个数。

设椭圆数为n,分割数为S(n),则:S(1)=2 , S(2) = 4 , S(3) = 8 , S(4) = 14 ……

解析:

当n - 1个圆时,区域数为f(n - 1).那么第n个圆就必须与前n - 1个圆相交,则第n个圆被分为2(n - 1)段线段,增加了2(n - 1)个区域。 则: f(n) = f(n - 1) + 2(n - 1) = f(1) + 2 + 4 + …… + 2(n - 1) = n ^ 2 - n + 2

#include<stdio.h>
int main()
{
    int n , i ;
    scanf("%d" , &n) ;
    printf("%d\n" , n * n - n + 2) ;
    return 0 ;
}
 

· 三角形分割平面(hdu1249)

设三角形数为n,分割数为S(n)

则S(1) = 2 , S(2) = 8 , S(3) = 20

当 n=2 时,新增的三角形与原有的三角形有了6个交点,即每边2个交点,也就产生6个新增区域,(图为六芒星我懒了)

也就是当我们画第n个三角形的时候,该三角形的一条边会穿过2 * ( n - 1)条边

递推式:S(n) = S(n - 1) + 6(n - 1)

S(n)=3n ^ 2 - 3 * n + 2

#include<stdio.h>
int main ()
{
    int t , n ;
    scanf("%d" , &t) ;
    while (t --)
    {
        scanf("%d" , &n) ;
        printf("%d\n" , 3 * n * n - 3 * n + 2) ;
    }
    return 0 ;
}

 

 

· 平面分割空间问题(hdu1290)

由二维的分割问题可知,平面分割与线之间的交点有关,即交点决定射线和线段的条数,从而决定新增的区域数。试想在三维中则是否与平面的交线有关呢?当有n-1个平面时,分割的空间数为f(n - 1) 。要有最多的空间数,则第n个平面需与前n-1个平面相交,且不能有共同的交线。即最多有n-1 条交线。而这n-1条交线把第n个平面最多分割成g(n-1)个区域。g(n)为(1)中的直线分平面的个数 )此平面将原有的空间一分为二,则最多增加g(n-1)个空间。

故:

f(n) = f(n - 1) + g(n - 1) ps : g(n) = n(n + 1) / 2 + 1

=f(n-2) + g (n - 2) + g(n - 1)

……

=f(1) + g(1) + g(2) + …… + g(n - 1)

=2 + (1 * 2 + 2 * 3 + 3 * 4 + …… + (n - 1) * n) / 2 +( n - 1)

=(1 + 2 ^ 2 + 3 ^ 2 + 4 ^ 2 + …… + n ^ 2 - 1 - 2 - 3 - …… - n ) / 2 + n + 1

=(n ^ 3 + 5 * n + 6) / 6

 

!!总结

平面分割问题二维的一般都是a* n ^ 2 + b * n + 2 , 三维的一般都是a * n ^ 3 + b * n ^ 2 + c * n + d ,用待定系数法即可

 

递归

1.基本概念

1.

一个函数直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。

2.两个重要特征:

结束条件和自我调用。结束条件定义了最简子问题的答案,自我调用是在解决子问题。

3.

以计算n的阶乘为例

分析:n的阶乘是指n!=n* (n-1) * (n-2) * …… * 2 * 1 = n*(n-1)!,当n>1时,n!包括(n-1)的阶乘这个较小的局部问题。因此,我们在计算这个问题的时候,可以调用具有相同功能的函数(即该函数的本身)来计算(n-1)的阶乘。另外需要注意的是,设计递归函数时必须要为其设置留有一个终点,例如这里我们要让n=1的时候返回1。

#include<stdio.h>
int f(int n)
{
    if(n == 1)
        return 1 ;
    return n * f(n - 1) ;
}
int main(void)
{
    int n ;
    scanf("%d",&n) ;
    printf("%d\n",f(n)) ;
    return 0 ;
}
 

以斐波那契为例

#include<stdio.h>
int f(int n)
{
    if(n <= 2)
        return 1 ;
    return f(n - 1) + f(n - 2) ;
}
int main(void)
{
    int n ;
    scanf("%d",&n) ;
    printf("%d\n",f(n)) ;
    return 0 ;
}

 


以汉诺塔问题

#include<stdio.h>
void move(char getone , char putone)
{
    printf("%c-->%c\n", getone, putone);
}
void hanoi(int n, char a, char b, char c)
 {
    if(n == 1)
    {
        move(a, c) ;//当n=1时直接从A移动到C
    }
    else
    {
        hanoi(n - 1, a, c, b) ; //按ACB数序执行N-1的汉诺塔移动
        move(a, c); //执行最大盘移动
        hanoi(n - 1, b, a, c) ;//按BAC数序执行N-1的汉诺塔移动
    }
}
int main(void)
{
    int n ;
    scanf("%d", &n) ;
    hanoi(n, 'A', 'B', 'C') ;
    return 0;
}
​
 

2.递归与循环的优缺点:

递归的优点:代码简洁清晰,容易验证正确性

递归的缺点:运行需要多次的函数调用,调用层数较多的话,会对执行效率有一定影响。较占用时间和空间资源。

循环的优点:速度快,结构简单。

循环的缺点:不可解决所有的问题,有的问题适合使用递归而不是循环。

如果使用循环并不困难的话,更推荐使用循环。

3.递归与枚举的差别:

枚举是横向的把问题划分,然后一次求解子问题。而递归是把问题逐级分解,是纵向的拆分。

4.递推与迭代,递归的差别:

递推算法的首要问题是得到相邻数据项间的关系(即递推关系)。递推算法避开了求通项公式的麻烦,把一个复杂问题求,分成了连续的若干步简单运算。

迭代是利用变量的原值推算出变量的一个新值,如果递归是自己调用自己的话,迭代就是A不停的调用B。

递归是程序调用自己的编程技巧称为递归,是函数自己调用自己。使用递归时,不有一个明确的递归结束条件,称为递归出口。

一般来说,可以将递推算法看成一种特殊的迭代算法。递推和迭代都是正向的将一个复杂的问题分解成小问题,一步一步得出结果。而递归是逆项的,多了一个回溯的过程。

 

分治

1.基本概念:

1.字面上的解释是“分而治之”,使用递归的技巧,可以将一个问题拆分成两个或者多个较小的局部问题,利用递归函数求出每个局部问题的解,然后在将结果整合,最终解决原问题,这种编程手法称为分治法。

2. 实现步骤如下:

1.将问题分割成局部问题

2.递归求解局部问题

3.将局部问题的解整合,然后解决原问题。

3.使用的情况:

1.缩小到一定程度可以较为容易的解决。

2.可以分解成若干个规模较小的相同问题

(反应了递归思想的应用)

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

(如果具备了前两条的特征但是不具备第三条,则可以考虑贪心或者动态规划)

4.分解出的子问题是相互独立的,即子问题之前不包含公共的子子问题。

4.应用:

1.二分搜索

2.大整数乘法

3.Strassen矩阵乘法

4.棋盘覆盖

5.归并排序

等等

归并排序为例:

(归并排序:是建立在归并操作上一种有效的排序算法。时间复杂度为O(nlogn),该算法采用分治法的一个典型应用,各层分治可以同时进行。其思想是将两个有序的数列合并成一个大的有序数列,通过递归,层层合并,即为归并。)

流程图如下(递归实现):

原理如下: 1、申请空间,大小为待排序序列的大小,用来存放合并后的序列。 2、设定两个下标,最初位置为两个已排序序列的起始位置。 3、比较两个下标所指向的元素,选择较小的元素放入合并空间,并将下标置为下一个位置。 4、重复3操作,知道某一下标到达序列尾部。 5、将另一个序列剩下的所有元素直接复制并合并到序列尾部。

#include<stdio.h>
#include<stdlib.h>int Merge(int tempArr[] , int sourceArr[] , int low , int mid , int high)
{
    int i = low , j = mid + 1 , k = low ;
    while(i <= mid && j <= high)
    {
        if(sourceArr[i] < sourceArr[j])
            tempArr[k++] = sourceArr[i++] ;
        else
            tempArr[k++] = sourceArr[j++] ;
    }
    while(i <= mid)  //将左边的元素填充到tempArr中
        tempArr[k++] = sourceArr[i++] ;
    while(j <= high) //将左边的元素填充到tempArr中
        tempArr[k++] = sourceArr[j++] ;
    for (int p = low ; p <= high ; ++ p)
        sourceArr[p] = tempArr[p] ; //将tempArr中的数据拷贝到原数组对应的序列区间
    return 0;
}
​
int MergeSort(int sourceArr[] , int tempArr[] , int low , int high)
{
    int mid ;
    if(low < high)
    {
        mid = (low + high) / 2 ; //mid将数组二分
        MergeSort(sourceArr, tempArr, low, mid) ; //对左边进行归并排序,使其有序
        MergeSort(sourceArr, tempArr, mid + 1, high) ;//对右边进行归并排序,使其有序
        Merge(tempArr, sourceArr, low, mid, high) ;//将两个有序数组合并
    }
    return 0 ;
}
​
int main(void)
{
    int i ;
    int a[10], b[10] ;
    for (i = 0 ; i < 10 ; ++ i)
        a[i] = rand() % 100 ;
    for (i = 0 ; i < 10 ; ++ i)
        printf("%d ", a[i]) ;
    printf("\n") ;
    MergeSort(a , b , 0 , 9) ;
    for (i = 0 ; i < 10 ; ++ i)
        printf("%d ", b[i]);
    return 0 ;
}

 

仅供参考23333

 

 

posted @ 2020-01-31 13:57  君月白吖  阅读(148)  评论(0编辑  收藏