深度优先搜索(DFS)

深度优先搜索会沿着一条可行分支尽量向深处访问,走不通时再返回到最近的分叉点。递归调用的进入与返回,正好对应搜索树中的深入与回退;DFS 既可遍历显式图,也可搜索由状态和转移构成的隐式图。

1. 图上的基本框架

图可能含环,因此要用访问数组避免重复访问。以下模板从 s 出发,访问它所在的全部连通部分;若要统计全图连通块,再枚举所有未访问的起点调用它。

vector<vector<int>> g;
vector<bool> vis;

void dfs(int u) {
    vis[u] = true;
    // 处理结点 u
    for (int v : g[u]) {
        if (!vis[v]) dfs(v);
    }
}

int components = 0;
for (int i = 1; i <= n; ++i) {
    if (!vis[i]) {
        ++components;
        dfs(i);
    }
}

对于以无向边给出的树,也可以传入 fa 来跳过父结点;这样常用于计算深度、子树大小和树形 DP。

2. 递归与显式栈

递归实现最直观,但深度很大时可能造成调用栈过深。显式栈能避免这一问题。若希望迭代版本的访问顺序接近递归版本,通常将邻接点按相反顺序压栈。

void dfsIter(int s) {
    stack<int> st;
    st.push(s);
    vis[s] = true;
    while (!st.empty()) {
        int u = st.top(); st.pop();
        // 处理结点 u
        for (int v : g[u]) {
            if (!vis[v]) {
                vis[v] = true;
                st.push(v);
            }
        }
    }
}

3. 常见应用

连通块与 Flood Fill:把网格中的每个格子看作结点,只向合法的上下左右邻格递归,即可统计岛屿、填充颜色或求区域大小。路径搜索:递归参数可携带当前位置、剩余步数或当前答案;到达目标后记录结果。子树信息:先递归孩子、后合并答案,可得到子树大小、最大深度等后序信息。

int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, 1, -1};

void floodFill(int x, int y) {
    vis[x][y] = true;
    for (int k = 0; k < 4; ++k) {
        int nx = x + dx[k], ny = y + dy[k];
        if (inside(nx, ny) && !vis[nx][ny] && grid[nx][ny] == '#')
            floodFill(nx, ny);
    }
}

4. 复杂度与使用边界

邻接表中,每个点和每条边至多被检查常数次,时间复杂度为 O(V + E),访问数组和递归栈额外占 O(V) 空间。邻接矩阵需要扫描整行,时间为 O(V²)

图遍历的 vis 通常一旦标记便不撤销;而枚举方案时的“路径内已用”状态往往需要撤销,那是回溯搜索的做法。遇到无权最短路时,优先使用 BFS,而不是 DFS。