算法 - DFS/BFS

写DFS函数的时候首先确定当前位置是否已经加入路径

DFS函数大概率会传递“位置信息”,根据位置信息获取下一步的选择,(大部分是在循环中)选择、执行、回退

在哪做选择,就在哪退出选择,参考题9

def DFS():
	# 出口
	if 满足出口条件:
		将满足条件的路径加入结果
		方法返回

	for 选择 in 选择列表:
    	# 做选择
    	将该选择从选择列表移除
    	路径.add(选择)
    	DFS(路径, 选择列表)
    	# 撤销选择
    	路径.remove(选择)
    	将该选择再加入选择列表

DFS和BFS本质上是决策树的遍历
动态规划要求重叠子问题的条件,而DFS/BFS则是没有任何条件的纯暴力搜索。而且,BFS找到的路径一定是最短的,但代价通常比DFS要大得多。

DFS通常的问题形式是,找各种意义的路径:棋盘摆放、集合划分等等。
BFS通常的问题形式是,寻找(决策树上的)最短路径。

画出递归树,想想每个节点该怎么做

1.DFS - N皇后问题

  • 每个格子都面临选择:放或者不放皇后
  • 注意一个优化点:如果某行放了皇后,则下次递归直接跳到下行
class Solution(object):
    res = []
    def solveNQueens(self, n):
        """
        :type n: int
        :rtype: List[List[str]]
        """
        self.res = []
        path = [["." for _ in range(n)] for _ in range(n)]
        self.dfs(path, 0, 0, n)
        return self.res


    def dfs(self, path, x, y, queens_left):
        n = len(path)
        # 出口
        if queens_left == 0:
            path_strs = []
            for i in range(n):
                path_strs.append("".join(path[i]))
            self.res.append(path_strs)
            return
        elif x > n - 1 or y > n - 1:
            return

        # 一般情况
        # 选择1:放皇后
        is_queen_available = True
        for k in range(n):  # 检查行和列
            is_queen_available = False if path[x][k] == "Q" else is_queen_available
            is_queen_available = False if path[k][y] == "Q" else is_queen_available
        temp_x, temp_y = x - 1, y - 1  # 检查左上方和右上方
        while (temp_x >= 0 and temp_y >= 0):
            if path[temp_x][temp_y] == "Q":
                is_queen_available = False
            temp_x -= 1
            temp_y -= 1
        temp_x, temp_y = x - 1, y + 1
        while (temp_x >= 0 and temp_y <= n - 1):
            if path[temp_x][temp_y] == "Q":
                is_queen_available = False
            temp_x -= 1
            temp_y += 1

        if is_queen_available:
            path[x][y] = "Q"  # 做选择
            queens_left -= 1
            self.dfs(path, x + 1, 0, queens_left)
            path[x][y] = "."  # 撤销选择
            queens_left += 1

        # 选择2:不放皇后
        if y == n - 1:
            x_new = x + 1
            y_new = 0
        else:
            x_new = x
            y_new = y + 1
        self.dfs(path, x_new, y_new, queens_left)

2.DFS - 子集(幂集)

  • 每次递归都面临问题:是否将下标 idx 的数字加入结果集
class Solution {

    List<List<Integer>> result;
    List<Integer> path;

    public List<List<Integer>> subsets(int[] nums) {
        // DFS,但是不能搜索idx之前的位置
        path = new LinkedList<>();
        result = new LinkedList<>();
        result.add(new LinkedList<>());  // 手动加入空集
        dfs(nums, 0);
        return result;
    }

    private void dfs(int[] nums, int idx) {
        if (idx == nums.length) {
            return;
        }
        // 将nums[idx]添加到结果
        path.add(nums[idx]);
        result.add(new LinkedList(path));
        // 保持nums[idx]继续搜索
        dfs(nums, idx + 1);
        // 撤回nums[idx]继续搜索
        path.remove(path.size() - 1);
        dfs(nums, idx + 1);
    }
}

3.DFS - 组合

  • 给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合
class Solution {

    List<List<Integer>> result = new LinkedList<>();
    List<Integer> path = new LinkedList<>();

    public List<List<Integer>> combine(int n, int k) {
        dfs(n, k, 0, 1);
        return result;
    }

    private void dfs(int n, int k, int count, int idx) {
        // 递归出口
        if (count == k) {
            result.add(new LinkedList<>(path));
            return;
        }
        if (idx > n) {
            return;
        }
        // 加入当前位置
        path.add(idx);
        dfs(n, k, count + 1, idx + 1);
        path.remove(path.size() - 1);
        // 不加入当前位置
        dfs(n, k, count, idx + 1);
    }
}

4.DFS - 括号生成

  • 数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
  • 有效括号组合需满足:左括号必须以正确的顺序闭合。
class Solution {

    List<String> result = new LinkedList<>();
    StringBuilder path = new StringBuilder();

    public List<String> generateParenthesis(int n) {
        dfs(n, 0, 0);
        return result;
    }

    private void dfs(int n, int leftCount, int rightCount) {
        // 递归出口
        if (rightCount == n) {
            result.add(path.toString());
            return;
        }
        if (leftCount > rightCount) {
            // 放入右括号
            path.append(")");
            dfs(n, leftCount, rightCount + 1);
            path.deleteCharAt(path.length() - 1);
        }
        if (leftCount < n) {
            // 放入左括号
            path.append("(");
            dfs(n, leftCount + 1, rightCount);
            path.deleteCharAt(path.length() - 1);
        }
    }
}

5.BFS - 二叉树的最小深度

找好搜索的结束条件,当一个节点没有左右子节点时,到达叶节点。

class Solution {
    public int minDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        // 层序遍历,如果某个节点没有叶子节点则返回
        Deque<TreeNode> q = new LinkedList<>();
        q.offerLast(root);
        int depth = 0;
        while (q.size() > 0) {
            depth++;
            int nodeNum = q.size();
            for (int i = 0; i < nodeNum; i++) {
                TreeNode node = q.pollFirst();
                if (node.left == null && node.right == null) {
                    return depth;
                }
                if (node.left != null) {
                    q.offerLast(node.left);
                }
                if (node.right != null) {
                    q.offerLast(node.right);
                }
            }
        }
        return 0;
    }
}

6.BFS - 转盘锁

class Solution(object):
    def openLock(self, deadends, target):
        """
        :type deadends: List[str]
        :type target: str
        :rtype: int
        """
        # 工具方法
        def plus(num, bit):
            origin = int(num[bit])
            if origin < 9:
                origin += 1
            else:
                origin = 0

            if bit == 0:
                return str(origin) + num[1: ]
            elif bit == 3:
                return num[0: 3] + str(origin)
            else:
                return num[0: bit] + str(origin) + num[bit + 1: ]
            
        def minus(num, bit):
            origin = int(num[bit])
            if origin > 0:
                origin -= 1
            else:
                origin = 9

            if bit == 0:
                return str(origin) + num[1: ]
            elif bit == 3:
                return num[0: 3] + str(origin)
            else:
                return num[0: bit] + str(origin) + num[bit + 1: ]

        # 特判
        if "0000" in deadends:
            return -1

        # BFS
        added = ["0000"]  # 不走回头路,重要!
        queue = ["0000"]
        times = 0
        while (len(queue) > 0):
            length = len(queue)
            for i in range(length):
                code = queue[i]
                # 出口
                if code == target:
                    return times
                # 处理每一位
                for j in range(4):
                    temp_code = plus(code, j)  # +1
                    if temp_code not in deadends and temp_code not in added:
                        queue.append(temp_code)
                        added.append(temp_code)
                    temp_code = minus(code, j)  # -1
                    if temp_code not in deadends and temp_code not in added:
                        queue.append(temp_code)
                        added.append(temp_code)
            times += 1
            queue = queue[length:]

        return -1

以上为经典的BFS解法。对于已知终点的BFS问题,可以采用双向BFS加速运行,从起点和终点向中间扩展,直到出现交集。

int openLock(String[] deadends, String target) {
    Set<String> deads = new HashSet<>();
    for (String s : deadends) deads.add(s);
    // 用集合不用队列,可以快速判断元素是否存在
    Set<String> q1 = new HashSet<>();
    Set<String> q2 = new HashSet<>();
    Set<String> visited = new HashSet<>();
    
    int step = 0;
    q1.add("0000");
    q2.add(target);
    
    while (!q1.isEmpty() && !q2.isEmpty()) {
        // 哈希集合在遍历的过程中不能修改,用 temp 存储扩散结果
        Set<String> temp = new HashSet<>();

        /* 将 q1 中的所有节点向周围扩散 */
        for (String cur : q1) {
            /* 判断是否到达终点 */
            if (deads.contains(cur))
                continue;
            if (q2.contains(cur))
                return step;
            visited.add(cur);

            /* 将一个节点的未遍历相邻节点加入集合 */
            for (int j = 0; j < 4; j++) {
                String up = plusOne(cur, j);
                if (!visited.contains(up))
                    temp.add(up);
                String down = minusOne(cur, j);
                if (!visited.contains(down))
                    temp.add(down);
            }
        }
        /* 在这里增加步数 */
        step++;
        // temp 相当于 q1
        // 这里交换 q1 q2,下一轮 while 就是扩散 q2
        q1 = q2;
        q2 = temp;
    }
    return -1;
}

7.DFS - 组合总数

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

class Solution {

    List<List<Integer>> result = new LinkedList<>();
    List<Integer> path = new LinkedList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        dfs(candidates, target, 0, 0);
        return result;
    }

    private void dfs(int[] candidates, int target, int sum, int idx) {
        // 递归出口
        if (idx >= candidates.length) {
            return;
        }
        if (sum == target) {
            result.add(new LinkedList<>(path));
            return;
        }
        // 1.使用当前位置的值
        if (candidates[idx] + sum <= target) {
            path.add(candidates[idx]);
            dfs(candidates, target, sum + candidates[idx], idx);
            path.remove(path.size() - 1);
        }
        // 2.不使用当前位置的值
        dfs(candidates, target, sum, idx + 1);
    }
}

8.DFS - 字符串的全排列

class Solution {

    Set<String> result = new HashSet<>();
    StringBuilder path = new StringBuilder();
    boolean[] visited;

    public String[] permutation(String s) {
        visited = new boolean[s.length()];
        dfs(s, 0);
        return result.toArray(new String[result.size()]);
    }

    private void dfs(String s, int count) {
        int length = s.length();
        if (count == length) {
            result.add(path.toString());
            return;
        }
        for (int i = 0; i < s.length(); i++) {
            if (!visited[i]) {
                visited[i] = true;
                path.append(s.charAt(i));
                dfs(s, count + 1);
                visited[i] = false;
                path.deleteCharAt(path.length() - 1);
            }
        }
    }
}

9.DFS - 是否存在word路径

class Solution {

    private char[] letters;
    private int m;
    private int n;
    private boolean[][] visited;
    private char[][] board;

    public boolean exist(char[][] board, String word) {
        m = board.length;
        n = board[0].length;
        letters = word.toCharArray();
        this.board = board;
        visited = new boolean[m][n];
        // 初始化访问矩阵
        for (int x = 0; x < m; x++) {
            for (int y = 0; y < n; y++) {
                    visited[x][y] = false;
            }
        }
        // 执行搜索
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (letters[0] == board[i][j]) {
                    if (dfs(i, j, 1)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private boolean dfs(int x, int y, int idx) {
        // 递归出口
        if (idx == letters.length) {
            return true;
        }
        visited[x][y] = true;  //  DFS: 向前
        // 向上下左右扩展
        int lx = x;
        int ly = y - 1;
        boolean l = false;
        int rx = x;
        int ry = y + 1;
        boolean r = false;
        int ux = x - 1;
        int uy = y;
        boolean u = false;
        int dx = x + 1;
        int dy = y;
        boolean d = false;
        if (ly >= 0 && !visited[lx][ly] && board[lx][ly] == letters[idx]) {
            l = dfs(lx, ly, idx + 1);
        }
        if (ry < n && !visited[rx][ry] && board[rx][ry] == letters[idx]) {
            r = dfs(rx, ry, idx + 1);
        }
        if (ux >= 0 && !visited[ux][uy] && board[ux][uy] == letters[idx]) {
            u = dfs(ux, uy, idx + 1);
        }
        if (dx < m && !visited[dx][dy] && board[dx][dy] == letters[idx]) {
            d = dfs(dx, dy, idx + 1);
        }
        visited[x][y] = false;  // DFS: 撤销
        return l || r || u || d;
    }
}

10.DFS + MEMO - 最长递增路径

给定一个 n 行 m 列矩阵 matrix ,矩阵内所有数均为非负整数。 你需要在矩阵中找到一条最长路径,使这条路径上的元素是递增的。并输出这条最长路径的长度

  • 如果用纯DFS会超时
  • 发现重叠子问题:记录以每个位置开始的最长路径长度,如果邻接的格子比当前更大,则一定没走过
class Solution {

    int[][] dp;  // 记录从dp[i][j]开始的最长路径长度

    public int longestIncreasingPath(int[][] matrix) {
        int result = 0;
        int m = matrix.length;
        int n = matrix[0].length;
        dp = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                result = Math.max(dfs(matrix, i, j, m, n), result);
            }
        }
        return result;
    }

    private int dfs(int[][] matrix, int x, int y, int m, int n) {
        // 查表
        if(dp[x][y] > 0) {
            return dp[x][y];
        }
        // 求上下左右的最大长度
        int u = 0;
        int d = 0;
        int l = 0;
        int r = 0;
        int ux = x - 1;
        int uy = y;
        int dx = x + 1;
        int dy = y;
        int lx = x;
        int ly = y - 1;
        int rx = x;
        int ry = y + 1;
        // 做选择
        if (ux >= 0 && matrix[ux][uy] > matrix[x][y]) {
            u = dfs(matrix, ux, uy, m, n);
        }
        if (dx < m && matrix[dx][dy] > matrix[x][y]) {
            d = dfs(matrix, dx, dy, m, n);
        }
        if (ly >= 0 && matrix[lx][ly] > matrix[x][y]) {
            l = dfs(matrix, lx, ly, m, n);
        }
        if (ry < n && matrix[rx][ry] > matrix[x][y]) {
            r = dfs(matrix, rx, ry, m, n);
        }
        dp[x][y] = Arrays.stream(new int[]{u, d, l, r}).max().getAsInt() + 1;
        return dp[x][y];
    }
}

11.DFS - 循环依赖

class P3:
    def __init__(self):
        self.res = []
        self.path = []

    def find(self, serivces):
        num = len(serivces)
        visited = [False for _ in range(num)]

        def dfs(idx, num):
            # idx已经加入!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            (n0, n1) = serivces[idx]

            # 检查是否有循环依赖
            for t in self.path[: -1]:  # 因为最后一个元素是新加入的所以不能用来检测成环!!!!!!!!!!!!!!!!!!!!
                if t[0] == n1 or t[1] == n1:
                    self.res.append(self.path[:])
                    return None

            # 继续进行
            for i in range(num):
                if not visited[i] and serivces[i][0] == n1:
                    self.path.append(serivces[i])
                    visited[i] = True
                    dfs(i, num)
                    self.path.pop()
                    visited[i] = False

        for i in range(num):
            self.path.append(serivces[i])
            visited[i] = True
            dfs(i, num)
            self.path.pop()
            visited[i] = False
        return self.res

print(P3().find([('A', 'B'), ('B', 'C'), ('C', 'A'), ('A', 'D')]))

12.DFS + MEMO - 跳跃游戏

添加链接描述
在这里插入图片描述

  • DFS + MEMO
    • 使用 Set 记录无法到达的位置,避免重复搜索
class Solution {

    Set<Integer> notAvailable = new HashSet<>();  // 记录不可达的位置,避免重复搜索

    public boolean canJump(int[] nums) {
        return dfs(nums, 0);
    }

    private boolean dfs(int[] nums, int idx) {
        if (idx >= nums.length - 1) {
            return true;
        }
        if (notAvailable.contains(idx)) {
            return false;
        }
        for (int i = 1; i <= nums[idx]; i++) {
            if (dfs(nums, idx + i)) {
                return true;
            }
        }
        notAvailable.add(idx);
        return false;
    }
}
  • 更巧妙的贪心法
class Solution {
    public boolean canJump(int[] nums) {
        // 贪心法
        int mostRightIdx = 0;
        for (int i = 0; i < nums.length; i++) {
            if (mostRightIdx < i) {
                return false;
            }
            mostRightIdx = Math.max(mostRightIdx, nums[i] + i);
        }
        return (mostRightIdx >= nums.length - 1);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值