文章目录
递归的定义与实现原理
定义:函数调用自身的编程技巧称为递归
为什么函数要调用自己呢?
答:因为递归的下一层状态是由上一层推来的,函数一次次调用自己、一层层推下去一直到末尾就能得到答案
那既然是从一个起始状态一直推到末尾状态,那为啥不用循环而要用递归呢?
答:因为递归的灵魂在于 “归” 字,每次走到末状态后回到上一层,这个过程就叫 回溯 ,每次走到结尾更新答案后,回溯到上一层,上一层可以 继续递归调用到另外一种状态 ,这种返回上一层再推到下一层的操作是循环无法完成的
可能已经看晕了,那我们看图理解
例①:求ABC三个字母的所有排列
那么它的求解过程应该是
从起点开始,一路走A —> AB —> ABC,在走到第一个末尾ABC的时候,又要重新回到上一层AB,发现这层到不了其他状态了,于是再回到上一层A,发现A还可以到AC,于是又递归到下一层状态AC去,以此类推。
思考一下如果用for循环写,就需要3层for循环,那假如有26个字母,那就是26层for循环。。。
但如果是递归,当回溯到 A 后,又可以直接递归到AC去
那这个递归这么神奇,它是怎么实现的呢?
答:我们在开头说了,递归是函数自己调用自己,那不就死循环了吗?说得对!所以为了防止死循环,在函数每次调用自己的时候都要告诉下一层它目前的状态,然后判断这个状态是不是结尾,如果是的话就结束递归
还是拿上一个例子说明,ABC三个字母排列,状态肯定是长度,初始长度是0(因为啥字母都没加呢),加了A之后,调用下一层,并对下一层说 :现在的长度是1,我们只要3就够了,千万别加多了,然后第二层加了个 B 变成了 AB,第二层也照葫芦画瓢的对下一层说 现在长度是2,一直到最后变成ABC之后,下一层听到上一层说长度已经到 3 了,它就不继续加了,而是直接 r e t u r n return return 回去
代码如下
char s[5];
int vis[5];
void solve(int len=0){
if(len==3){//判断是不是结尾
printf("%s\n",s);
return ;//是就不用加了,直接输出答案并返回上一层
}
for(int i=0;i<3;i++){
if(vis[i])continue;
s[len]='a'+i;
vis[i]=1;//标记这个字母已经用过了
solve(len+1);//加了字母之后调用下一层并告诉它现在的长度
vis[i]=0;//这个字母找完了要把标记还原
}
}
根据上面的例子,我们了解了,递归有如下几个步骤
- 找到初状态,末状态,状态的变化
- 判断是否到达末状态,若是结束当前状态,不再继续到下一层,若不是则执行步骤3
- 选取下一个合法的状态继续向下一层递归执行步骤 2 2 2,并告诉下一层目前的状态,若找不到合法状态,就返回上一层执行步骤 3 3 3
就是这么一个套娃的过程,直到所有状态都找完程序就结束了
什么时候需要用递归呢?
答:你可以根据题目要求画个图,只要找答案的过程是一个 树形 的结构就需要用到递归
例题
bzoj 1621 分岔路口
题意:
初始一共有
N
N
N 头奶牛,每到一个分岔路口,如果这群奶牛的数量可以分成
X
X
X和
X
+
K
X+K
X+K的话,就分裂并继续往下走,否则就留在原地,问最后这群奶牛最后分成了多少群
Example
input
6 2
output
3
样例是这样的
看到这个树形的结构,很明显是个递归问题,我们按照前面说的步骤来:
初状态就是奶牛未分裂的数量
N
N
N
末状态是
A
A
A 不能分成
B
B
B 和
B
+
K
(
1
≤
B
)
B+K(1 \le B)
B+K(1≤B) ,化简一下就是
A
≤
K
+
2
A\le K+2
A≤K+2 且
(
A
−
K
)
%
2
=
0
(A-K)\%2=0
(A−K)%2=0
状态变化是当前奶牛数量
A
A
A 分为两个相差
K
K
K 的数,即
(
A
−
K
)
/
2
(A-K)/2
(A−K)/2和
(
A
+
K
)
/
2
(A+K)/2
(A+K)/2
int solve(int A){
if(A-K<2||(A-K)%2==1){
//到了末状态,返回这一状态的答案,不继续往下递归
return 1;
}
//否则就继续到下一层状态去
int ans=solve((A-K)/2)+solve((A+K)/2);
//这一层的答案就是它分裂的两堆的答案的和
return ans;
}
int main(){
int ans=solve(N);
}
对应到样例那张图就是