递归,回溯,DFS,Floodfill,记忆化搜索


一、 递归 (Recursion)

1. 核心原理

递归是一种函数通过调用自身来解决问题的方法。其核心是将一个大规模问题分解为与原问题结构相同、但规模更小的子问题来求解。

递归三要素:

  1. 定义 (Function Definition): 明确递归函数的输入、输出和功能。例如,fib(n) 的功能是计算第 n 个斐波那契数。
  2. 分解 (Decomposition): 将问题分解为子问题,并通过调用自身来解决。例如,fib(n) = fib(n-1) + fib(n-2)
  3. 基准情况 (Base Case): 定义一个或多个终止条件,防止无限递归。例如,fib(0) = 0, fib(1) = 1

2. 经典问题与代码实现

问题1:斐波那契数
  • 问题描述: 计算斐波那契数列的第 n 项。
  • 技术点: 纯粹的递归分解,但存在大量的重复计算,导致时间复杂度为指数级 O(2n)。
  • 优化思路 (记忆化搜索): 使用一个 memo 数组(备忘录)来存储已计算过的子问题的解。在计算前先检查备忘录,如果存在则直接返回,避免重复计算,将时间复杂度降至 O(n)。

优化后的C++实现 (记忆化搜索)

C++

复制代码
#include <vector>

class Solution {
public:
    int fib(int n) {
        if (n < 2) {
            return n;
        }
        // memo数组初始化为-1,表示未计算
        std::vector<int> memo(n + 1, -1);
        return dfs(n, memo);
    }

private:
    int dfs(int n, std::vector<int>& memo) {
        // Base Case
        if (n < 2) {
            return n;
        }
        // 如果已经计算过,直接返回结果
        if (memo[n] != -1) {
            return memo[n];
        }
        // 计算并存入备忘录
        memo[n] = dfs(n - 1, memo) + dfs(n - 2, memo);
        return memo[n];
    }
};

复杂度分析

  • 时间复杂度 : O(n),因为每个子问题 dfs(i) 只被计算一次。
  • 空间复杂度 : O(n),递归栈的深度和 memo 数组的空间。
问题2:汉诺塔
  • 问题描述: 将 N 个盘子从源柱 A 借助辅助柱 B 移动到目标柱 C。

  • 技术点:

    经典的递归分治问题。

    1. n-1 个盘子从 A 移动到 B (借助 C)。
    2. 将第 n 个盘子从 A 移动到 C。
    3. n-1 个盘子从 B 移动到 C (借助 A)。

**C++ 实现 **

复制代码
#include <vector>

class Solution {
public:
    void hanota(std::vector<int>& A, std::vector<int>& B, std::vector<int>& C) {
        dfs(A.size(), A, B, C);
    }

private:
    // 将 n 个盘子从 src 移动到 dest,借助 aux
    void dfs(int n, std::vector<int>& src, std::vector<int>& aux, std::vector<int>& dest) {
        // Base Case: 只有一个盘子,直接移动
        if (n == 1) {
            dest.push_back(src.back());
            src.pop_back();
            return;
        }

        // 1. 将 n-1 个盘子从 src 移到 aux
        dfs(n - 1, src, dest, aux);
        
        // 2. 将 src 剩下的最大盘子移到 dest
        dest.push_back(src.back());
        src.pop_back();

        // 3. 将 n-1 个盘子从 aux 移到 dest
        dfs(n - 1, aux, src, dest);
    }
};

复杂度分析

  • 时间复杂度: O(2n),移动步数递归关系为 T(n)=2T(n−1)+1。
  • 空间复杂度: O(n),递归栈深度。

二、 搜索 (Depth-First Search, DFS)

1. 核心原理

深度优先搜索是一种用于遍历或搜索树或图的算法。它从根节点(或任意节点)开始,沿着一条路径尽可能深地探索,直到到达路径末端,然后回溯到上一个节点,继续探索其他未访问的分支。

2. 经典问题与代码实现

DFS 在二叉树问题中应用广泛,根据根节点的访问时机,分为前序、中序、后序遍历。

问题1:二叉树遍历与验证
  • 问题描述: 验证一个二叉树是否为有效的二叉搜索树(BST)。
  • 技术点: BST 的一个关键性质是其中序遍历结果是一个严格递增的序列。
  • 实现: 对树进行中序遍历,同时记录前一个访问的节点值 prev。在访问当前节点时,检查其值是否大于 prev

C++ 实现

复制代码
#include <climits>

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

class Solution {
private:
    // 使用 long long 防止节点值为 INT_MIN 时出错
    long long prev = LONG_MIN; 

public:
    bool isValidBST(TreeNode* root) {
        if (root == nullptr) {
            return true;
        }

        // 1. 遍历左子树
        if (!isValidBST(root->left)) {
            return false;
        }

        // 2. 访问当前节点
        if (root->val <= prev) {
            return false;
        }
        prev = root->val;

        // 3. 遍历右子树
        return isValidBST(root->right);
    }
};

约束与边界

  • 必须使用 long long 类型的 prev 变量,因为节点的 val 可能是 INT_MIN。如果 prevint,当 root->val 等于 INT_MIN 时,root->val <= prev 会判断错误。
问题2:路径求和
  • 问题描述: 求二叉树中从根节点到叶子节点的所有路径所表示的数字之和。
  • 技术点: 采用前序遍历(DFS),将父节点计算出的路径数值传递给子节点。
  • 实现: 递归函数 dfs(node, currentSum)currentSum 表示从根到 node 父节点的路径数值。

C++ 实现

复制代码
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

class Solution {
public:
    int sumNumbers(TreeNode* root) {
        return dfs(root, 0);
    }

private:
    int dfs(TreeNode* root, int currentSum) {
        if (root == nullptr) {
            return 0;
        }
        
        currentSum = currentSum * 10 + root->val;

        // 叶子节点,返回当前路径代表的数字
        if (root->left == nullptr && root->right == nullptr) {
            return currentSum;
        }

        // 非叶子节点,返回左右子树的和
        return dfs(root->left, currentSum) + dfs(root->right, currentSum);
    }
};

三、 回溯 (Backtracking)

1. 核心原理

回溯是一种通过试错 来解决问题的搜索算法,通常以 DFS 的形式实现。它在问题的解空间树(状态树)中进行搜索,当发现当前选择无法导向有效解时,就撤销选择 (回溯),退回到上一步,尝试其他选择。

回溯算法模板:

复制代码
void backtrack(路径, 选择列表) {
    if (满足结束条件) {
        记录路径;
        return;
    }

    for (选择 in 选择列表) {
        做出选择;
        backtrack(路径, 选择列表); // 递归
        撤销选择; // 回溯
    }
}

2. 经典问题与代码实现

问题1:全排列 (无重复元素)
  • 问题描述: 给定一个不含重复数字的数组,返回其所有可能的全排列。
  • 技术点: 典型回溯问题。使用一个 used (或 check) 数组来标记哪些元素已被使用。
  • 实现: path 记录当前排列,used 标记元素使用情况。

**C++ 实现 **

复制代码
#include <vector>
#include <algorithm>

class Solution {
public:
    std::vector<std::vector<int>> permute(std::vector<int>& nums) {
        std::vector<std::vector<int>> result;
        std::vector<int> path;
        std::vector<bool> used(nums.size(), false);
        dfs(nums, path, used, result);
        return result;
    }

private:
    void dfs(const std::vector<int>& nums, std::vector<int>& path, 
             std::vector<bool>& used, std::vector<std::vector<int>>& result) {
        // 结束条件:路径长度等于数组长度
        if (path.size() == nums.size()) {
            result.push_back(path);
            return;
        }

        for (int i = 0; i < nums.size(); ++i) {
            if (used[i]) {
                continue; // 跳过已使用的元素
            }

            // 做出选择
            path.push_back(nums[i]);
            used[i] = true;

            // 递归
            dfs(nums, path, used, result);

            // 撤销选择 (回溯)
            used[i] = false;
            path.pop_back();
        }
    }
};
问题2:全排列 (含重复元素)
  • 问题描述: 给定一个可能包含重复数字的数组,返回所有不重复的全排列。
  • 技术点: 为避免重复,需要引入剪枝逻辑。
  • 优化思路 (剪枝):
    1. 首先对原数组排序,使相同元素相邻。
    2. 在循环中,如果当前元素 nums[i] 与前一个元素 nums[i-1] 相同,并且 used[i-1]false,则说明 nums[i-1] 刚被回溯过。此时若选择 nums[i],会产生与之前重复的排列。因此,跳过这种情况。

**C++ 实现 **

复制代码
#include <vector>
#include <algorithm>

class Solution {
public:
    std::vector<std::vector<int>> permuteUnique(std::vector<int>& nums) {
        std::sort(nums.begin(), nums.end()); // 排序是剪枝的前提
        std::vector<std::vector<int>> result;
        std::vector<int> path;
        std::vector<bool> used(nums.size(), false);
        dfs(nums, path, used, result);
        return result;
    }

private:
    void dfs(const std::vector<int>& nums, std::vector<int>& path, 
             std::vector<bool>& used, std::vector<std::vector<int>>& result) {
        if (path.size() == nums.size()) {
            result.push_back(path);
            return;
        }

        for (int i = 0; i < nums.size(); ++i) {
            if (used[i]) {
                continue;
            }
            // 剪枝逻辑
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
                continue;
            }

            path.push_back(nums[i]);
            used[i] = true;
            dfs(nums, path, used, result);
            used[i] = false;
            path.pop_back();
        }
    }
};
问题3:N皇后问题
  • 问题描述: 在 N×N 的棋盘上放置 N 个皇后,使其不能互相攻击。

  • 技术点: 在二维平面上的回溯。需要判断列、主对角线和副对角线是否已被占用。

  • 优化思路:

    使用三个布尔数组

    复制代码
    cols

    ,

    复制代码
    diag1

    ,

    复制代码
    diag2

    来记录占用情况,实现 O(1)的冲突检查。

    • 列:cols[j]
    • 主对角线 (左上到右下):同一条对角线上的点 (r, c)r-c 的值恒定。用 diag1[r - c + n - 1] 记录 (加 n-1 是为了将负数索引转为正数)。
    • 副对角线 (右上到左下):同一条对角线上的点 (r, c)r+c 的值恒定。用 diag2[r + c] 记录。

**C++ 实现 **

复制代码
#include <vector>
#include <string>

class Solution {
public:
    std::vector<std::vector<std::string>> solveNQueens(int n) {
        this->n = n;
        path.assign(n, std::string(n, '.'));
        cols.assign(n, false);
        diag1.assign(2 * n - 1, false);
        diag2.assign(2 * n - 1, false);
        dfs(0);
        return result;
    }

private:
    int n;
    std::vector<std::vector<std::string>> result;
    std::vector<std::string> path;
    std::vector<bool> cols, diag1, diag2;

    void dfs(int row) {
        if (row == n) {
            result.push_back(path);
            return;
        }

        for (int col = 0; col < n; ++col) {
            // 冲突检测
            if (!cols[col] && !diag1[row - col + n - 1] && !diag2[row + col]) {
                // 做出选择
                path[row][col] = 'Q';
                cols[col] = diag1[row - col + n - 1] = diag2[row + col] = true;

                // 递归
                dfs(row + 1);

                // 撤销选择
                cols[col] = diag1[row - col + n - 1] = diag2[row + col] = false;
                path[row][col] = '.';
            }
        }
    }
};

四、 FloodFill (泛洪填充)

1. 核心原理

FloodFill 是一种在多维数组(通常是二维矩阵,如图像)上,将与起始点相连通的、具有相同值的区域,替换为新值的算法。其本质是图的遍历(DFS或BFS)。

定义:

复制代码
floodFill(image, sr, sc, newColor)

= 从

复制代码
(sr, sc)

点开始,将所有与该点颜色相同且联通的像素点颜色替换为

复制代码
newColor
  • 实现 (DFS):

    1. 获取起始像素 (sr, sc) 的原始颜色 originalColor

    2. 如果 originalColornewColor 相同,则无需操作,直接返回。

    3. (sr, sc) 的颜色修改为 newColor

    4. 向其上下左右四个方向的相邻像素递归调用

      复制代码
      dfs

      递归的条件是:

      • 相邻像素坐标在矩阵边界内。
      • 相邻像素的颜色等于 originalColor
  • 约束: 算法的正确性依赖于先修改当前节点,再递归邻接节点的顺序,以避免因颜色未变而导致的无限递归。

2. 经典问题与代码实现

问题1:图像渲染 (LeetCode 733)

问题描述:

给定一个图像矩阵、一个起始像素坐标

复制代码
(sr, sc)

和一个新颜色

复制代码
newColor

,对起始像素所在区域进行颜色填充 3

  • 技术点: 直接应用 FloodFill 的 DFS 实现。

C++ 实现 (源自文件)

C++

复制代码
#include <vector>

class Solution {
private:
    int m, n;
    int originalColor;
    int dx[4] = {0, 0, 1, -1};
    int dy[4] = {1, -1, 0, 0};

public:
    std::vector<std::vector<int>> floodFill(std::vector<std::vector<int>>& image, int sr, int sc, int newColor) {
        originalColor = image[sr][sc];
        // 如果起始颜色和新颜色相同,无需操作
        if (originalColor == newColor) {
            return image;
        }
        m = image.size();
        n = image[0].size();
        dfs(image, sr, sc, newColor);
        return image;
    }

private:
    void dfs(std::vector<std::vector<int>>& image, int r, int c, int newColor) {
        // 将当前像素颜色更新
        image[r][c] = newColor;

        // 向四个方向递归
        for (int i = 0; i < 4; ++i) {
            int new_r = r + dx[i];
            int new_c = c + dy[i];

            // 检查边界条件和颜色条件
            if (new_r >= 0 && new_r < m && new_c >= 0 && new_c < n && image[new_r][new_c] == originalColor) {
                dfs(image, new_r, new_c, newColor);
            }
        }
    }
};

复杂度分析

  • 时间复杂度: O(M×N),其中 M 和 N 是矩阵的行数和列数。每个像素点最多被访问一次。
  • 空间复杂度: O(M×N),在最坏情况下(整个图像都是同一颜色),递归栈的深度可能达到 M×N。
问题2:岛屿数量 (LeetCode 200)

问题描述:

计算一个由 '1' (陆地) 和 '0' (水) 组成的二维网格中岛屿的数量 。

技术点:

遍历整个网格,每当遇到一个 '1' (陆地),就将岛屿计数加一,并以此陆地为起点执行 FloodFill (DFS),将整个岛屿(所有相连的 '1')"淹没"(例如,修改为 '0' 或其他标记),以防重复计数 。

  • 实现: 主循环遍历网格,DFS函数负责淹没岛屿。

**C++ 实现 **

复制代码
#include <vector>

class Solution {
public:
    int numIslands(std::vector<std::vector<char>>& grid) {
        if (grid.empty() || grid[0].empty()) {
            return 0;
        }
        int m = grid.size();
        int n = grid[0].size();
        int count = 0;

        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == '1') {
                    count++;
                    dfs(grid, i, j);
                }
            }
        }
        return count;
    }

private:
    void dfs(std::vector<std::vector<char>>& grid, int r, int c) {
        int m = grid.size();
        int n = grid[0].size();
        
        // 边界检查或当前非陆地
        if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] != '1') {
            return;
        }
        
        // 淹没当前陆地
        grid[r][c] = '0'; 

        // 递归淹没邻接陆地
        dfs(grid, r + 1, c);
        dfs(grid, r - 1, c);
        dfs(grid, r, c + 1);
        dfs(grid, r, c - 1);
    }
};

五、 记忆化搜索 (Memoization)

1. 核心原理

记忆化搜索是自顶向下 (Top-Down) 动态规划的一种实现形式。它本质上是带有备忘录 (memo) 的递归。通过缓存子问题的计算结果,来避免在递归过程中重复计算相同的子问题,从而极大地优化时间性能。

  • 核心思想:

    1. 备忘录 (Memo): 通常使用数组或哈希表,存储[子问题输入] -> [子问题解]的映射。初始化为一个特殊值(如-1),表示该子问题尚未求解。

    查询:

    复制代码
    在递归函数的入口,首先检查备忘录中是否已存在当前子问题的解。如果存在,直接返回缓存的结果 。

    计算与存储:如果备忘录中不存在,则正常进行递归计算。在计算出结果后,返回之前,将其存入备忘录 。

2. 经典问题与代码实现

问题1:不同路径 (LeetCode 62)

问题描述:

一个机器人在 m x n 网格的左上角,只能向右或向下移动,问到达右下角共有多少条不同路径 8

  • 技术点: 一个典型的DP问题。dp[i][j] 表示到达 (i, j) 的路径数。状态转移方程为 dp[i][j] = dp[i-1][j] + dp[i][j-1]。用递归实现时,存在大量重叠子问题,适合记忆化搜索。

C++ 实现 (记忆化搜索)

C++

复制代码
#include <vector>

class Solution {
public:
    int uniquePaths(int m, int n) {
        // memo[i][j] 存储到达 (i-1, j-1) 的路径数,初始化为-1
        std::vector<std::vector<int>> memo(m, std::vector<int>(n, -1));
        return dfs(m - 1, n - 1, memo);
    }

private:
    int dfs(int r, int c, std::vector<std::vector<int>>& memo) {
        // Base Case: 起点只有一条路径
        if (r == 0 && c == 0) {
            return 1;
        }
        // 越界,无路径
        if (r < 0 || c < 0) {
            return 0;
        }

        // 查询备忘录
        if (memo[r][c] != -1) {
            return memo[r][c];
        }

        // 计算并存储
        memo[r][c] = dfs(r - 1, c, memo) + dfs(r, c - 1, memo);
        return memo[r][c];
    }
};
问题2:矩阵中的最长递增路径 (LeetCode 329)

问题描述:

在一个整数矩阵中,找到从任意单元格出发的最长递增路径的长度。只能上下左右移动 。

  • 技术点:

    1. 遍历矩阵中的每个单元格,将其作为起点,调用DFS计算从该点出发的最长递增路径。
    2. 最终结果是所有起点计算出的最大值。
    3. 如果不使用记忆化,dfs(i, j) 会被重复计算多次。因此,memo[i][j] 用于存储从 (i, j) 出发的最长递增路径长度。

C++ 实现 (记忆化搜索)

C++

复制代码
#include <vector>
#include <algorithm>

class Solution {
private:
    int m, n;
    int dx[4] = {0, 0, 1, -1};
    int dy[4] = {1, -1, 0, 0};
    
public:
    int longestIncreasingPath(std::vector<std::vector<int>>& matrix) {
        if (matrix.empty() || matrix[0].empty()) return 0;
        m = matrix.size();
        n = matrix[0].size();
        
        std::vector<std::vector<int>> memo(m, std::vector<int>(n, 0)); // 0表示未计算
        int max_len = 0;
        
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                max_len = std::max(max_len, dfs(matrix, i, j, memo));
            }
        }
        return max_len;
    }

private:
    int dfs(const std::vector<std::vector<int>>& matrix, int r, int c, std::vector<std::vector<int>>& memo) {
        // 如果已计算,直接返回
        if (memo[r][c] != 0) {
            return memo[r][c];
        }

        // 路径长度至少为1 (自身)
        int current_max = 1;
        
        for (int i = 0; i < 4; ++i) {
            int new_r = r + dx[i];
            int new_c = c + dy[i];

            if (new_r >= 0 && new_r < m && new_c >= 0 && new_c < n && matrix[new_r][new_c] > matrix[r][c]) {
                current_max = std::max(current_max, 1 + dfs(matrix, new_r, new_c, memo));
            }
        }

        // 存入备忘录并返回
        memo[r][c] = current_max;
        return current_max;
    }
};

边界与约束

  • longestIncreasingPath中,备忘录memo的初始值设为0,因为路径长度至少为1。这与通常使用-1作为未计算标记不同,但在此场景下是有效的区分。
  • 记忆化搜索将暴力递归的指数级时间复杂度,优化为 子问题数量 × 计算单个子问题的时间。对于矩阵问题,通常是 O(M×N)。
相关推荐
网安INF2 分钟前
SHA-1算法详解:原理、特点与应用
java·算法·密码学
无聊的小坏坏17 分钟前
从数学到代码:一文详解埃拉托色尼筛法(埃式筛)
算法
Gyoku Mint44 分钟前
机器学习×第七卷:正则化与过拟合——她开始学会收敛,不再贴得太满
人工智能·python·算法·chatgpt·线性回归·ai编程
黑听人1 小时前
【力扣 中等 C++】90. 子集 II
开发语言·数据结构·c++·算法·leetcode
黑听人1 小时前
【力扣 简单 C】21. 合并两个有序链表
c语言·开发语言·数据结构·算法·leetcode
励志成为大佬的小杨2 小时前
时间序列基础
人工智能·算法
黑听人2 小时前
【力扣 简单 C】83. 删除排序链表中的重复元素
c语言·开发语言·数据结构·算法·leetcode
微凉的衣柜2 小时前
机器人导航中的高程图 vs 高度筛选障碍物点云投影 —— 如何高效处理避障问题?
算法·机器人
Shaun_青璇2 小时前
Cpp 知识3
开发语言·c++·算法
小鸡脚来咯3 小时前
ThreadLocal实现原理
java·开发语言·算法