leetcode | 分类整理3(搜索)

搜索

BFS:广度优先

每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <= dj。利用这个结论,可以求解最短路径等 最优解 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径。

在程序实现 BFS 时需要考虑以下问题:

  • 队列:用来存储每一轮遍历得到的节点;
  • 标记:对于遍历过的节点,应该将它标记,防止重复遍历。

1) 每一层遍历的节点都与根节点距离相同:决定了什么时候path++:就是每遍历一层之前保留queue.size,这个size就是这一层的大小

2) 什么时候置flag?已经从队列中poll了之后

3) 最优解问题:因为每次遍历的都是和根节点距离相同的节点,所以最先到达终点的一定是最短路径

1091. 二进制矩阵中的最短路径(m)(经典BFS+visited)

重点:注意只有{0}一个数的情况

public int shortestPathBinaryMatrix(int[][] grid) {
        if(grid[0][0]==1 || grid[grid.length-1][grid[0].length-1]==1)
            return -1;
        if (grid.length == 1 && grid[0][0] == 0) {
            return 1;
        }
//        顺时针
        int[][] xOy = {{-1,0},{-1,-1},{0,-1},{1,-1},{1,0},{1,1},{0,1},{-1,1}};
//        int[][] xOy = {{1, 0}, {1, 1}, {1,-1}, {0, 1}, {0, -1}, {-1, 0},{-1, -1}, {-1, 1}};
        int n = grid.length;
        boolean[][] visited = new boolean[n][n];
        visited[0][0] = true;

        Queue<int[]> queue = new LinkedList<>();
        queue.add(new int[] {0,0});
        int pathLength = 0;

        while (!queue.isEmpty()) {
//            这一层节点的个数
            int size = queue.size();
            pathLength++;
            while (size-- > 0) {
                int[] node = queue.poll();
                int x = node[0];
                int y = node[1];
//                grid[x][y] = 1;
                for (int[] d : xOy) {
                    int nx = x + d[0];
                    int ny = y + d[1];

                    if (nx == n-1 && ny == n-1) {
                        return pathLength+1;
                    }

                    if (nx < 0 || nx >= n || ny < 0 || ny >= n || visited[nx][ny] || grid[nx][ny] == 1){
                        continue;
                    }

                    visited[nx][ny] = true;
                    queue.add(new int[] {nx,ny});
                }
            }
        }

        return -1;
    }

279. 完全平方数(m)

思路:

可以将每个整数看成图中的一个节点,如果两个整数之差为一个平方数,那么这两个整数所在的节点就有一条边。

要求解最小的平方数数量,就是求解从节点 n 到节点 0 的最短路径。

本题也可以用动态规划求解,在之后动态规划部分中会再次出现。

完全平方数的规律:1,4,9,16.。。中间刚好相差(3,5,7,9....),即n^2-(n-1)^2 = 2n-1

public int numSquares(int n) {
        List<Integer> list = generateSquares(n);
        Queue<Integer> queue = new LinkedList<>();
        boolean[] visited = new boolean[n+1];

        queue.add(n);
        visited[n] = true;

        int level = 0;

        while (!queue.isEmpty()) {
            int size = queue.size();
            level++;
            while (size-- > 0) {
                int cur = queue.poll();
                for (int i: list
                     ) {
                    int next = cur - i;
//                    因为LinkedList是按顺序读,我们存的时候就是(1,4,9...)
                    if (next < 0) {
                        break;
                    }
                    if (next == 0) {
                        return level;
                    }

                    visited[next] = true;
                    queue.add(next);

                }
            }
        }

        return n;
    }

    /**
     * 生成小于 n 的平方数序列
     * @return 1,4,9,...
     */
    private List<Integer> generateSquares(int n) {
        List<Integer> squares = new ArrayList<>();
        int square = 1;
        int diff = 3;
        while (square <= n) {
            squares.add(square);
            square += diff;
            diff += 2;
        }
        return squares;
    }

127. 单词接龙(m)(重点!!)

重点:

1. 如何比较两个String中是不是有一位字符不同?

所以需要建立一个图(graph),单词是节点,如果两个单词之间只有一位不同则有一条边

2. 如何做visited[]标记数组?因为wordList中是String类型,如何按下标记录标记?

建立graph的时候使用下标而不是单词作为节点

public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        if (!wordList.contains(endWord)) {
            return 0;
        }

//        便于建图
        wordList.add(beginWord);

//        Queue<String> queue = new LinkedList<>();
        Queue<Integer> queue = new LinkedList<>();

        List<Integer>[] graph = buildGraph(wordList);

        int n = wordList.size();

        boolean[] visited = new boolean[n+1];
        int end = 0;
        for (int i = 0 ; i < wordList.size() ;i ++) {
            if (!endWord.equals(wordList.get(i))) {
                end++;
            }
            else {
                break;
            }
        }

        queue.add(n-1);
        visited[n-1] = true;

        int step = 0;

        while (!queue.isEmpty()) {
            int size = queue.size();
            step++;
            while (size-- > 0) {
                int cur = queue.poll();
//                graph[cur]是个list<Integer>
                for (int next : graph[cur])  {
                    if (next == end) {
                        return step+1;
                    }
                    if (visited[next]) {
                        continue;
                    }
                    visited[next] = true;
                    queue.add(next);
                }
            }
        }

        return 0;
    }

    public boolean isConnected(String s1,String s2) {
        int diffCnt = 0;
        for (int i = 0; i < s1.length() && diffCnt <= 1; i++) {
            if (s1.charAt(i) != s2.charAt(i)) {
                diffCnt++;
            }
        }

        return diffCnt == 1;
    }

    public List<Integer>[] buildGraph(List<String> wordList) {
        int n = wordList.size();
        List<Integer>[] graph = new List[n];
        for (int i = 0 ; i < n  ; i++) {
            graph[i] = new LinkedList<>();
            for (int j = 0 ; j < n; j++) {
                if (isConnected(wordList.get(i),wordList.get(j))) {
                    graph[i].add(j);
                }
            }
        }

        return graph;
    }

DFS(深度优先,重点!!!) 

从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 可达性 问题

在程序实现 DFS 时需要考虑以下问题:

  • 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。
  • 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。

695. 岛屿的最大面积(m)

重点为了方便起见,本题没有采用visited数组,而是直接将遍历过的置为0。注意找到什么情况需要恢复visited数组,什么情况不需要(本题不需要,剑指offer66题机器人路径规划也不需要),像剑指offer中65题找矩阵中的路径就需要(因为是要找序列)

int[][] direction = {{0,1},{0,-1},{1,0},{-1,0}};

    public int maxAreaOfIsland(int[][] grid) {
        if (grid == null || grid.length <= 0) {
            return 0;
        }
//        几行
        int m = grid.length;
//        几列
        int n = grid[0].length;
        int maxArea = 0;
        for (int i = 0 ; i < m ; i++) {
            for (int j = 0 ; j < n; j++) {
                maxArea = Math.max(maxArea,dfs(grid,i,j));
            }
        }
        return maxArea;
    }

    private int dfs(int[][] grid, int i, int j) {
        if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] == 0) {
            return 0;
        }
//        本来应该是visited数组来标记,但是这里为了方便直接将访问过的置为0
        grid[i][j] = 0;
        int area = 1;
        for (int[] d : direction) {
            area += dfs(grid,i+d[0],j+d[1]);
        }
        return area;
    }

200. 岛屿的数量(m)

重点:矩阵看做有向图,就是有向图求连通分量的问题

和上题差不多,但是这里不需要统计大小,所以重点是遍历过的都改为0即可,所以直接用一个返回值为void的dfs。

int[][] direction = {{0,1},{0,-1},{1,0},{-1,0}};

public int numIslands(char[][] grid) {
    ...
    int count = 0;
    for (int i = 0; i < m ;i ++) {
        for (int j = 0; j < n ;j++) {
            if (grid[i][j] != '0')
            {
                dfs(grid,i,j);
                count++;
            }
        }
    }
    return count;
}

private void dfs(char[][] grid, int i, int j) {
    if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] == '0') {
        return ;
    }

    grid[i][j] = '0';
    for (int[] d : direction) {
        dfs(grid,i+d[0],j+d[1]);
    }
}

547. 朋友圈(m)

重点:矩阵看做无向图

就是无向图求连通分量的问题

不用两层循环,一层即可,因为必有m[i][j]=m[j][i]

就是先找0的朋友,假设1是,再找1的朋友。。。

public int findCircleNum(int[][] M) {
    if (M == null || M.length <= 0) {
        return 0;
    }

    int count = 0;
    boolean[] visited = new boolean[M.length];
    for (int i = 0; i < M.length; i++) {
        if (!visited[i]) {
            dfs(M,i,visited);
            count++;
        }
    }
    return count;
}

private void dfs(int[][] M,int i,boolean[] visited) {
    visited[i] = true;
    for (int j = 0; j < M.length ; j++) {
        if (M[i][j] == 1 && !visited[j]) {
            dfs(M,j,visited);
        }
    }
}

130. 被围绕的区域(m)

重点:找到边界和与边界相连的O,这些O不会被更改(因为边界处的不可能被包围),把这些O做上特殊标记#,然后剩下的O全部改为X即可。

找边界和边界相连的,相当于求最大岛屿面积,只是只能是有边界的地方,即求#的最大面积

417. 太平洋和大西洋水流问题(m)

重点:

1. 思路:逆流,看能不能从海洋流回去

2. Arrays.asList(),该方法是将数组转化为list,但是长度不可变(没有add、remove等方法)

List<List<Integer>> res = new ArrayList<>();
res.add(Arrays.asList(i, j));

回溯:

Backtracking(回溯)属于 DFS。

  • 普通 DFS 主要用在 可达性问题 ,这种问题只需要执行到特点的位置然后返回即可。
  • 而 Backtracking 主要用于求解 排列组合 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。

因为 Backtracking 不是立即返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题:

  • 在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素;
  • 但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素,所以标记可能要复位

[39,40],[46,47],[78,90]都是相同问题但是存在重复与否的差异,解题方法能够大同小异,仔细记住。

17. 电话号码的字母组合(m)

backtrack(digits,0,"");
private void backtrack(String digits, int index, String s) {
    if (index == digits.length()) {
        res.add(s);
        return;
    }

    char ch = digits.charAt(index);
    String letters = KEYS[ch-'0'];
    for (int i = 0 ; i < letters.length(); i++) {
        backtrack(digits,index+1,s+letters.charAt(i));
    }

    return;
}

93. 复原IP地址(m)

我们要知道IP的格式,IP地址总共有四段,每一段可能有一位,两位或者三位,范围是[0, 255]

当某段是三位时,我们要判断其是否越界(>255),还有一点很重要的是,当只有一位时,0可以成某一段,如果有两位或三位时,像 00, 01, 001, 011, 000等都是不合法的,所以我们还是需要有一个判定函数来判断某个字符串是否合法。

我们用k来表示当前分的段数,如果k = 4,则表示三个点已经加入完成,四段已经形成,若这时字符串刚好为空,则将当前分好的结果保存。若k != 4, 则对于每一段,我们分别用一位,两位,三位来尝试,分别判断其合不合法,如果合法,则调用递归继续分剩下的字符串,最终和求出所有合法组合

79. 单词搜索(m)——经典题

经典回溯!!!!有visited,有复位

private final static int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    private int m;
    private int n;
    public boolean exist(char[][] board, String word) {
        if (word == null) {
            return true;
        }
        if (board == null || board.length <= 0 || board[0].length <= 0) {
            return false;
        }

        m = board.length;
        n = board[0].length;
        boolean[][] visited = new boolean[m][n];

        for (int i = 0 ; i < m ; i++) {
            for (int j = 0 ;j < n; j++) {
                if (backTracking(0,i,j,visited,board,word)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean backTracking(int curLen, int i, int j, boolean[][] visited, char[][] board, String word) {
        if (curLen == word.length()) {
            return true;
        }
        if (i < 0 || i >= m || j < 0 || j >= n || visited[i][j] || board[i][j] != word.charAt(curLen)) {
            return false;
        }
        visited[i][j] = true;
        for (int[] d : direction) {
            if (backTracking(curLen+1,i+d[0],j+d[1],visited,board,word)) {
                return true;
            }
        }

//        标记一定要复位(回溯)
        visited[i][j] = false;

        return false;
    }

257. 二叉树的所有路径(e)——经典题

public List<String> binaryTreePaths(TreeNode root) {
        List<String> ans = new ArrayList<>();
        if (root == null) {
            return ans;
        }

        List<Integer> path = new ArrayList<>();
        backtracking(root,path,ans);
        return ans;
    }

    private void backtracking(TreeNode root, List<Integer> path, List<String> ans) {
        if (root == null){
            return;
        }
        path.add(root.val);
        if (root.left == null && root.right == null) {
            StringBuilder tmp = new StringBuilder();
            for (int i = 0; i < path.size(); i++) {
                tmp.append(path.get(i));
                if (i != path.size()-1) {
                    tmp.append("->");
                }
            }
            ans.add(tmp.toString());
        }
        backtracking(root.left,path,ans);
        backtracking(root.right,path,ans);
//        回溯
        path.remove(path.size()-1);
    }
}

46. 全排列(m)

public List<List<Integer>> permute(int[] nums) {
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        boolean[] visited = new boolean[nums.length];
        backtracking(nums,list,res,visited);

        return res;
    }

    private void backtracking(int[] nums, List<Integer> list, List<List<Integer>> res, boolean[] visited) {
        if (list.size() == nums.length) {
//            必须要重新new一个list
            res.add(new ArrayList<>(list));
            return;
        }

        for (int i = 0 ; i < nums.length ;i ++) {
            if (visited[i]) {
                continue;
            }
            visited[i] = true;
            list.add(nums[i]);
            backtracking(nums,list,res,visited);
//            回溯
            list.remove(list.size()-1);
            visited[i] = false;
        }
    }

47. 全排列2(m)

数组中可能有重复,但是最后的结果不能有重复的组合

重点:两步走1. 排序;2. !visited[i]剪枝

public List<List<Integer>> permuteUnique(int[] nums) {
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        boolean[] visited = new boolean[nums.length];
//        修改1:排序
        Arrays.sort(nums);
        backtracking(nums,list,res,visited);

        return res;
    }

    private void backtracking(int[] nums, List<Integer> list, List<List<Integer>> res, boolean[] visited) {
        if (list.size() == nums.length) {
            res.add(new ArrayList<>(list));
            return;
        }
        for (int i = 0;i < nums.length;i ++) {
            if (visited[i]) {
                continue;
            }
//            修改2:在 used[i - 1] 刚刚被撤销的时候剪枝,说明接下来会被选择,搜索一定会重复,故"剪枝"
            if (i > 0 && nums[i-1] == nums[i] && !visited[i-1]) {
                continue;
            }
            visited[i] = true;
            list.add(nums[i]);
            backtracking(nums,list,res,visited);
            visited[i] = false;
            list.remove(list.size()-1);
        }
    }

77. 组合(m)

重点:i的遍历不需要到n,只需要到n-k+1,用来剪枝,也是为了防止重复

这是一个回溯法函数,它将第一个添加到组合中的数和现有的组合作为参数。 backtrack(first, curr)

若组合完成- 添加到输出中。

遍历从 first t到 n的所有整数。

将整数 i 添加到现有组合 curr中。

继续向组合中添加更多整数 :
backtrack(i + 1, curr).

将 i 从 curr中移除,实现回溯。

public List<List<Integer>> combine(int n, int k) {
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        backtracking(n,k,1,list,res);

        return res;
    }

    private void backtracking(int n, int k, int start, List<Integer> list, List<List<Integer>> res) {
        if (list.size() == k) {
            res.add(new ArrayList<>(list));
        }

        for (int i = start; i <= n; i++) {
            list.add(i);
            backtracking(n,k,i+1,list,res);
            list.remove(list.size()-1);
        }
    }

39. 组合求和(m)

剑指中和为S的序列的变种

for (int i = start; i < candidates.length; i++) {
            if (candidates[i] <= target) {
                list.add(candidates[i]);
//                不能是i+1,因为可以重复
                backtracking(list,res,i,target-candidates[i],candidates);
                list.remove(list.size()-1);
            }
        }

40. 含有相同元素的组合求和(m)

重点:https://leetcode-cn.com/problems/permutations-ii/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liwe-2/

看47题

“解集不能包含重复的组合”,就提示我们得对数组先排个序(“升序”或者“降序”均可,下面示例中均使用“升序”)。
“candidates 中的每个数字在每个组合中只能使用一次”: 那就按照顺序依次减去数组中的元素(target-camdidate[i]),递归求解即可:遇到 0 就结算且回溯,遇到负数也回溯。
candidates 中的数字可以重复:  遇到(i != 0 && candidates[i-1] == candidates[i] && !visited[i-1]) 就continue(剪枝)

public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        boolean[] visited = new boolean[candidates.length];
//        排序
        Arrays.sort(candidates);
        backtracking(list, res, 0, visited, target, candidates);
        return res;
    }

    private void backtracking(List<Integer> list, List<List<Integer>> res, int start, boolean[] visited, int target, int[] candidates) {
        if (target == 0 ){
            res.add(new ArrayList<>(list));
            return;
        }

        for (int i = start;i < candidates.length; i++) {
            if (i != 0 && candidates[i-1] == candidates[i] && !visited[i-1]) {
                continue;
            }
            if (candidates[i] <= target) {
                list.add(candidates[i]);
                visited[i] = true;
                backtracking(list,res,i+1,visited,target-candidates[i],candidates);
                visited[i] = false;
                list.remove(list.size()-1);
            }
        }
    }

78. 子集(m)

public List<List<Integer>> subsets(int[] nums) {
        List<Integer> temSub = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        boolean[] visited = new boolean[nums.length];
        for (int size = 0; size <= nums.length; size++) {
            backtracking(nums,temSub,size,0,res,visited);
        }


        return res;
    }

    private void backtracking(int[] nums, List<Integer> temSub, int size,int start,List<List<Integer>> res, boolean[] visited) {
        if (temSub.size() == size) {
            res.add(new ArrayList<>(temSub));
            return;
        }

        for (int i = start; i < nums.length; i ++) {
            if (visited[i]) {
                continue;
            }
            visited[i] = true;
            temSub.add(nums[i]);
            backtracking(nums,temSub,size,i+1,res,visited);
            visited[i] = false;
            temSub.remove(temSub.size()-1);
        }
    }

90. 子集2

有重复元素

加上两步:1.排序;2.!visited[i-1]

发布了53 篇原创文章 · 获赞 5 · 访问量 1512

猜你喜欢

转载自blog.csdn.net/zhicheshu4749/article/details/104259877