有些事情本来很遥远,你争取,它就会离你越来越近
- [概念 + 名词解释](#概念 + 名词解释)
- [dfs 回溯类问题](#dfs 回溯类问题)
- [dfs 迷宫搜索类问题](#dfs 迷宫搜索类问题)
概念 + 名词解释
递归
是指在一个函数的定义中调用自身的过程,有些复杂问题可以划分为多个相同子问题的话,就可以大概率可以使用递归来解决。经典算法如暴搜
、回溯
、深度优先遍历(dfs)
、FloodFile算法
、记忆化搜索
等本质上都是使用递归来解决,换句话来说,这些本质上来说都是一种问题。
如何理解递归
初学者可以在做递归简单问题的时候,画递归展开图来帮助理解递归,如斐波那契数列问题 f(n) = f(n - 1) + f(n - 2),要求出f(4),就得先递归去求解 f(3)和 f(2);
当学习了数据结构之后呢,可以做一些二叉树相关
的 OJ 题目去理解递归,但二叉树刚开始做其实都是非常迷的,但慢慢理解后,好像确实就只是这么个事哈哈。
当做了一些简单递归题目和二叉树后,就可以尝试从宏观上理解递归了,可以将递归问题分为三步:
- 不细想递归展开图,越复杂的问题越不能画递归展开图,递归展开图只适用于在简单题中理解递归
- 把递归的函数想象成一个
黑盒
,你让这个黑盒去完成一个任务,并信任这个黑盒一定能完成。只需要设计好这个黑盒即可! - 将复杂问题的主逻辑写出来,只在乎递归的结果,并将这个问题解决
那么问题就变为了,如何设计递归函数,也就是如何设计一个这样的黑盒?
首先,找到题目中的多个相同子问题------递归函数头的设计
只关心其中一个子问题是如何解决的------递归函数体的设计
根据经验或题目要求再确定一下递归的出口即可
单看比较抽象,使用一个例子来解释一下上面的步骤:
面试题------汉诺塔
汉诺塔问题,大家都听过吧,将多个圆盘从一个柱A 借助 柱B 移动到 柱C,前提是不小的盘子必须放在大的盘子的上面,问一共有多少种解法?
当然汉诺塔问题是有公式解决的的,但这没有任何意义,我们的目的是从宏观上理解递归。
当有n个圆盘的时候,我们可以先将(n-1)个圆盘移动到 塔2 上面,然后再将塔1的最后一个大圆盘移至 塔3,再用同样的方法将塔2上的(n-1)个元素借助塔1移至塔3,要将这 n - 1 个移动,就要先移动 n - 2,如此往复...
找到了重复子问题:要移动 n 个,就要先解决 n - 1 个怎么搞,要解决 n - 1 个,就要先解决 n - 2 个怎么搞...,并且函数头需要将这几个柱子全部传过去
那么其中一个子问题需要怎么解决呢?
假设有 3个柱子ABC吗,n 个盘子,那么需要先将 n - 1 个盘子借助 C 先移动到 B,再将地下那个最大的移动到 C ,然后A 空了,再借助 A 将 B 上的 n - 1 个盘子移动到 C上,就完成了任务。说明在 n 减到 1 之前,都需要重复上面的步骤,先将 n -1 个盘子借助一个柱子移动到另一个上,然后将第 n 个移动 。那么函数体的设计:
那么,在 n > 1 的时候就要一直递归,直到 n = 1 时才进行第一次返回,所以递归出口就是 n == 1,递归返回前,需要将盘子从 A 移动到 C,然后返回。
汉诺塔力扣代码:
cpp
class Solution {
public:
void _haota(vector<int>& A, vector<int>& B, vector<int>& C, int n)
{
if(n == 1)
{
C.push_back(A.back());
A.pop_back();
return;
}
_haota(A, C, B, n - 1);
C.push_back(A.back());
A.pop_back();
_haota(B, A, C, n - 1);
}
void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {
int n = A.size(); // 需要移动的盘子数量
_haota(A, B, C, n);
}
};
其实我也不知道自己到底说没说明白...
dfs 回溯类问题
步骤:
先使用决策树画出所有情况,如对 [1, 2, 3] 进行全排列
使用 全局变量path 来记录某条路径上的结果,在决策树画到底部的时候,判断 path 中的结果是否符合结果,然后需要"向上返回",并且顺便"恢复现场",如上图中,path 中在底部变成了 123,恢复现场后就成了 12;如果没有可以继续走的路,就继续"向上返回",再恢复现场,然后判断是否有其他路径可以走,遍历完决策树中所有情况后,就暴搜出了所有情况。
剪枝
即剪去不合理的分支,当全排列的时候,某个数字被列举了之后,就不能再使用了,因此要剪去这个分支!一般我们可以使用 bool 类型的check[] 数组来实现剪枝。
全排列力扣代码
cpp
class Solution {
public:
vector<vector<int>> ret;
vector<int> path;
bool check[7] = { false };
vector<vector<int>> permute(vector<int>& nums) {
dfs(nums);
return ret;
}
void dfs(vector<int>& nums)
{
if(path.size() == nums.size())
{
ret.push_back(path); // 将合理路径放入最终结果
return;
}
for(int i = 0; i < nums.size(); i++)
{
if(!check[i])
{
check[i] = true;
path.push_back(nums[i]);
dfs(nums);
check[i] = false; // 恢复现场
path.pop_back(); // 恢复现场
}
}
}
};
蓝桥杯------飞机降落
这道题的数据要求不算高,我们就可以考虑将所有情况都枚举一遍,也就是暴搜!
cpp
#include<bits/stdc++.h>
using namespace std;
// 飞机降落
struct plane
{
int a; // 到达时间
int b; // 可以盘旋的时间
int c; // 降落所需的时间
};
int T; // 组数
int N; // 每组的飞机数
plane vp[12];
bool check[12];
bool dfs(int num, int last)//num 为第几个, last 为上一架的降落时间
{
if(num == N)
{
return true;// 能走到这一步,说明符合题意且结束了
}
for(int i = 0; i < N; i++)
{
if(!check[i] && vp[i].a + vp[i].b >= last)// 说明可以降落
{
check[i] = true;
if(dfs(num + 1, max(last, vp[i].a) + vp[i].c)) return true;
check[i] = false;
}
}
return false;// 全部都遍历, 如果没有满足情况的话, 说明不行, 就返回
}
int main()
{
cin >> T; // 组数
for(int i = 0; i < T; i++)
{
cin >> N;
for(int i = 0; i < N; i++)
{
cin >> vp[i].a >> vp[i].b >> vp[i].c;
}
if(dfs(0, 0)) cout << "YES" << endl;
else cout << "NO" << endl;
}
return 0;
}
dfs 迷宫搜索类问题
搜索类问题一般都是在二维矩阵中进行,以某个坐标为起点进行一次/多次深度优先遍历,直到遇到某个位置停止。
在二维矩阵中搜索,一般可以定义两个向量数组
cpp
// 当题目要求只能上下左右四个方向走时
int dx[4] = { 0, 0, 1, -1 };
int dy[4] = { 1, -1, 0, 0 };
// 如当前的位置坐标为 (i, j), 那么可以走的位置有:(矩阵 m 行 n 列)
for(int k = 0; k < 4; k++)
{
int x = dx[k] + i, int y = dy[k] + j;
if(x >= 0 && x < m && y >= 0 && y < n && !visit[x][y] && /* 题目要求条件*/)
{
visit[x][y] = true;
dfs(x, y, /**/);
}
}
cpp
// 当题目要求可以上下左右、斜左上、斜右上、斜左下、斜右下这8个方向走时
int dx[4] = { 0, 0, 1, -1 , 1, 1, -1, -1};
int dy[4] = { 1, -1, 0, 0 , -1, 1, -1, 1};
for(int k = 0; k < 8; k++)
{
int x = dx[k] + i, int y = dy[k] + j;
if(x >= 0 && x < m && y >= 0 && y < n && !visit[x][y] && /* 题目要求条件*/)
{
visit[x][y] = true;
dfs(x, y, /**/);
}
}
还会定义一个 visit[][] 数组,用来记录某个位置是否被遍历过了
参考代码:
cpp
class Solution {
public:
// 当题目要求只能上下左右四个方向走时
int dx[4] = { 0, 0, 1, -1 };
int dy[4] = { 1, -1, 0, 0 };
// 当题目要求可以上下左右、斜左上、斜右上、斜左下、斜右下这8个方向走时
int dx[4] = { 0, 0, 1, -1 , 1, 1, -1, -1};
int dy[4] = { 1, -1, 0, 0 , -1, 1, -1, 1};
bool visit[16][16] = { false };
int ret = 0;
int getMaximumGold(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
int gold = 0;
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
{
if (grid[i][j] != 0)
{
visit[i][j] = true;
gold += grid[i][j];
dfs(grid, i, j, gold);
gold -= grid[i][j];
visit[i][j] = false;
}
}
}
return ret;
}
void dfs(vector<vector<int>>& grid, int i, int j, int gold)
{
for (int k = 0; k < 4; k++)
{
int x = i + dy[k], y = j + dx[k];
if (x >= 0 && x < grid.size() && y >= 0 && y < grid[0].size() &&
grid[x][y] != 0 && !visit[x][y]) {
gold += grid[x][y];
visit[x][y] = true;
dfs(grid, x, y, gold);
gold -= grid[x][y];
visit[x][y] = false;
}
}
ret = max(ret, gold);
return;
}
};
蓝桥杯------岛屿个数
cpp
int T;
int M, N;
int nums[12][12];
bool visit[12][12];
int dx[4] = {0, 0, 1, -1}, dy[4] = {1, -1, 0, 0};
int dxx[8] = {0, 0, 1, -1, 1, 1, -1, -1};
int dyy[8] = {1, -1, 0, 0, -1, 1, 1, -1};
int ret = 0;
void dfs_board(int i, int j)
{
for(int k = 0; k < 4; k++)
{
int x = i + dx[k];
int y = j + dy[k];
if(x >= 1 && x <= M && y >= 1 && y <= N && !visit[x][y] && nums[x][y] == 1)
{
visit[x][y] = true; // 遍历陆地
dfs_board(x, y);
}
}
}
void dfs_sea(int i, int j)
{
for(int k = 0; k < 8; k++)
{
int x = i + dxx[k];
int y = j + dyy[k];
if(x >= 1 && x <= M && y >= 1 && y <= N && !visit[x][y] && nums[x][y] == 0)
{
visit[x][y] = true; // 将外海全部标出来
dfs_sea(x, y);
}
if(x >= 1 && x <= M && y >= 1 && y <= N && !visit[x][y]
&& nums[x][y] == 1)
{
visit[x][y] = true;
ret ++; // 陆地数量 + 1
dfs_board(x, y); // 当遇到陆地的时候,就去搜索这个陆地的连通块
}
}
}
int main()
{
cin >> T;
while(T--)
{
ret = 0;
memset(nums, 0, sizeof(nums));
memset(visit, 0, sizeof(visit));
cin >> M >> N;
for(int i = 1; i <= M; i++) // 将数据存入 nums 二维数组
{
string str;
cin >> str;
for(int j = 1; j <= N; j++)
{
nums[i][j] = str[j - 1] - '0'; // 将数据输入,只有 0 和 1
}
}
int flag = 0;
for(int i = 1; i <= M; i++)
{
for(int j = 1; j <= N; j++)
{
if((i == 1 || i == M || j == 1 || j == N) &&
nums[i][j] == 0 && !visit[i][j]) // 在边缘位置找外海
{
flag = 1;
visit[i][j] = true;
dfs_sea(i, j); // 从外海往进渗透,有8个方向
}
}
}
if(flag == 0) ret = 1;
cout << ret << endl;
}
return 0;
}