(一)递归与分治
分治的全称为“分而治之”,也就是说,分治法将原问题划分成若干个规模较小而结构与原问题相似或者相同的子问题,然后分别解决这些子问题,最后合并子问题的解,即可得到原问题的解。总结一下分治法的三个步骤:
①分解:将原问题分解为若干个相似或者相同的子问题。
②解决:递归解决所有的子问题。
③合并所有子问题的解得到原问题的解。
举个例子
/*递归求斐波那契*/
public static int Fibonacci(int n){
if(n==0)return 1;
if(n==1)return 1;
return Fibonacci(n-1)+Fibonacci(n+1);//计算Fibonacci(n)时,要计算Fibonacci(n-1)和Fibonacci(n-2),两个相加就是合并。
}
递归,一类反复调用自身的函数,但是每次都可以把问题规模缩小,因此递归可以很好的实现分治的思想。
递归的两个重要概念:(以递归版斐波那契为例子)
①递归边界
if(n==0)return 1;
if(n==1)return 1;
②递归式
return Fibonacci(n-1)+Fibonacci(n+1);
/*这种递归式放在尾部的我们称之为尾递归。*/
下面来看几道题目,了解一下基础的递归案例
P1044 [NOIP2003 普及组] 栈
题目内容
题目描述
宁宁考虑的是这样一个问题:一个操作数序列,1,2,…,n,栈 A 的深度大于 n。
现在可以进行两种操作,
将一个数,从操作数序列的头端移到栈的头端(对应数据结构栈的 push 操作)
将一个数,从栈的头端移到输出序列的尾端(对应数据结构栈的 pop 操作)
你的程序将对给定的 n,计算并输出由操作数序列 1,2,…,n 经过操作可能得到的输出序列的总数。输入格式
输入文件只含一个整数 n(1≤n≤18)。
输出格式
输出文件只有一行,即可能输出序列的总数目。
输入输出样例
输入 #1
3
输出 #1
5
题目分析:
我们如果要使用递归来做,就要先找到递归关系式和递归边界。
由于栈只有两种操作,pop()压栈和push()弹栈,如果栈为空,只能压栈push(),如果待入栈的序列为空,只有弹出pop()的操作,且只有一种输出序列。
所以有三种情况,递归函数要有两个参数,栈中元素i,待入栈元素n.
第一种情况:i=0,这时候只能入栈,i+1,n-1
第二种情况:n=0,这时候只能出栈,而且要不断把栈里面的元素弹出,只能有一种序列,直接返回1。
第三种情况:n!=0,i!=0这时候,可以出栈,也可以继续进栈,返回两个子问题进栈(i+1,n-1),出栈(i-1,n)
所以我们就找到了递归关系式,和递归边界。
递归边界:
if(n==0)return 1;
递归关系式:
if(i==0&&n!=0)return fun(i+1,n-1);
else{
//其他情况
return fun(i+1,n-1)+fun(i-1,n);
}
参考代码:
import java.util.Scanner;
public class Main {
static long fun(int i,int n) {
//递归写法
if (n==0) {
return 1;
}
if (i==0) {
return fun(i+1, n-1);
}
return fun(i+1, n-1)+fun(i-1, n);
}
public static void main(String[] args) {
Scanner scanner =new Scanner(System.in);
int n=scanner.nextInt();
System.out.println(fun(0,n));
}
}
但是!!!
测试情况却是:(超时)
这种情况出现的原因是出现了重复计算,我们通过分析程序的调用栈来说明这种递归方式如何产生出这种情况的。
上述程序调用栈如下图所示(以n==5为例):
可以见到,fun(1,3),fun(0,3)被重复算了一次,fun(1,2)被重复算了3次,如果n给的特别大,我们将重复计算很多内容,所以我们试想通过建立一个数组,将计算过的数据保存到一个数组中,当再次遇到这个数据时,不必再次计算,直接使用即可。我们称之为记忆化搜索 (空间换取时间!),这也时dp的主要思想。
改进后的代码:
import java.util.Scanner;
public class P1044 {
static long [][] data=new long[51][51];//[i][n],保存结果的数组
static long fun(int i,int n) {
//递归写法
if (n==0) {
return 1;
}
if (i==0) {
if (data[i+1][n-1]==0) {
//判断条件为,如果没有计算,就进行计算
data[i+1][n-1]=fun(i+1, n-1);
}
return data[i+1][n-1];
}
if (data[i+1][n-1]==0) {
data[i+1][n-1]=fun(i+1, n-1);
}
if (data[i-1][n]==0) {
data[i-1][n]=fun(i-1, n);
}
return data[i+1][n-1]+data[i-1][n];
}
public static void main(String[] args) {
for (int i = 0; i < 51; i++) {
data[i][0]=1;
}
Scanner scanner =new Scanner(System.in);
int n=scanner.nextInt();
System.out.println(fun(0,n));
}
}
这样我们就AC过了!(欧耶)
P1255 数楼梯
题目内容
题目描述
楼梯有 NN 阶,上楼可以一步上一阶,也可以一步上二阶。
编一个程序,计算共有多少种不同的走法。输入格式
一个数字,楼梯数。
输出格式
输出走的方式总数。
输入输出样例
输入
4
输出
5
说明/提示
对于 60% 的数据,N≤50;
对于 100% 的数据,N≤5000。
题目分析:
还是一样,记忆化搜索递归才能做出来,不然n=50就已经开始卡了。
找递归边界和递归关系式:
因为走楼梯可以走两步或者一步,用n表示剩余的楼梯,当前问题等于走一步fun(n-1)加上fun(n-2)这两个子问题的解。当剩余一步时,只能有一种情况,当剩余两步时,可以走1步后再走一步到,也可以直接走两步,有两种情况。
递归边界:
if(n==1)return 1;
if(n==2)return 2;
递归关系式
return fun(n-1)+fun(n-2);
最后别忘了记忆化搜索!!!(空间换取时间)
AC代码:
//写这道题时,long都爆炸了,用的是BigInteger。
import java.math.BigInteger;
import java.util.Scanner;
public class P1255{
static BigInteger [] data=new BigInteger[5010];
static BigInteger fun(int n) {
if (n==0) {
return BigInteger.valueOf(<