【本文适合对递归不熟悉的人(比如我)看】
简介:递归函数是多重for循环的简洁写法,它的时间复杂度可以和暴力枚举多重for循环一样。但是它的代码更加简洁且可读性更强。
下面是我的思考逻辑:如果我们把递归函数看成是一个操作者,且每次调用递归的递归函数都是不一样的操作者,也就是说,递归函数被调用5次就有5个操作者参与了解题过程。如此,我们需要明确两点,其一,递归何时结束,即最后一个操作者是谁,他满足什么条件。其二,就是这个操作者在本函数里需要做点什么。那么在这样的背景下,每个操作者要干的事都是一样的。因为我们用递归方式解题的条件就是——大问题分解后的小问题的解决方式相同。
下面是我举的一些递归例子,都是是笔者在学习的时候做的递归题,不过有些题目我进行了改编,以方便笔者阐述总结自己的观点。(这些例子不是斐波那契数列,也不是n的阶乘这些经典题目)
题目一
有五个空位
每个空位要么是男生要么是女生
问有多少种排列情况?
男生用0表示,女生用1表示,列出所有的情况
相信大家看到这道题一定倍感不屑,一道初中生都会做的题。根本不需要递归,一个公式就可以解决。嘿嘿,不过别急,耐心往后看,这道题只是抛砖引玉。
下面就这道简单的题开始阐述我对递归的看法:
就像我在开始的时候说的,假定有一个递归函数(很多个做相同工作的操作者),那么每个人需要做什么相同的工作才能解决这个问题呢?显然,每个人只需要在自己所负责的空位上安排一个男生或者女生,并且把后面一个位置的安排交给下一个人就可以了。
好的,下面是代码
int ans = 0; // 排列数
int ansArr[nc]; // 用来记录每一个位子的人的性别,nc是5
void findAns(int which,int total) // total表示现在已经排好的人数
{
if (total == nc)
{
ans++;
for (int cn = 0; cn < 5; cn++)
cout << ansArr[cn];
cout << endl;
return;
}
for (int i = 0; i < 2; i++)
{
ansArr[which] = i; // 0表示男生 1表示女生
findAns(which + 1, total + 1);
}
}
因为每个操作者既可以安排男生又可以安排女生,每种安排都需要把这种情况交给下一个操作者。所以有一个for循环。
想必这道题一定让你直呼不过瘾!好吧,实在抱歉(⊙x⊙;),我们继续看下一道题
题目二
X星球要派出一个5人组成的观察团前往W星。
其中:
A国最多可以派出4人。
B国最多可以派出2人。
C国最多可以派出2人。
D E F国分别可以派出1 1 3人
问最终派往W星的观察团会有多少种国别的不同组合?
此题改编于蓝桥杯的一道填空题。
现在这道题与上一道题类似,本质上也是“填人”的问题。
如果这道题沿用上一道题的思路,那么就会有些许麻烦,因为每一个位子上可以安排上六种人。后面的操作者还要考虑经过前面操作者的安排,某国人是否已经没了,所以会有点麻烦。那要怎么样才能让每个操作者都做同样的简单的事呢?经过一番思考,宁想到可以"让一个操作者负责一个国家的人员安排"。
好的,下面是代码
int nmax[6] = { 4,2,2,1,1,3 }; // 每个国家能派出的最多人数
int ans = 0; // 最后排列个数
char anstr[6]; // 记录每一个位子上的人的国籍
// which是第几个国家,顺序与上面的数字对应;
// total是操作者操作后已安排的总人数;
// j用来记录anstr的下标,也就是下一个操作者开始操作的位置
void findAns(int which, int total)
{
if (total == nhuman)
{
ans++;
for (int cn = 0; cn < 5; cn++)
cout << anstr[cn];
cout << endl;
return;
}
// i是第which国的操作者派遣的which国人数
for (int i = 0; i <= nmax[which]; i++)
{
for (int k = total, ic = 0; ic < i; ic++, k++)
anstr[k] = which + 'A';
findAns(which + 1, total + i); // 交给下一个操作者
}
}
你可能会问,如果某一国的最多人数超过了5呢?很简单,在第一个for循环语句后面加一个if条件判断把所有语句框进去即可。
我在做递归题的时候最难的就是不知道递归函数的参数列表该怎么写,参数该怎么传给下一个操作者。就以这个题为例,我最开始并没有想到要设一个total数据记录已有的人数。最开始total位置我直接传的i,那么我们可以思考,为什么要引入total数据呢?
我的结论是:我们需要关注——"递归传入的参数是否在每一种递归情况的函数里都会被用到"。也就是说所有的操作者是不是都需要这个数据,显然,每个操作者都需要已经安排的总人数(total)而不是前一个操作者安排的人数(i),他们也需要知道自己是第几个国家的操作者(which+1)。
大家都发现了,上面两道题的递归函数里都用到了for循环。最开始说了,递归就是for循环的简洁形式,所以第一题还有其他方法。
每个操作者为了找到所有情况,先安排一个男生,然后再传给下一个操作者,下一步安排一个女生,再传给下一个操作者。代码如下:
void findAns(int which,int total) // total表示现在已经排好的人数
{
if (total == nc)
{
ans++;
for (int cn = 0; cn < 5; cn++)
cout << ansArr[cn];
cout << endl;
return;
}
//for (int i = 0; i < 2; i++)
//{
// ansArr[which] = i; // 0表示男生 1表示女生
// findAns(which + 1, total + 1);
//}
ansArr[which] = 1;
findAns(which + 1, total + 1);
ansArr[which] = 0;
findAns(which + 1, total + 1);
}
相信宁看到这里已经明白我的想法了!(ノ*・ω・)ノ。我们把函数主体实现看做是操作者需要进行的操作,可以从多个思维角度去思考具体执行什么操作,这种思维也就是分治思想。
感谢宁能耐着性子看到这里。如果宁想再看几个递归的题的话,下文就是( ̄︶ ̄)↗ 。
第三题
输入括号的对数,输出可以组成有效对的组数
具体来说:
像")()("这样的就不叫"有效对"
解题:总体而言,每个操作者都要在自己负责的位子上填左括号,填完左括号传给下一个操作者,再填完右括号,传给下一个操作者。(先填左括号还是先填右括号无所谓)但是只有在满足某些条件的情况下才能填左括号或者右括号(因为结果必须是"有效对",有限制了)。下面就去思考什么时候填左括号什么时候填右括号——当左括号个数少于括号对数n的时候可以填左括号、当右括号数少于左括号数时可以填右括号。
下面是完整代码:
// 版权: Tongji University.
// 文件名: CombinationOfParentheses.cpp
#include <iostream>
using namespace std;
int Map[16]; // 记录括号的情况 1表示左,0表示右
int Answer = 0;
// 函数功能:打印括号
void fnPrintParentheses(int Maxx)
{
char Parentheses[16] = { '\0' };
for (int i = 0; i <= Maxx - 1; i++)
{
if (Map[i] == 0)
Parentheses[i] = '(';
else if (Map[i] == 1)
Parentheses[i] = ')';
}
cout << "\"";
for (int i = 0; i <= Maxx - 1; i++)
cout << Parentheses[i];
cout << "\"" << endl;
}
void dfs(int src, int Maxx, int LeftParNum, int RightParNum, int iCounter = 1)
{
if (src == Maxx)
{
Answer++;
if (Answer == 1)
cout << "所有括号组合如下: " << endl;
fnPrintParentheses(Maxx);
}
else
{
if (LeftParNum > RightParNum)
{
Map[iCounter] = 1;
dfs(src + 1, Maxx, LeftParNum, RightParNum + 1, iCounter + 1);
} // 当左括号数多于右括号时可填右括号
if (LeftParNum < Maxx / 2) //
{
Map[iCounter] = 0;
dfs(src + 1, Maxx, LeftParNum + 1, RightParNum, iCounter + 1);
} // 当左括号数少于于总括号数的一半时可填左括号
}
}
int main()
{
int n;
cout << "请输入括号的对数: ";
while (1)
{
cin >> n;
if (!cin)
{
cerr << "Error,CIN Failed !!!";
return -1;
}
if (n < 1 || n > 8)
{
cerr << "Element range error";
return -1;
}
else
break;
}
dfs(1, n * 2, 1, 0); // 起始第一个肯定是左括号 1
cout << "共有 " << Answer << " 组" << endl;
return 0;
}
第四题——汉诺塔
分别输入盘子的数量、起始柱和终点柱
输出移动的具体步骤
int ans = 0;
// n是盘号,从上到下依次为1、2、3...
void hanoi(int n, char src, char tmp, char dst)
{
if (n == 1)
{
cout << setw(2) << n << "# " << src << "-->" << dst << endl;
ans++;
return;
}
// 每个操作者把上面n-1层放到空柱上再把自己手上的第n层放到目标柱上
hanoi(n - 1, src, dst, tmp);
// 每个操作者把第n个放到目标柱上,此时直接输出就可。
cout << setw(2) << n << "# " << src << "-->" << dst << endl;
ans++;
// 每个操作者把空柱上的n-1层放到目标柱上,这件事需要下一个操作者去完成。
hanoi(n - 1, tmp, src, dst);
return;
}
注意这道题每个操作者干的事是有顺序的。
第五题——合成十
输入一段随机数组成的矩阵,比如:
1 2 2 2 1
2 1 3 2 2
3 1 3 1 2
再输入坐标比如2行5列,找到在水平方向和竖直方向上和他相连的相同的数,以[2,5]为起始点的话就有[2,4][1,4][1,3][1,2][3,5],有5个数
故输出5
a数组用来记录矩阵每个元素,t用来记录这个位置查过了没有,1表示查过0表示没查过
int digui(int i, int j, int a[11][13], int t[11][13])
{
static int count = 0;
if (a[i][j] == a[i - 1][j] && t[i - 1][j] != 1)
{
t[i - 1][j] = 1;
count++;
digui(i - 1, j, a, t);
}
if (a[i][j] == a[i + 1][j] && t[i + 1][j] != 1)
{
t[i + 1][j] = 1;
count++;
digui(i + 1, j, a, t);
}
if (a[i][j] == a[i][j + 1] && t[i][j + 1] != 1)
{
t[i][j + 1] = 1;
count++;
digui(i, j + 1, a, t);
}
if (a[i][j] == a[i][j - 1] && t[i][j - 1] != 1)
{
t[i][j - 1] = 1;
count++;
digui(i, j - 1, a, t);
}
return count;
}
思路就不废话了,相信大家已经可以自己理解了(~ ̄▽ ̄)~。
本文到此结束,谢谢!( •̀ ω •́ )✧。
#------2022年4月4日------#