【C++题解】DFS和BFS

发布于:2025-09-08 ⋅ 阅读:(21) ⋅ 点赞:(0)

4小时编码练习计划,专注于深度优先搜索(DFS)和广度优先搜索(BFS)这两种基本且强大的算法。

下午 (4小时): 搜索算法专题——DFS与BFS

DFS和BFS是图论和多种问题求解中的基石算法。深刻理解它们的原理、差异和代码实现模板,是提升算法能力的必经之路。

练习计划概览

  • 总时长: 约 4 小时

  • 核心目标:

    1. 掌握深度优先搜索(DFS)的递归回溯思想,能够写出解决“排列组合”和“可行性搜索”问题的代码模板。

    2. 掌握广度优先搜索(BFS)基于队列的层次遍历思想,熟练使用其解决“无权图最短路径”问题。

    3. 清晰地辨别DFS(找所有解/任意解)和BFS(找最优解/最短步数)的典型应用场景。

    4. 学会使用visited数组或哈希表来标记状态,避免重复搜索和死循环。


第一部分:深度优先搜索 (DFS)——“不撞南墙不回头” (约 1.5 小时)

DFS的核心思想是“一条路走到黑”,它沿着一个分支深入探索,直到无法再前进时才回溯到上一步,换个方向继续探索。它天然地适合用递归实现,常用于求解所有可能的方案。

题目编号 题目名称 核心知识点 练习目标
LeetCode 46 /
Luogu P1706
全排列 (Permutations) DFS, 回溯, 状态管理 DFS入门必做题。练习最基础的DFS回溯模板。学习如何通过 visited 数组记录哪些数字已被使用,在递归的每一层选择一个未使用过的数字,并在回溯时恢复现场(pop_backvisited[i] = false)。
LeetCode 51 /
Luogu P1219
N皇后 (N-Queens) DFS, 回溯, 剪枝 在“全排列”的基础上,增加了复杂的约束条件(行、列、对角线不能冲突)。这道题能极好地锻炼你如何在DFS的递归过程中进行“剪枝”——即提前判断当前路径是否合法,从而避免无效的深入搜索,提升效率。
题解
// 46 - 经典的使用DFS回溯
/*代码框架
	bool visited[MAXN]; // 访问标记数组
	
	void dfs(int u) {
	    visited[u] = true; // 标记已访问
	    // process(u); // 处理当前节点 u 的逻辑
	
	    for (int v : adj[u]) { // 遍历 u 的所有邻居 v
	        if (!visited[v]) { // 如果邻居 v 未被访问
	            dfs(v); // 继续深入
	        }
	    }
	}
*/
#include <iostream>
#include <vector>

using namespace std;

vector<int> path;
vector<vector<int>> ans;
void bt(vector<int>& nums,vector<bool> &used){
    if(path.size()==nums.size()){
        ans.push_back(path);
        return;
    }
    for(int i=0;i<nums.size();i++){
        if(!used[i]){
            used[i] = true;
            path.push_back(nums[i]);
            bt(nums,used);
            path.pop_back();
            used[i] = false;
        }
    }
}
vector<vector<int>> permute(vector<int>& nums) {
    vector<bool> used(nums.size(),false);
    bt(nums,used);
    return ans;
}

int main(){
    vector<int> nums ={1,2,3};      //  样例输入。
    vector<vector<int>> rst = permute(nums);
    for(vector<int> v : rst){
        for(int i : v){
            cout << i << " ";
        }
        cout << endl;
    }
    return 0;
}

//51 - 特别经典的问题,同样回溯去做

#include <iostream>
#include <vector>
using namespace std;

class Solution {
private:
    vector<vector<string>> result;
    vector<string> board;

    // 检查在(row, col)位置放置皇后是否安全
    bool isSafe(int row, int col, int n) {
        // 检查同一列是否有皇后
        for (int i = 0; i < row; i++) {
            if (board[i][col] == 'Q') {
                return false;
            }
        }

        // 检查左上对角线
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (board[i][j] == 'Q') {
                return false;
            }
        }

        // 检查右上对角线
        for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            if (board[i][j] == 'Q') {
                return false;
            }
        }

        return true;
    }

    // 回溯函数
    void backtrack(int row, int n) {
        // 如果所有行都放置了皇后,找到一个解
        if (row == n) {
            result.push_back(board);
            return;
        }

        // 尝试在当前行的每一列放置皇后
        for (int col = 0; col < n; col++) {
            if (isSafe(row, col, n)) {
                // 放置皇后
                board[row][col] = 'Q';

                // 递归处理下一行
                backtrack(row + 1, n);

                // 回溯,移除皇后
                board[row][col] = '.';
            }
        }
    }

public:
    vector<vector<string>> solveNQueens(int n) {
        // 初始化棋盘
        board = vector<string>(n, string(n, '.'));
        result.clear();

        // 从第0行开始回溯
        backtrack(0, n);

        return result;
    }
};
int main(){
    int n;
    cin >> n;

    Solution solution;
    vector<vector<string>> rst = solution.solveNQueens(n);

    cout << "共找到 " << rst.size() << " 种解法:" << endl << endl;

    for (int i = 0; i < rst.size(); i++) {
        cout << "解法 " << (i + 1) << ":" << endl;
        for (const string& row : rst[i]) {
            cout << row << endl;
        }
        cout << endl;
    }

    return 0;
}

练习建议:

  • 画递归搜索树:在做这两道题时,强烈建议您在纸上画出当N较小(如3或4)时的递归搜索树。这能非常直观地帮助您理解“深入”、“回溯”、“剪枝”这些概念是如何在代码中体现的。

  • 模板化:尝试将“全排列”的代码抽象成一个通用的回溯模板:

    void dfs(参数) {
        if (满足终止条件) {
            // 存放结果
            return;
        }
        for (选择 : 本层可选的集合) {
            // 处理节点
            dfs(路径, 选择列表);
            // 回溯,撤销处理
        }
    }
    

第二部分:广度优先搜索 (BFS)——“一层一层地毯式搜索” (约 2 小时)

BFS借助队列实现,从起点开始,先访问所有距离为1的邻居,再访问所有距离为2的邻居,以此类推,像水波纹一样向外扩散。这个特性决定了它能够找到从起点到任何其他点的最短路径(在边权为1的图中)。

题目编号 题目名称 核心知识点 练习目标
LeetCode 994 腐烂的橘子
(Rotting Oranges)
BFS, 队列, 多源BFS 经典的网格BFS问题。此题是“多源BFS”的绝佳范例,即初始时有多个起点(腐烂的橘子)同时开始搜索。通过BFS的层次遍历,可以很自然地模拟出时间一分钟一分钟流逝、橘子一层一层腐烂的过程。
LeetCode 127 单词接龙
(Word Ladder)
BFS, 隐式图, 字符串处理 BFS求解最短路径的经典应用。这道题的难点在于图是“隐式”的——节点是单词,两个单词之间是否存在边需要我们自行判断(是否只差一个字母)。这要求我们在BFS过程中动态地寻找邻居节点,是BFS能力的进阶考验。
题解
//994 - BFS,使用队列作为核心观察目前广度搜索节点,然后一层一层去处理,每次都会记录这次队列的大小。

#include <iostream>
#include <vector>
#include <deque>
#include <queue>
using namespace std;

class Solution {
public:
    int orangesRotting(vector<vector<int>>& grid) {
        int m = grid.size();
        int n = grid[0].size();
        queue<pair<int, int>> q;
        int fresh = 0;

        // 找到所有腐烂的橘子,并统计新鲜橘子数量
        for(int i = 0; i < m; i++) {
            for(int j = 0; j < n; j++) {
                if(grid[i][j] == 2) {
                    q.push({i, j});
                } else if(grid[i][j] == 1) {
                    fresh++;
                }
            }
        }

        // 如果没有新鲜橘子,直接返回0
        if(fresh == 0) return 0;

        int minutes = 0;
        int directions[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

        while(!q.empty()) {
            int size = q.size();
            bool hasRotten = false;

            // 处理当前层的所有腐烂橘子
            for(int i = 0; i < size; i++) {
                pair<int, int> curr = q.front();
                q.pop();
                int x = curr.first;
                int y = curr.second;

                // 检查四个方向
                for(int d = 0; d < 4; d++) {
                    int nx = x + directions[d][0];
                    int ny = y + directions[d][1];

                    // 边界检查和新鲜橘子检查
                    if(nx >= 0 && nx < m && ny >= 0 && ny < n && grid[nx][ny] == 1) {
                        grid[nx][ny] = 2;  // 变腐烂
                        q.push({nx, ny});
                        fresh--;
                        hasRotten = true;
                    }
                }
            }

            // 如果这一轮有橘子腐烂,时间+1
            if(hasRotten) {
                minutes++;
            }
        }

        // 如果还有新鲜橘子,返回-1;否则返回时间
        return fresh == 0 ? minutes : -1;
    }
};

int main(){
    Solution solution;

    // 测试用例1: [[2,1,1],[1,1,0],[0,1,1]]
    // 预期输出: 4
    vector<vector<int>> grid1 = {{2,1,1},{1,1,0},{0,1,1}};
    cout << "测试用例1: " << solution.orangesRotting(grid1) << endl;

    // 测试用例2: [[2,1,1],[0,1,1],[1,0,1]]
    // 预期输出: -1
    vector<vector<int>> grid2 = {{2,1,1},{0,1,1},{1,0,1}};
    cout << "测试用例2: " << solution.orangesRotting(grid2) << endl;

    // 测试用例3: [[0,2]]
    // 预期输出: 0
    vector<vector<int>> grid3 = {{0,2}};
    cout << "测试用例3: " << solution.orangesRotting(grid3) << endl;

    // 测试用例4: [[1]]
    // 预期输出: -1
    vector<vector<int>> grid4 = {{1}};
    cout << "测试用例4: " << solution.orangesRotting(grid4) << endl;

    return 0;
}
//127 - 转化成图求最短路径即可,我这里一开始想的dijstra,交上去虽然过了,但是耗时比较多,因为这计算了每个节点的距离,所有还有一种BFS也在里面,时间要节省一些。

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <climits>
#include <queue>
using namespace std;

class Solution {
private:
    int n; // 目标节点索引
public:
    bool similar(string a, string b) {
        int diff = 0;
        for (int i = 0; i < a.size(); i++) {
            if (a[i] != b[i]) {
                diff++;
            }
        }
        return diff == 1;
    }
    int dijstra(vector<vector<bool>>& grid, int target) {
        int size = grid.size();
        vector<int> dij(size, size + 1);
        vector<bool> check(size, false);
        dij[0] = 0;

        for (int count = 0; count < size; count++) {
            int minDist = INT_MAX;
            int u = -1;

            // 找到未访问节点中距离最小的节点
            for (int i = 0; i < size; i++) {
                if (!check[i] && dij[i] < minDist) {
                    minDist = dij[i];
                    u = i;
                }
            }

            if (u == -1) break; // 没有可达节点
            check[u] = true;

            // 更新相邻节点的距离
            for (int v = 0; v < size; v++) {
                if (!check[v] && grid[u][v] && dij[u] != INT_MAX) {
                    if (dij[u] + 1 < dij[v]) {
                        dij[v] = dij[u] + 1;
                    }
                }
            }
        }

        return dij[target] == size + 1 ? -1 : dij[target];
    }

    int bfs(vector<vector<bool>>& grid, int target) {
        int size = grid.size();
        vector<int> dist(size, -1);
        queue<int> q;

        q.push(0);
        dist[0] = 0;

        while (!q.empty()) {
            int curr = q.front();
            q.pop();

            if (curr == target) {
                return dist[target];
            }

            // 遍历所有相邻节点
            for (int next = 0; next < size; next++) {
                if (grid[curr][next] && dist[next] == -1) {
                    dist[next] = dist[curr] + 1;
                    q.push(next);
                }
            }
        }

        return dist[target];
    }
    int ladderLength(string beginWord, string endWord,
                     vector<string>& wordList, bool useBFS = true) {
        vector<vector<bool>> grid(wordList.size() + 2,
                                  vector<bool>(wordList.size() + 2, false));
        if (find(wordList.begin(), wordList.end(), endWord) == wordList.end()) {
            return 0;
        }
        for (int i = 0; i < wordList.size(); i++) {
            if (similar(beginWord, wordList[i])) {
                grid[0][i + 1] = true;
            }
        }
        for (int i = 0; i < wordList.size(); i++) {
            if (endWord == wordList[i]) {
                n = i + 1;
            }
            for (int j = i; j < wordList.size(); j++) {
                if (similar(wordList[j], wordList[i])) {
                    grid[i + 1][j + 1] = true;
                    grid[j + 1][i + 1] = true;
                }
            }
        }

        int result;
        if (useBFS) {
            cout << "使用BFS算法..." << endl;
            result = bfs(grid, n);
        } else {
            cout << "使用Dijkstra算法..." << endl;
            result = dijstra(grid, n);
        }

        return result == -1 ? 0 : result + 1;
    }
};

int main() {
    Solution solution;

    cout << "请选择算法:" << endl;
    cout << "1. BFS (广度优先搜索)" << endl;
    cout << "2. Dijkstra (迪杰斯特拉算法)" << endl;
    cout << "请输入选择 (1 或 2): ";

    int choice;
    cin >> choice;
    bool useBFS = (choice == 1);

    cout << "\n开始测试..." << endl;

    // 测试用例1
    string beginWord1 = "hit";
    string endWord1 = "cog";
    vector<string> wordList1 = {"hot","dot","dog","lot","log","cog"};
    cout << "\n测试用例1: beginWord=\"hit\", endWord=\"cog\"" << endl;
    cout << "wordList=[\"hot\",\"dot\",\"dog\",\"lot\",\"log\",\"cog\"]" << endl;
    int result1 = solution.ladderLength(beginWord1, endWord1, wordList1, useBFS);
    cout << "结果: " << result1 << endl;

    // 测试用例2
    string beginWord2 = "hit";
    string endWord2 = "cog";
    vector<string> wordList2 = {"hot","dot","dog","lot","log"};
    cout << "\n测试用例2: beginWord=\"hit\", endWord=\"cog\"" << endl;
    cout << "wordList=[\"hot\",\"dot\",\"dog\",\"lot\",\"log\"]" << endl;
    int result2 = solution.ladderLength(beginWord2, endWord2, wordList2, useBFS);
    cout << "结果: " << result2 << endl;

    return 0;
}

练习建议:

  • 队列是核心:牢记BFS的核心数据结构是队列。queue.push() 对应将新节点加入下一层待访问列表,queue.pop() 对应取出当前层的节点进行处理。

  • 距离/步数 tracking: 通常需要一个dist数组或 map 来记录从起点到每个节点的最短距离。在BFS中,dist[neighbor] = dist[current] + 1 是更新距离的关键。对于“腐烂的橘子”,BFS的层数本身就代表了分钟数。

  • 避免重复访问:和DFS一样,visited 数组或 set 对于BFS至关重要,用于确保每个节点只入队一次,防止在有环的图中无限循环。


目标达成自查 (约 0.5 小时)

完成以上练习后,进行复盘和总结:

  1. DFS vs. BFS 的核心区别是什么?

    • 数据结构:DFS主要依赖递归(系统栈),BFS依赖队列。

    • 搜索顺序:DFS是“深度”优先,一路走到底;BFS是“广度”优先,一层层扩展。

  2. 如何根据问题选择算法?

    • 问题要求最短路径/最少步数/最少操作次数吗? -> 首选 BFS

    • 问题要求找出所有方案/组合/排列,或者只是判断“能不能”到达某个状态(不要求最短)? -> DFS 更自然

  3. 两种搜索算法的通用模板是什么?

    • 我能否独立、无误地写出DFS回溯和BFS层次遍历的基本代码框架?

    • 我是否清楚在模板的哪个位置进行“访问”标记(visited)可以避免重复计算?(对于BFS,在节点入队时标记;对于DFS,在刚进入递归函数时标记)。


网站公告

今日签到

点亮在社区的每一天
去签到