DFS

DFS

深度优先搜索(DFS,Depth-First Search)是一种遍历或搜索树/图结构的算法,其核心思想是“一条路走到底”,通过递归或栈实现。DFS 在解决 组合问题、排列问题、路径问题、连通性问题 时非常高效。以下是 DFS 的通用模板、回溯、剪枝、记忆化搜索的详细解析。

一、DFS 通用模板

递归实现模板

public void dfs(参数列表) {
    // 终止条件
    if (终止条件满足) {
        记录结果或返回;
        return;
    }
    
    // 遍历所有可能的子节点或选择
    for (所有可能的子节点或选择) {
        if (子节点或选择合法) {
            标记当前选择(如设置访问状态);
            dfs(新的参数); // 递归深入下一层
            撤销当前选择(回溯); // 恢复状态
        }
    }
}

栈实现模板(非递归)

public void dfsWithStack(起始节点) {
    Stack<节点类型> stack = new Stack<>();
    stack.push(起始节点);
    Set<节点类型> visited = new HashSet<>(); // 记录已访问节点
    
    while (!stack.isEmpty()) {
        节点类型 node = stack.pop();
        if (node 是目标节点) {
            处理结果;
            continue;
        }
        // 遍历相邻节点
        for (节点类型 neighbor : node的相邻节点列表) {
            if (!visited.contains(neighbor)) {
                visited.add(neighbor);
                stack.push(neighbor);
            }
        }
    }
}

二、回溯(Backtracking)

回溯是 DFS 的一种应用形式,核心是 尝试所有可能的选择,并在发现当前路径无法得到解时回退(撤销选择)

经典问题:全排列

public class Permutations {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        backtrack(nums, new ArrayList<>(), res);
        return res;
    }
    
    private void backtrack(int[] nums, List<Integer> path, List<List<Integer>> res) {
        if (path.size() == nums.length) {
            res.add(new ArrayList<>(path)); // 找到一种排列
            return;
        }
        
        for (int num : nums) {
            if (path.contains(num)) continue; // 跳过已选元素
            path.add(num);          // 选择当前元素
            backtrack(nums, path, res);
            path.remove(path.size() - 1); // 撤销选择(回溯)
        }
    }
}

关键点:通过 path.add()path.remove() 实现选择与撤销。


三、剪枝(Pruning)

剪枝是通过 提前终止无效递归分支 来优化 DFS 效率的技术。

示例:组合总和(剪枝优化)

public class CombinationSum {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates); // 排序便于剪枝
        List<List<Integer>> res = new ArrayList<>();
        backtrack(candidates, target, 0, new ArrayList<>(), res);
        return res;
    }
    
    private void backtrack(int[] nums, int remain, int start, List<Integer> path, List<List<Integer>> res) {
        if (remain < 0) return; // 剪枝:剩余值小于0,无需继续
        if (remain == 0) {
            res.add(new ArrayList<>(path));
            return;
        }
        
        for (int i = start; i < nums.length; i++) {
            if (nums[i] > remain) break; // 剪枝:排序后后续元素都大于remain
            path.add(nums[i]);
            backtrack(nums, remain - nums[i], i, path, res); // 允许重复选择
            path.remove(path.size() - 1);
        }
    }
}

剪枝点

  1. 排序后提前终止循环(nums[i] > remain)。
  2. 递归前检查 remain < 0

四、记忆化搜索(Memoization)

记忆化搜索通过 缓存中间计算结果 避免重复递归,常用于优化重复子问题。

示例:斐波那契数列

public class Fibonacci {
    private Map<Integer, Integer> memo = new HashMap<>();
    
    public int fib(int n) {
        if (n <= 1) return n;
        if (memo.containsKey(n)) {
            return memo.get(n); // 直接返回缓存结果
        }
        int res = fib(n - 1) + fib(n - 2);
        memo.put(n, res); // 缓存结果
        return res;
    }
}

示例:矩阵中的最长递增路径(DFS + 记忆化)

public class LongestIncreasingPath {
    private int[][] dirs = {{0,1}, {0,-1}, {1,0}, {-1,0}};
    private int[][] memo; // 记忆化缓存
    
    public int longestIncreasingPath(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        memo = new int[m][n];
        int maxLen = 0;
        
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                maxLen = Math.max(maxLen, dfs(matrix, i, j));
            }
        }
        return maxLen;
    }
    
    private int dfs(int[][] matrix, int i, int j) {
        if (memo[i][j] != 0) return memo[i][j]; // 命中缓存
        
        int max = 1;
        for (int[] dir : dirs) {
            int x = i + dir[0], y = j + dir[1];
            if (x < 0 || y < 0 || x >= matrix.length || y >= matrix[0].length) continue;
            if (matrix[x][y] <= matrix[i][j]) continue;
            max = Math.max(max, 1 + dfs(matrix, x, y));
        }
        memo[i][j] = max; // 缓存结果
        return max;
    }
}

五、DFS 的典型应用场景

问题类型 示例 关键技术
组合/排列问题 全排列、子集、组合总和 回溯、剪枝
路径问题 迷宫问题、矩阵中的最长递增路径 记忆化搜索
连通性问题 岛屿数量、图的连通分量 标记访问状态
决策问题 八皇后、数独 回溯、剪枝

六、总结

技术 核心思想 适用场景
通用模板 递归或栈实现,遍历所有可能路径 所有 DFS 问题
回溯 撤销选择,尝试其他可能性 组合、排列、决策问题
剪枝 提前终止无效递归分支 优化时间复杂度
记忆化搜索 缓存中间结果,避免重复计算 存在重复子问题(如动态规划)

实战建议

  1. 明确递归终止条件:避免无限递归。
  2. 合理设计状态标记与恢复:确保回溯正确性。
  3. 优先考虑剪枝:显著提升算法效率。
  4. 记忆化搜索与动态规划结合:解决复杂重复子问题。
posted @ 2025-02-24 17:11  咋还没来  阅读(69)  评论(0)    收藏  举报