目录


🎬 云泽Q :个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列》
⛺️遇见安然遇见你,不负代码不负卿~
前言
大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~
一、递归初阶
学会从宏观的角度看待递归。
1. 什么是递归?
函数自己调用自己。
2. 为什么会用到递归?
本质:在处理主问题时,需要解决子问题,两者的处理方式完全一致。问题 -> 相同的子问题 -> 相同的子子问题...... 直到子问题不能继续拆分
3. 从宏观角度看待递归!
不要在意递归的细节展开图 --- 写完代码不要再去纠结递归展开图;
把递归函数当成一个黑盒 --- 赋予这个黑盒一个任务;
相信这个黑盒一定能帮助我们完成这个任务。
4. 如何写好一个递归:
先找到相同的子问题 -> 确定函数的功能以及函数头的设计;
只关心某一个子问题是如何解决的 -> 函数体
不能继续拆分的子问题 -> 递归出口


1.1 汉诺塔
该题目源自信息学奥赛一本通,要注意一下这个网站的题目提交是有次数限制的,所以每次提交的时候最好要保证自己的代码是正确的,不然提交几次之后发现不能提交了就要等到明天了

问题拆解核心:将「移动 n 个盘子」的大问题,拆解为 3 个可解决的小问题(递归思想):
- 先把上面 n-1 个盘子从源柱子(x),借助目标柱子(z),移到辅助柱子(y);
- 再把最底层的第 n 个盘子直接从源柱子(x)移到目标柱子(z)(这是唯一一步直接移动最大盘子的操作);
- 最后把辅助柱子(y)上的 n-1 个盘子,借助源柱子(x),移到目标柱子(z)。
递归终止条件:当 n=0 时(没有盘子需要移动),直接返回,结束递归。
图中代码逻辑对应下来
对应到n = 3的例子就是:
总问题:x上3个盘子要借助y(辅助柱子)的帮助放到z(终点柱子)上
子问题:a的n - 1个盘子借助z(终点柱子)的帮助放到y(辅助柱子)上
注意这里仅需关心一个子问题如何实现的即可
以上就是n = 3的第一步
n = 3的第二步
a上最下面的柱子放到终点柱子上,x柱子上第n个盘子先转移到z上
第三步:
将刚刚放到辅助柱子y上那些n - 1盘子,借助x的帮助,全部放到终点柱子z上
处理递归出口:当 n = 0的时候,也就是没有盘子的时候,就没有东西可以转移了
这中递归题目就是从宏观角度分析问题,只要发现问题中有重复子问题就可以用递归来写,并且写递归时只要关心重复子问题是怎么解决的就可以写出来递归逻辑,整体是个非常丝滑的过程,就是理清子问题,相信自己和代码,然后大力出奇迹
a上面的n个盘子借助c的帮助全部转移到b上
cpp
#include<iostream>
using namespace std;
int n;
char a, b, c;
//x柱子上的n个盘子经过y的帮助下放到z上
void dfs(int n, char x, char y, char z)
{
if(n == 0) return;
//子问题:x上的n - 1个盘子经过z的帮助下放到y上
dfs(n - 1, x, z, y);
cout << x << "->" << n << "->" << z << endl;
//printf("%c->%d->%c\n", x, n, z);
dfs(n - 1, y, x, z);
}
int main()
{
cin >> n >> a >> b >> c;
//a上的n个盘子经过c的帮助下放到b上
dfs(n, a, c, b);
return 0;
}
要点补充:这道题目如果输出用cout会运行超时:
运行超时的原因 ⏱️
当 n 取最大值 20 时,总移动次数是 2^20^ - 1 = 1,048,575 次,需要输出超过 100 万行:
- cout 默认配置效率极低:它默认开启了与 C 标准 IO 的同步(ios_base::sync_with_stdio(true)),每次输出都会有额外开销,大量输出时会超过 1000ms 的时间限制,导致「运行超时」。
- printf 效率更高:C 风格的格式化输出没有同步开销,速度远快于默认配置的 cout,所以不会超时。
1.2 占卜DIY
一副去掉大小王的 52 张扑克分为 13 堆(编号 1~13,每堆 4 张),第 13 堆为生命牌(共 4 条命),4 张 K 为 "死神"。按规则模拟抽牌、放牌流程:
- 抽取生命牌最上面一张,正面朝上放到对应数字的堆顶。
- 从刚放牌的堆最底部抽一张,重复放牌操作。
- 抽到 K 则丢弃,死一条命,重新从生命牌开始。
- 4 条命耗尽后,统计 "开了多少对":同一数字的 4 张牌都正面朝上(K 不算)。

cpp
#include<iostream>
using namespace std;
const int N = 14;
int n = 13, m = 4;
//矩阵索引0~13,0~4,覆盖1~13堆编号,1~4牌位置
int a[14][5];
//数组的索引范围是 0~13,覆盖1~13堆牌
int cnt[N];
void dfs(int x)
{
//拿到k结束递归
if(x == 13) return;
int t = a[x][cnt[x]];
cnt[x]--;
dfs(t);
}
int main()
{
for(int i = 1; i <= n; i++)
{
cnt[i] = 4;
for(int j = 1; j <= m; j++)
{
char ch; cin >> ch;
if(ch >= '2' && ch <= '9') a[i][j] = ch - '0';
else if(ch == 'A') a[i][j] = 1;
else if(ch == 'J') a[i][j] = 11;
else if(ch == 'Q') a[i][j] = 12;
else if(ch == 'K') a[i][j] = 13;
//字符0
else a[i][j] = 10;
}
}
//开始摸生命牌
for(int i = 1; i <= m; i++)
{
dfs(a[n][i]);
}
//统计结果,看cnt数组中有多少个0
int ret = 0;
for(int i = 1; i <= n; i++)
{
if(cnt[i] == 0) ret++;
}
cout << ret << endl;
return 0;
}
1.3 FBI树
FBI树


【解法】
重复子问题:处理每一棵子树:
- 确定出该子树的类型;
- 然后从中间分开,先处理左子树,再处理右子树;
- 然后打印该子树的类型。
如何快速判断出该子树的类型?因为我们要求的是一段区间内 1 的个数,我们可以利用「前缀和」数组求出这段区间和,然后在查询某段区间时,判断一下此时的区间和:
- 如果等于区间长度,说明是 I 类型;
- 如果等于 0,说明是 B 类型;
- 否则就是 F 类型。
cpp
#include<iostream>
using namespace std;
const int N = 10;
int n;
//大小开2^N(字符串长度)
int f[1 << N];
void dfs(int left, int right)
{
//区间不存在的时候,递归终止
if(left > right) return;
//先判断类型
char ret;
//区间长度
int sum = f[right] - f[left - 1];
if(sum == 0) ret = 'B';
else if(sum == right - left + 1) ret = 'I';
else ret = 'F';
if(left == right)
{
cout << ret;
return;
}
//左右区间处理完毕,输出当前结点类型
int mid = (left + right) / 2;
dfs(left, mid); dfs(mid + 1, right);
cout << ret;
}
int main()
{
cin >> n;
//扩大到字符串长度
n = (1 << n);
for(int i = 1; i <= n; i++)
{
char ch; cin >> ch;
int t = 0;
if(ch == '1') t = 1;
//预处理前缀和数组
f[i] = f[i - 1] + t;
}
//传入需要构建 + 后序遍历的区间
dfs(1, n);
return 0;
}
结语
