【算法】(4)DFS、BFS、Backtracking

文章目录

  • 参考
  • BFS
    • 解题框架
    • 例1:二叉树的最小深度
    • 例2:打开转盘锁(图遍历)
  • DFS
    • 解题框架
    • 例1:二叉树的最小深度
  • 回溯
    • 解题框架
    • 例1 :全排列
    • 例2:N皇后
    • 例3:目标和

参考

labuladong/fucking-algorithm
CyC2018/CS-Notes

BFS

  • 广度优先搜索,主要用于求最优解问题
  • 优先遍历同层的节点,如二叉树的层序遍历
  • 核心数据结构是队列
  • 注意事项
    – 使用队列存储每一轮遍历得到的节点;
    – 可以考虑使用额外空间对遍历过的节点进行标记

解题框架

int BFS(Node start, Node target) {
     
    Queue<Node> q; // 核心数据结构 Set<Node> visited; // 避免走回头路
    q.offer(start); // 将起点加入队列 visited.add(start);
    int step = 0; // 记录扩散的步数
    while (q not empty){
     
        int sz = q.size();
        /* 将当前队列中的所有节点向四周扩散 */
        for (int i = 0; i < sz; i++) {
     
            Node cur = q.poll();
            /* 划重点:这里判断是否到达终点 */
        }
        if (cur is target)
        return step;
        /* 将 cur 的相邻节点加入队列 */
        for (Node x : cur.adj())
            if (x not in visited){
     
            q.offer(x);
            visited.add(x);
        }
        /* 划重点:更新步数在这里 */
        step++;
    }
}

例1:二叉树的最小深度

111. 二叉树的最小深度

//BFS,层序遍历
//空间O(n),时间O(n)
//BFS可以不用遍历完所有节点,DFS一定是要遍历完所有节点的
//BFS的空间消耗平均比DFS要高一点,其实还是空间换了时间
public int minDepth(TreeNode root) {
     
    if(root == null) return 0 ;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    int depth = 1;
    while(queue.size()!=0){
     
        int size = queue.size();
        for(int i = 0 ; i < size ;i++){
     
            TreeNode node = queue.poll();
            if(node.left == null && node.right==null){
     
                //已经是子节点了
                return depth;
            }
            if(node.left!=null){
     
                queue.offer(node.left);
            }
            if(node.right!=null){
     
                queue.offer(node.right);
            }
        }
        //遍历完一层后
        depth++;
    }
    return depth;
}

例2:打开转盘锁(图遍历)

752. 打开转盘锁

//BFS,每个数字组合是一个节点,波动一次,会产生8种不同的数字组合(如:0000拨动一次的结果集合为:1000,9000,0100,0900,0010,0090,0001,0009)
//那么这就形成了一张图,图中每个节点相邻8个节点,边为拨动的次数1
//那么题目就可以转换为:求图中0000节点到target节点的最短距离(不能经过deadends节点)
//使用BFS的遍历框架即可
public int openLock(String[] deadends, String target) {
     
    Queue<String> queue = new LinkedList<>();
    Set<String> visited = new HashSet<>();//记录已经访问过的节点
    Set<String> deadendsSet = new HashSet<>();
    for(String deadend : deadends){
     
        deadendsSet.add(deadend);
    }
    int minSpinCount = 0;
    queue.offer("0000");
    while(queue.size() != 0){
     
        int size = queue.size();
        for(int i = 0 ; i < size ; i++){
     
            String node = queue.poll();
            if(visited.contains(node)) continue;//如果已经访问过,那就跳过
            if(deadendsSet.contains(node)) continue; //如果是死亡数字,也跳过
            if(target.equals(node)){
     
                //如果找到target
                return minSpinCount;
            }
            visited.add(node);
            //将node其相邻的节点放入队列,node相邻的节点有8个
            for(int k = 0 ; k < 4 ;k++){
     
                queue.offer(spinUp(node,k));
                queue.offer(spinDown(node,k));
            }
        }
        //距离加1
        minSpinCount++;
    }
    return -1;
}

//将某一位向上拨
private String spinUp(String node, int i){
     
    char[] chars = node.toCharArray();
    if(chars[i] == '0'){
     
        chars[i] = '9';
    }else{
     
        chars[i]--;
    }
    return new String(chars);
}
//将某一位向下拨
private String spinDown(String node, int i){
     
    char[] chars = node.toCharArray();
    if(chars[i] == '9'){
     
        chars[i] = '0';
    }else{
     
        chars[i]++;
    }
    return new String(chars);
}

DFS

  • 深度优先搜索,主要用于解决可达性问题
  • 到达一个新节点时,立即对新节点进行遍历,如二叉树的前中后序遍历
  • 核心数据结构是
  • 注意事项
    – 需要用来保存信息,这样当更深的节点遍历完后,能够继续遍历当前节点,可以使用隐式递归
    – 可以考虑使用额外空间对遍历过的节点进行标记

解题框架

和回溯类似

result = []
def backtrack(路径,选择列表):
	if 满足结束条件:
		result.add(路径)
		return
	for 选择 in 选择列表:
		做选择
		backtrack(路径,选择列表)
		撤销选择

例1:二叉树的最小深度

111. 二叉树的最小深度

private int minDepth = Integer.MAX_VALUE;
public int minDepth(TreeNode root) {
     
    if(root==null) return 0;
    dfs(1,root);
    return minDepth;
}

//时间O(n),空间O(树的高度h)
private void dfs(int depth,TreeNode root){
     
    if(root.left == null && root.right ==null){
     
        //到达叶子节点
        minDepth = Math.min(depth,minDepth);
    }
    if(root.left != null){
     
        dfs(depth+1,root.left);
    }
    if(root.right != null){
     
        dfs(depth+1,root.right);
    }
}

回溯

  • 属于DFS,主要用于求解排列组合问题
  • 如果在解决一个问题时,可以分为多个步骤,每个步骤有多个选择,这样会形成多条路径(或形成一颗决策树),那么在解决这种问题时,可以使用回溯的思想
  • 解决一个回溯问题,就是决策树的遍历过程
  • 回溯算法就是穷举所有可能的选择,遍历所有解,因此是属于一种暴力解法
  • 回溯算法需要考虑3个问题 : 路径选择列表结束条件
    – 路径:到当前节点时已经做出的选择
    – 选择列表:当前节点可以做出的选择
    – 结束条件:到达决策树底层,无法再做出新的选择

解题框架

result = []
def backtrack(路径,选择列表):
	if 满足结束条件:
		result.add(路径)
		return
	for 选择 in 选择列表:
		做选择
		backtrack(路径,选择列表)
		撤销选择

例1 :全排列

46. 全排列

List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
     
    isVisited = new boolean[nums.length];
    backtrack(new ArrayList<>(),nums);
    return result;
}

//时间O(n * n!),递归栈空间O(n),辅助空间O(n)
private boolean[] isVisited;
private void backtrack(List<Integer> list,int[] nums){
     
    if(list.size() == nums.length){
     
        result.add(new ArrayList(list));
        return;
    }
    for(int i = 0 ; i < nums.length ;i++){
     
        if(isVisited[i]) continue;
        list.add(nums[i]);
        isVisited[i] = true;
        backtrack(list,nums);
        list.remove(list.size()-1);
        isVisited[i] = false;
    }
}
  • 结束条件:list.size() == nums.length
  • 选择列表:nums中未访问过的数字if(isVisited[i]) continue;
  • 做选择:list.add(nums[i]);
  • 撤销选择:list.remove(list.size()-1);

例2:N皇后

51. N皇后

class Solution {
     

    private List<List<String>> result = new ArrayList<>();

    //回溯法:每一行有n个列可供选择,遍历所有可能
    public List<List<String>> solveNQueens(int n) {
     
        //初始化棋盘
        char[][] board = new char[n][n];
        for(int i = 0 ; i < n ;i++){
     
            for(int j = 0 ; j < n ;j++){
     
                board[i][j] = '.';
            }
        }
        backtrack(board,0,n);
        return result;
    }

    private void backtrack(char[][] board, int row,int n){
     
        if(row == n){
     
            List<String> boardStrList = new ArrayList<>();
            for(int i = 0 ; i < n;i++){
     
                StringBuilder sb = new StringBuilder();
                for(int j = 0 ;j < n;j++){
     
                    sb.append(board[i][j]);
                }
                boardStrList.add(sb.toString());
            }   
            result.add(boardStrList);
            return;
        }

        //row有n种可能
        for(int col = 0 ; col < n;col++){
     
            //找到合适的列
            if(isValid(board,row,col,n)){
     
                board[row][col] = 'Q';
                backtrack(board,row+1,n);
                board[row][col] = '.';
            }
        }

    } 
    //判断该位置是不是合适放置
    private boolean isValid(char[][] board , int row ,int col,int n ){
     
        //判断列是否合适,这一行肯定是没有的
        for(int i = 0 ; i < n ;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;
    }

}
  • 结束条件:没有更多的行if(row == n)
  • 选择列表:每一行有n个列可供选择,使用isValid方法排除掉不可能的选择
  • 做选择:board[row][col] = 'Q';
  • 继续向下一行:backtrack(board,row+1,n);
  • 撤销选择:board[row][col] = '.';

例3:目标和

494. 目标和

  • 该题使用动态规划效率最高
class Solution {
     

    private int count = 0;

    //回溯,递归深度O(n),时间O(2^n)
    public int findTargetSumWays(int[] nums, int S) {
     
        backtrack(0,0,nums,S);
        return count;
    }

    private void backtrack(int i, int sum ,int nums[] ,int S){
     
        if(i == nums.length){
     
            if(sum == S){
     
                count++;
            }
            return;
        }
        //加
        backtrack(i+1,sum+nums[i],nums,S);
        //减
        backtrack(i+1,sum-nums[i],nums,S);
    }

}
  • 结束条件:最后一个数字参与计算之后,即i == nums.length
  • 选择列表:加或者减
  • 做选择:sum+nums[i]sum-nums[i]
  • 继续向下一个数字:backtrack(i+1,sum+nums[i],nums,S);backtrack(i+1,sum-nums[i],nums,S);
  • 不需要撤销选择,因为当前的sum是没有变的

你可能感兴趣的:(算法)