算法之递归与分治

0、目录

1、算法概念与设计分析

算法总体思想-大事化小小事化了

  对这k个子问题分别求解。如果子问题的规模仍然不够小,则再划分为k个子问题,如此递归的进行下去,直到问题规模足够小,很容易求出其解为止。
  将求出的小规模的问题的解合并为一个更大规模的问题的解,自底向上逐步求出原来问题的解。
  分治法的设计思想是,将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
  直接或间接地调用自身的算法称为 递归算法。用函数自身给出定义的函数称为 递归函数
递归有两种

  • 直接递归:自己调用自己
  • 间接递归:A调用B,B调用A
//直接递归:自己调用自己
A(){A();}
//间接递归:A调用B,B调用A
A(){B();}
B(){A();}

  由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。
  分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。
怎么写递归函数:

  1. 相同的事情交给相同的函数做。
  2. 当你在写函数A()时,如果又碰到了同样的事情时,就直接调用自己。
  3. 已找到递归方程(两边都有同一个函数),可以用递归。如n!、Fibonacci数列等。
  4. 没有递归方程的话(例如不是要求算某个数,而是要求做某些事),你要找出分解后小规模问题与原问题的相似之处。如Hanoi塔、全排列; 有时需要利用递归调用、返回的顺序特征。
  5. 一个返回值的话,就直接return;多个的话,作为函数的参数。参数中需要改变的量应该采用引用调用,例如&max,&min。
  6. 别忘了边界条件—出口。

递归的优缺点:
优点: 结构清晰,可读性强,而且容易用数学归纳法来证明算法的正确性,因此它为设计算法、调试程序带来很大方便。
缺点: 递归算法的运行效率较低,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多。
分治法的适用条件:
分治法所能解决的问题一般具有以下几个特征:

  • 该问题的规模缩小到一定的程度就可以容易地解决;
  • 该问题可以分解为若干个规模较小的相同问题
  • 利用该问题分解出的子问题的解可以合并为该问题的解;
  • 该问题所分解出的各个子问题是相互独立的,即子问题之间不重复。

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

分治法的基本步骤:

divide-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);  //递归的解各子问题
    return merge(y1,...,yk);  //将各子问题的解合并为原问题的解
}

人们从大量实践中发现,在用分治法设计算法时,最好使子问题的规模大致相同。即将一个问题分成大小相等的k个子问题的处理方法是行之有效的。这种使子问题规模大致相等的做法是出自一种 平衡(balancing)子问题 的思想,它几乎总是比子问题规模不等的做法要好。

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

通过迭代法求得方程的解:

2、例题

2.1 阶乘

例: 阶乘函数
  阶乘函数可递归地定义为:
边界条件、出口

边界条件、出口

递归方程

递归方程

  边界条件与递归方程是递归函数的二个要素,递归函数只有具备了这两个要素,才能在有限次计算后得出结果。边界条件一般为显式的,例如if()…


递归过程与工作栈:

  • 递归过程在实现时,需要自己调用自己。
  • 每一次递归调用时,需要为过程中使用的参数、局部变量、返回地址等另外分配存储空间。
  • 层层向下递归,退出时的次序正好相反:
    递归次序-------------------------------->
    n! --> (n-1)! --> (n-2)! --> 1! --> 0!=1
    <---------------------------------- 返回次序
  • 因此,每层递归调用需分配的空间形成递归工作记录,按后进先出的栈组织存取。


n的阶乘完整代码:

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

例:Hanoi塔问题
  设a,b,c是3个塔座。开始时,在塔座a上有一叠共n个圆盘,这些圆盘自下而上,由大到小地叠在一起。各圆盘从小到大编号为1,2,…,n,现要求将塔座a上的这一叠圆盘移到塔座b上,并仍按同样顺序叠置。在移动圆盘时应遵守以下移动规则:
规则1:每次只能移动1个圆盘;
规则2:任何时刻都不允许将较大的圆盘压在较小的圆盘之上;
规则3:在满足移动规则1和2的前提下,可将圆盘移至a,b,c中任一塔座上。

Hanoi塔问题(递归的关键是分解为同结构的小问题)


Hanoi塔问题完整代码:

#include <stdio.h>
void Hanoi(int n, char A, char B, char C) {
    if(n==1) {
    	printf("%c->%c\n", A, C);
    } else {
    Hanoi(n-1, A, C, B);
    printf("%c->%c\n", A, C);
    Hanoi(n-1, B, A, C);
    }
}
int main(void) {
    int n; //n为盘子的个数 
    scanf("%d", &n); 
    Hanoi(n, '1', '2', '3');
    return 0;
}
2.3 整数划分

例:整数划分问题
  将正整数n表示成一系列正整数之和:n=n1+n2+…+nk,其中n1≥n2≥…≥nk≥1,k≥1。正整数n的这种表示称为正整数n的划分。求正整数n的不同划分个数。
例如:正整数6有如下11种不同的划分:
6;
5+1;
4+2,4+1+1;
3+3,3+2+1,3+1+1+1;
2+2+2,2+2+1+1,2+1+1+1+1;
1+1+1+1+1+1。

n=n1+n2+…+nk,其中n1≥n2≥…≥nk≥1,k≥1
  前面的几个例子中,问题本身都具有比较明显的递归关系,因而容易用递归函数直接求解。
  在本例中,如果设p(n)为正整数n的划分数,则难以找到递归关系,因此考虑增加一个自变量:将sum中最大加数n1不大于m的划分个数记作q(n,m)。
可以建立q(n,m)的如下递归关系。
(1) q(n,1)=1,n ≥ 1;
当最大加数n1不大于1时,任何正整数n只有一种划分形式,即
(2) q(n,m)=q(n,n),m ≥ n;
最大加数n1实际上不能大于n。因此,q(1,m)=1。
将sum中最大加数n1 ≤ m的划分个数记作q(n,m)
(3) q(n,n)=1+q(n,n-1);
正整数n的划分由n1=n的划分和n1≤n-1的划分组成(n1不可能大于n)。
q(6,6)=1+q(6,5)
(4) q(n,m)=q(n,m-1)+q(n-m,m),n>m>1;
正整数n的最大加数n1不大于m的划分由n1=m的划分和
n1≤m-1 的划分组成(思路同(3) )。
q(6,3)=q(6,2)+q(6-3,3)
q(n-m,m): n=m+n2+…, n2<=n1=m
n=n1+n2+…+nk,其中n1≥n2≥…≥nk≥1,k≥1

正整数n的划分数p(n)=q(n,n)。
整数划分问题完整代码:

#include<stdio.h>
int q(int n,int m){
    if(n==1 || m==1){
    	return 1;
    }else if(n > m){
  	return q(n-m,m)+q(n,m-1);
    }else if(n < m){
  	return q(n,n);
    }else{
  	return 1+q(n,n-1);
    }
}
int main(){
    int n;
    scanf("%d",&n);
    printf("%d\n",q(n,n));
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
/** * * @author SunnyMoon */ ////////////////////////////////////////////////////////////////////////////// /** * 概念介绍: * * 消除递归: * 一个算法作为一个递归的方法通常从概念上很容易理解,但实际使用中递归的效率不高,在这种 * 情况下,把递归算法转换成非递归算法是非常有用的,这种转换经常用到栈。 * * 递归和栈: * 递归和栈之间有着紧密的联系,大部分的编译器使用栈实现递归的。 * * 调用方法的时候发生什么: * 1. 编译器会把这个方法所有当前参数及返回地址压入栈中; * 2. 将控制权交给这个方法,方法通过获得栈顶元素值访问参数; * 3. 方法运行结束的时候,值退栈,参数消失且控制权重新回到返回地址; * * 模拟递归方法: * 可以将任意一个递归方法转换为非递归的基于栈的方法。在一些简单的情况可以完全消除栈,只 * 使用一个简单的循环,但是在很复杂的情况,算法中必须须要保留栈。本例子是简单的情况,可 * 以进一步完全消除栈。 */ ///////////////////////////////////////////////////////////////////////////// /** * 计算三角数字的问题: * 递归算法描述如下 * int triiangle(int n){ * if(n==1) * return 1; * else * return (n+triangle(n-1)); * } */ import java.io.*; /** * 模拟一个递归方法,通用的方式消除递归 */ class Params {//封装了方法的返回地址和方法的参数 public int number; public int returnAddress; public Params(int num, int returnAdd) { number = num; returnAddress = returnAdd; } } class StackX {//模拟递归时使用的栈 private int maxSize; private Params[] stackArray; private int top; public StackX(int s) { maxSize = s; stackArray = new Params[maxSize]; top = -1; } public void push(Params p) { stackArray[++top] = p; } public Params pop() { return stackArray[top--]; } public Params peek() { return stackArray[top]; } } class StackTriangleApp { static int theNumber; static int theAnswer; static StackX theStack; static int logicAddress; static Params theseParams; public static void main(String[] args) throws IOException{//主方法 System.out.print("Number = "); theNumber = getInt(); stackTriangle(); System.out.println(""); System.out.println("Trriangle = " + theAnswer); } @SuppressWarnings("empty-statement") public static void stackTriangle() {//计算三角数字的方法,模拟递归方法 theStack = new StackX(100); logicAddress = 1;//设置一个逻辑地址为入口地址 while (step() == false); } public static boolean step() { switch (logicAddress) { case 1: theseParams = new Params(theNumber, 6);//设定循环返回的地址 theStack.push(theseParams); logicAddress = 2; break; case 2: theseParams = theStack.peek(); if (theseParams.number == 1) { theAnswer = 1; logicAddress = 5; } else { logicAddress = 3; } break; case 3: Params newParams = new Params(theseParams.number - 1, 4); theStack.push(newParams); logicAddress = 2; break; case 4: theseParams = theStack.peek(); theAnswer = theAnswer + theseParams.number; logicAddress = 5; break; case 5: theseParams = theStack.peek(); logicAddress = theseParams.returnAddress; theStack.pop(); break; case 6: return true; } return false; } public static String getString() throws IOException{ InputStreamReader isr = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(isr); String s = br.readLine(); return s; } public static int getInt() throws IOException{ String s=getString(); return Integer.parseInt(s); } } /** * 总结: * 当要求效率的时候可以把弟归转化为基于栈的非递归,进而可以把基于栈的转化为仅有循环的 * 非递归,这种情况下效率是最高的。 * 但是一些复杂的情况可以转化为基于栈的非递归,但是无法消除栈的。 * 一些递归算法是非常优秀的,比如分治算法。 */
L型组件填图问题 1.问题描述 设B是一个n×n棋盘,n=2k,(k=1,2,3,…)。用分治法设计一个算法,使得:用若干个L型条块可以覆盖住B的除一个特殊方格外的所有方格。其中,一个L型条块可以覆盖3个方格。且任意两个L型条块不能重叠覆盖棋盘。 例如:如果n=2,则存在4个方格,其中,除一个方格外,其余3个方格可被一L型条块覆盖;当n=4时,则存在16个方格,其中,除一个方格外,其余15个方格被5个L型条块覆盖。 2. 具体要求 输入一个正整数n,表示棋盘的大小是n*n的。输一个被L型条块覆盖的n*n棋盘。该棋盘除一个方格外,其余各方格都被L型条块覆盖住。为区别各个方格是被哪个L型条块所覆盖,每个L型条块用不同的数字或颜色、标记表示。 3. 测试数据(仅作为参考) 输入:8 输:A 2 3 3 7 7 8 8 2 2 1 3 7 6 6 8 4 1 1 5 9 9 6 10 4 4 5 5 0 9 10 10 12 12 13 0 0 17 18 18 12 11 13 13 17 17 16 18 14 11 11 15 19 16 16 20 14 14 15 15 19 19 20 20 4. 设计与实现的提示 对2k×2k的棋盘可以划分成若干块,每块棋盘是原棋盘的子棋盘或者可以转化成原棋盘的子棋盘。 注意:特殊方格的位置是任意的。而且,L型条块是可以旋转放置的。 为了区分棋盘上的方格被不同的L型条块所覆盖,每个L型条块可以用不同的数字、颜色等来标记区分。 5. 扩展内容 可以采用可视化界面来表示各L型条块,显示其覆盖棋盘的情况。 经典的递归问题, 这是我的大代码, 只是本人很懒, 不想再优化
递归分治算法是计算机科学中常用的算法设计方法,它们通常用于解决复杂的问题。在算法设计实验中,我们可以通过分析递归分治算法的性能以及实现过程来更深入地理解它们的原理和应用。 下面是针对递归分治算法设计实验的分析: 1. 算法实现:在实验中,我们需要实现递归分治算法递归算法通常包含一个基本情况和一个递归情况。基本情况是结束递归的条件,而递归情况是通过调用自己来解决问题分治算法通常包含三个步骤:分解问题、解决问题和合并结果。在分解问题的过程中,将原问题划分为若干个子问题,然后递归地解决子问题。在解决问题的过程中,对每个子问题进行求解。在合并结果的过程中,将子问题的结果合并成原问题的解。 2. 算法性能:在分析算法性能时,我们需要考虑算法的时间复杂度和空间复杂度。递归算法的时间复杂度通常与递归深度有关,而分治算法的时间复杂度通常与问题规模有关。空间复杂度通常与算法递归深度和使用的数据结构有关。在实验中,我们可以通过比较递归分治算法的时间复杂度和空间复杂度来评估它们的性能。 3. 算法应用:递归分治算法在实际应用中都有广泛的应用。递归算法适用于具有递归结构的问题,例如树和图。分治算法适用于可以分解为若干个子问题问题,例如排序、查找和计算几何等问题。在实验中,我们可以通过应用递归分治算法来解决不同类型的问题,例如二叉树的遍历、归并排序和最近点对问题等。 总之,递归分治算法是计算机科学中非常重要的算法设计方法。通过实验,我们可以更好地理解它们的原理和应用,并且能够更加深入地研究算法的性能和实现过程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值