Depth First Search (Java) - ShuweiLeung/Thinking GitHub Wiki

该页笔记始于"2018.7.16",结于“2018.7.25”,使用Java语言

能用DFS的题目往往也能用BFS,所以拿到搜索类的题目,思考DFS简单还是BFS简单,挑简单的搜索方法做


1.(733:Easy)Flood Fill

  • 该题就是将上下左右相邻的具有相同颜色的pixel全部改为new color即可,递归求解,注意越界情况的处理。

2.(559:Easy)Maximum Depth of N-ary Tree

  • 该题是求三叉树的高度,不论求几叉树的高度,用BFS来求解无疑是最简单高效的解法,而不是DFS。

3.(841:Medium)Keys and Rooms

  • 该题设置一个set储存未访问的房间编号从1开始算起,因为room 0我们一定可以访问),然后我们遍历从room 0开始遍历,拿到当前房子的钥匙。如果钥匙所对应的房间还未访问过的话,我们先将该房间从set集合中删去,然后再进入钥匙对应的房间进行更深层次的遍历;否则当前钥匙没什么作用,直接略过即可。最后在主函数中判断set集合的size是否为0,若为0说明全部房间都已访问过,返回true。
  • 具体细节参看实现代码。

4.(烂题不懂)(529:Medium)Minesweeper

  • 该题类似于扫雷,因为不会玩扫雷,所以不会做该题,也不愿意搞懂该题。

下次复习时再和同学请教该烂题

5.(547:Medium)(经典题)Friend Circles

  • 该题如果使用DFS(Union-Find也可以),遍历每个关系x->y(y是x的direct friend),接着进行更深层次的搜索,遍历y->z(z是y的direct friend),他们都属于同一个friend circle。对于已经搜索过的关系,我们将M[i][j]标记为-1,当在搜索过程中若遇到某个位置的关系值为-1,直接跳过即可。

该题也可以使用Union-Find进行求解,但我是使用的DFS,下次复习时再研究union-find解法

6.(491:Medium)(非常经典题)Increasing Subsequences

  • 该题其实就是一道典型的DFS题目,依次按顺序搜索数组序列进行拼接,保证后面的数字大于等于前面的数字即可。关键在于,如果将所有的搜索路径都存入一个总的结果集list集合中,当搜索路径长度大于等于2判断该路径在list集合中是否出现时,非常耗时,相当于从index=0开始遍历到list集合的末尾结束,最后导致"超时"解决的方法是换用查找速度更快的数据结构,如set集合,直接根据hash码进行搜索,而不是根据索引遍历所有元素进行比对,可以通过所有的test case,beat 67%。

7.(200:Medium)(非常经典题)Number of Islands

  • 该题与第5题(547)思路有些相似,我们在这里使用DFS(union-find、BFS也可以),对于已经标记过的'1'结点标记为'*',然后试探其上下左右四个方向是否还有'1'结点,有的话即进行更深层次的搜索,相当于扩大当前land的面积。当与某一个'1'结点相邻的所有land都被探索完后,递归搜索函数会返回到最上层,那么当下一次再遇到一个'1'结点,就是另外一个land,要将总land数变量num加1。(beat 98.75%)
  • Union-Find解法,有时对于follow up也需要参考Union Find解法

该题还可以使用union-find、bfs,下次复习时再研究其他两种解法

8.(473:Medium)(非常非常经典题)Matchsticks to Square

  • 该题一开始想用没有任何技巧的DFS来求解,即遍历所有的组合,使当前的火柴棍长度为square的边长,然后再拼凑下一条正方形边长,但会遇到两个问题:1.DFS过程中,遍历的索引可能是跳跃的,因为当前的火柴棍长度和与正方形目标边长相差的长度可能在后面的索引位置,它们可能中间相差好几个火柴棍 2.如何标记当前火柴棍已经被使用过,且在遍历到后面的火柴棍时,前面可能还有未使用的火柴棍。总之这种逻辑处理很混乱,不可取。正确思路如下(参考Discuss高票答案)

For better description of the problem, let's reformulate it in the following symbolic way:

Given an array nums with n elements, let T(i, s1, s2, s3, s4) denote whether we can partition the subarray nums[0, i](both inclusive) into four disjoint groups such that the sum of elements in the j-th group is sj, with j = 1, 2, 3, 4.

With this definition, our original problem will be T(n - 1, side, side, side, side) where side is the side length of the square.

To solve for T(i, s1, s2, s3, s4), note that the last element of the subarray nums[0, i] (which is nums[i]) must belong to one of the disjoint groups, therefore we have the following recurrence relation:

T(i, s1, s2, s3, s4) = T(i - 1, s1 - nums[i], s2, s3, s4) ||
                       T(i - 1, s1, s2 - nums[i], s3, s4) ||
                       T(i - 1, s1, s2, s3 - nums[i], s4) ||
                       T(i - 1, s1, s2, s3, s4 - nums[i])

The interpretation is as follows: if nums[i] belongs to the j-th group, we subtract it from sj, then recursively solve for the subproblem with reduced array size and modified group sum. However, we do not know which group it belongs to beforehand, therefore each of the groups will be examined until we either hit the right group or determine that no partition is possible. Also note that if all elements in the input array are positive, an element cannot fall into a group with a group sum smaller than the element itself, i.e., nums[i] cannot belong to the j-th group if nums[i] > sj.

The termination condition for the recursion is when the subarray is empty, i.e., i = -1, at which time we will check whether the sum of each group is zero. If so, a partition is found and return true; otherwise no partition is possible and return false.

  • 单纯使用上面的思路而不进行任何优化,虽然会通过所有的测试例,但却非常的耗时。为了提高代码效率,可以将nums数组进行排序,然后从后往前进行数组的遍历处理(因为较小的边选择性很多,但较大边选择性很少)。具体的解题思路可以参考代码实现注释及参考链接
  • 参考链接Java DFS solution with various optimizations (sorting, sequential-partition, DP)

9.(417:Medium)(非常非常经典题)Pacific Atlantic Water Flow

  • 刚开始没有理解题意,以为加括号的点是一条路径,连通两大洋的,但是看来看去感觉也不太对,后来终于明白了,是每一个点都有一条单独的路径来通向两大洋
  • 在理解题意的基础上,我们就遇到一个问题:如果从某个坐标开始尝试搜索,看其是否能到达边界,那么怎样判断该坐标既能到达太平洋又能到达大西洋呢?当然我们可以统计所有从该坐标能到达的边界点,判断这个边界点集合是否同时与太平洋和大西洋的边界重合,但这个思路应该是超时的
  • 那么何不将该问题分解成两个子问题呢?我们分别求能到达太平洋的点集和能到达大西洋的点集,但此时又遇到一个问题,怎样求呢?如果按正向方式求,即遍历每一个坐标,并对其搜索,看看是否能到达两大洋,那么又变成了上面的思路。那么,如果倒过来按反向方式,从两大洋的边界出发,分别求能到达两大洋边界的点集合,最后两集合的并集就是答案,这个思路就是该题的解题思想。
  • 具体来讲,可以采用BFS或DFS来进行实现,核心思路是建立两个与输入矩阵同样大小的二维数组,对于能到达某个大洋的坐标(x,y),我们在对应的二维数组的x、y索引处将其标记为true,表示分别能到达两大洋的坐标位置。然后从两大洋边界出发,标记比当前高度高的、未访问过的四个方向的坐标,并递归的搜索。最后取相同位置下两个二维数组同时为true的坐标返回即可。具体的BFS和DFS实现细节请参看代码实现及注释

10.(207:Medium)(非常非常经典题)Course Schedule

  • 本意是想使用BFS,将以某个课程为根的树上的所有结点都遍历后,若没有环则说明所有课程都可以上,但不能判断[(0,1),(0,2),(1,2)]的情况,否则若用map集合来记录某个课程的搜索路径来判断是否有环,思路很复杂且超时。
  • 正确的思路是使用"拓扑排序topological sort",拓扑排序不仅可以解决有向图中各个课程的先后顺序问题(在能上完全部课程的前提下),而且可以判断有向图中是否有环。拓扑排序需要借助DFS,且将每个课程的状态分为visiting(正在处理)、visited(已处理)两种,若正在处理的课程有一个指向另一个正在处理的课程,说明有环。具体拓扑排序的思路及解题方法请参看代码及参考视频
  • 参考链接花花中文拓扑排序思路讲解

该题好像用BFS也能做,下次复习时再研究

11.(210:Medium)(非常非常经典题)Course Schedule II

  • 该题其实与第10题(210)没什么大的区别,只不过是在更改课程为visited状态时,将其加入到一个修课顺序的list集合中而已,仍然是使用拓扑排序来求解。
  • 具体细节可以参考代码实现及第10题(207)的参考链接教程。
  • 直接参看下边的拓扑排序思路代码:
    //拓扑排序的典型解法,该题是一个经典应用题目
    public int[] findOrder(int numCourses, int[][] prerequisites) { 
        if (numCourses == 0) return null;
        // Convert graph presentation from edges to indegree of adjacent list.
        int indegree[] = new int[numCourses], order[] = new int[numCourses], index = 0;
        for (int i = 0; i < prerequisites.length; i++) // Indegree - how many prerequisites are needed.
            indegree[prerequisites[i][0]]++;    

        Queue<Integer> queue = new LinkedList<Integer>();
        for (int i = 0; i < numCourses; i++) 
            if (indegree[i] == 0) {
                // Add the course to the order because it has no prerequisites.
                order[index++] = i;
                queue.offer(i);
            }

        // How many courses don't need prerequisites. 
        while (!queue.isEmpty()) {
            int prerequisite = queue.poll(); // Already finished this prerequisite course.
            for (int i = 0; i < prerequisites.length; i++)  {
                if (prerequisites[i][1] == prerequisite) {
                    indegree[prerequisites[i][0]]--; 
                    if (indegree[prerequisites[i][0]] == 0) {
                        // If indegree is zero, then add the course to the order.
                        order[index++] = prerequisites[i][0];
                        queue.offer(prerequisites[i][0]);
                    }
                } 
            }
        }

        return (index == numCourses) ? order : new int[0];
    }

12.(542:Medium)(非常非常经典题)01 Matrix

  • 该题选用BFS做可能较容易一些,如果正向DFS求1到0的距离,会很复杂(不信参看自己写的代码),而且涉及到如何处理已经遍历过的位置的问题。那么我们可以反过来,求0到1的距离,因为0自身的距离已经求出,我们可以使用BFS逐步扩散的求周围1到0的距离,会更容易一些。
  • 具体可以参看代码实现及注释

可能DFS除了自己写的思路,还有更便捷的处理方式,下次复习时再研究

13.(332:Medium)(经典题)Reconstruct Itinerary

  • 该题有两个点需要注意:1.当从一个departure出发能到达两个目的地时,要先到字典序较小的目的地 2.该题不能使用BFS,因为BFS使用一个队列(考虑到按字典序大小排列,这里使用优先队列),当departure A能到达两个目的地B和C时,先到达B,假如B还有可到达的目的地D,此时将D加入队列中,优先队列中存在C和D,那么下次再取的时候会取到C,这时候错误就发生了,因为现在所在的是B地,他只有B->D的路径,而没有B->C的路径。所以应该使用DFS
  • 我们使用DFS,建立一个map,key是departure,value是一个PriorityQueue,按字典序排列departure可以到达的其他目的地。然后从"JFK"开始,每次取当前起点能到达的目的地中字典序最小的,然后进行递归搜索。具体请参看代码及参考链接。
  • 参考链接花花中文讲解
  • In this problem, the path we are going to find is an itinerary which: 1. uses all tickets to travel among airports2. preferably in ascending lexical order of airport code.
  • Keep in mind that requirement 1 must be satisfied before we consider 2. If we always choose the airport with the smallest lexical order, this would lead to a perfectly lexical-ordered itinerary, but pay attention that when doing so, there can be a "dead end" somewhere in the tickets such that we are not able visit all airports (or we can't use all our tickets), which is bad because it fails to satisfy requirement 1 of this problem. Thus we need to take a step back and try other possible airports, which might not give us a perfectly ordered solution, but will use all tickets and cover all airports. Thus it's natural to think about the "backtracking" feature of DFS.
  • 代码如下:
    Map<String, PriorityQueue<String>> flights;
    LinkedList<String> path;

    public List<String> findItinerary(String[][] tickets) {
        flights = new HashMap<>();
        path = new LinkedList<>();
        for (String[] ticket : tickets) { 	//简历map
            flights.putIfAbsent(ticket[0], new PriorityQueue<>());
            flights.get(ticket[0]).add(ticket[1]);
        }
        dfs("JFK");
        return path;
    }

    public void dfs(String departure) {
        PriorityQueue<String> arrivals = flights.get(departure);
        while (arrivals != null && !arrivals.isEmpty())  //按字典序从小到大遍历所有从当前departure出发能到达的目的地
            dfs(arrivals.poll()); //遍历当前departure的可达地点时,将领近机场弹出pq,起到了visited数组的作用,当再次访问departure时,不会出现无限递归
        path.addFirst(departure);   //将其所有目的地都遍历完以后,再把出发地加入路径集合中(backtracking,保证将死胡同的目的地放到后边访问)
    }

14.(133:Medium)(经典题)Clone Graph

  • 该题就是一个递归克隆问题,我记得之前在做tree类型的题目时,有递归克隆树的问题,该题相当于递归克隆一个图。与递归克隆树不同,在克隆图的过程中,可能会出现环,即指针的回指,因此我们在这里要用到一个map来存储已经遍历的结点的引用,当我们发现某个结点的neighbor出现在visited map集合中,则直接从map中取出对应的结点,将该visited结点加入当前结点的neighbors集合中,避免重复的无限循环递归;如果该邻居结点之前没访问过,则新建立一个结点,进行深层次的递归建图。
  • 具体思路可以参看代码实现及注释

15.(130:Medium)(非常经典题)Surrounded Regions

  • 该题的题意表述不是很清楚,核心思想是:对于四周被'X'包围的'O',我们要把这块区域全部换成'X';对于没有被'X'包围的'O',则仍保持不变。对于后者,其实就是指与边界相邻的'O'区域。因此,为了找到不被包围的'O'区域,我们从边界的'O'入手,BFS求出所有与边界'O'相邻的其他'O'位置,利用一个二维数组alive来记录存活的'O',最后统一遍历alive数组,将board中未存活的'O'换成'X'即可。
  • 具体思路可以参看代码实现及参考链接
  • 参考链接中文视频讲解

16.(430:Medium)(经典题)Flatten a Multilevel Doubly Linked List

  • 类似于多个链表的合并,具体的细节没有细看,自己实现了一下发现存在无限递归错误,递归终止条件没有处理好,但该题又没有办法在IDE环境下进行调试,于是直接参看discuss中答案提交。
  • 像这种递归处理,直接从最简单的情况开始思考,这样才容易推导,否则考虑的情况越复杂,逻辑越混乱

下次复习时再理一理该题的链接逻辑思路

17.(679:Hard)(非常经典题)24 Game

  • 该题直接看的Discuss,主要思路是从数组中任取两个数,将其做加减乘除运算,并将结果与剩余的其他数字进行组合,看看是否能构成24。直接参看下面的代码及注释:
    boolean res = false; //返回结果,true为能组成24
    final double eps = 0.001;  //精度,当double的数值小于0.001时,认为该数等于0

    public boolean judgePoint24(int[] nums) {
        List<Double> arr = new ArrayList<>();
        for(int n: nums) arr.add((double) n);  //将nums整型数组改为double的arraylist
        helper(arr);
        return res;
    }

    private void helper(List<Double> arr){
        if(res) return;
        if(arr.size() == 1){  //当数组元素只有1时,我们判断是否为24
            if(Math.abs(arr.get(0) - 24.0) < eps)
                res = true;
            return;
        }
        for (int i = 0; i < arr.size(); i++) {  //遍历数组,求所有两个数字的组合
            for (int j = 0; j < i; j++) {
                List<Double> next = new ArrayList<>(); //next数组存储第i和j个数的所有可能组合
                Double p1 = arr.get(i), p2 = arr.get(j);
                next.addAll(Arrays.asList(p1+p2, p1-p2, p2-p1, p1*p2)); //两数的加、减、乘,所有情况都包括了
                if(Math.abs(p2) > eps)  next.add(p1/p2);  //判断p2是否小于0.001(看作0),因为除数不能为0
                if(Math.abs(p1) > eps)  next.add(p2/p1);  //同理
                
                //将第i和j个数从arr数组中移去,尝试剩下的数字能否组成24
                arr.remove(i);
                arr.remove(j);
                for (Double n: next){ //一个一个尝试
                    arr.add(n);
                    helper(arr);
                    arr.remove(arr.size()-1);//将之前加上的数字从arr中再去除
                }
                //恢复数组中第i和j个数的值
                arr.add(j, p2);
                arr.add(i, p1);
            }
        }
    }

18.(329:Hard)(非常经典题)Longest Increasing Path in a Matrix To get max length of increasing sequences:

  1. Do DFS from every cell

  2. Compare every 4 direction and skip cells that are out of boundary or smaller

  3. Get matrix max from every cell's max(返回值是取全局最大的路径长度)

  4. Use matrix[x][y] <= matrix[i][j] so we don't need a visited[m][n] array(路径上的cell值是递增的,所以不会重复访问值较小或相等的cell)((i,j)是原位置,(x,y)是走一步后更新后的位置)

  5. The key is to cache the distance because it's highly possible to revisit a cell(cache是存储当前位置出发的最长递增路径长度的二维矩阵,类似于DP问题中用于递推的缓存变量)

  • 具体的思路实现请参看代码

19.(488:Hard)(经典题)Zuma Game

  • 该题关键在于将手中的球插入以及board的消去更新。
  • 具体思路请参看花花中文讲解

<20?>.(301:Hard)(非常非常经典题)Remove Invalid Parentheses

Key Points:

  1. Generate unique answer once and only once, do not rely on Set.
  2. Do not need preprocess.
  3. Runtime 3 ms.

Explanation:

We all know how to check a string of parentheses is valid using a stack. Or even simpler use a counter. The counter will increase when it is ‘(‘ and decrease when it is ‘)’. Whenever the counter is negative, we have more ‘)’ than ‘(‘ in the prefix.

To make the prefix valid, we need to remove a ‘)’. The problem is: which one? The answer is any one in the prefix. However, if we remove any one, we will generate duplicate results, for example: s = ()), we can remove s[1] or s[2] but the result is the same (). Thus, we restrict ourself to remove the first ) in a series of concecutive )s.

After the removal, the prefix is then valid. We then call the function recursively to solve the rest of the string. However, we need to keep another information: the last removal position. If we do not have this position, we will generate duplicate by removing two ‘)’ in two steps only with a different order.

For this, we keep tracking the last removal position and only remove ‘)’ after that.

Now one may ask. What about ‘(‘? What if s = ‘(()(()’ in which we need remove ‘(‘?

The answer is: do the same from right to left.

However a cleverer idea is: reverse the string and reuse the code!

该题并没有搞懂,下次复习时再研究学习

<21>.(785:Hard)(非常非常经典题)Is Graph Bipartite?

  • 这道题在最开始做的时候,看了半天,愣是没弄懂输出数据的意思,开始以为给的是边,后来发现跟图对应不上,就懵逼了,后来是通过研究论坛上大神们的解法,才总算搞懂了题目的意思,原来输入数组中的graph[i],表示顶点i所有相邻的顶点,比如对于例子1来说,顶点0和顶点1,3相连,顶点1和顶点0,2相连,顶点2和结点1,3相连,顶点3和顶点0,2相连。这道题让我们验证给定的图是否是二分图,所谓二分图,就是可以将图中的所有顶点分成两个不相交的集合,使得同一个集合的顶点不相连。为了验证是否有这样的两个不相交的集合存在,我们采用一种很机智的染色法,大体上的思路是要将相连的两个顶点染成不同的颜色,一旦在染的过程中发现有两连的两个顶点已经被染成相同的颜色,说明不是二分图。这里我们使用两种颜色,分别用1和-1来表示,初始时每个顶点用0表示未染色,然后遍历每一个顶点,如果该顶点未被访问过,则调用递归函数,如果返回false,那么说明不是二分图,则直接返回false。如果循环退出后没有返回false,则返回true。在递归函数中,如果当前顶点已经染色,如果该顶点的颜色和将要染的颜色相同,则返回true,否则返回false。如果没被染色,则将当前顶点染色,然后再遍历与该顶点相连的所有的顶点,调用递归函数,如果返回false了,则当前递归函数的返回false,循环结束返回true。具体可以参看代码实现。

22.(886:Hard)(非常经典题)Possible Bipartition

  • 我们用一个有向图存储dislike关系,此外我们将group分为两组-1和1(方便传值,只要乘-1即可),定义如下:
group[i] = 0 means node i hasn't been visited.
group[i] = 1 means node i has been grouped to 1.
group[i] = -1 means node i has been grouped to -1.
  • 我们从头开始分组,一旦某人没有被分组,那么与该人有关系的人,以及和该人有关系的人的人都会被分组,在分组过程中一旦发现冲突,返回false,这个分组的过程是用DFS实现的。代码实现如下:
    public boolean possibleBipartition(int N, int[][] dislikes) {
        int[][] graph = new int[N][N];
        for (int[] d : dislikes) {  //建立有向图
            graph[d[0] - 1][d[1] - 1] = 1;
            graph[d[1] - 1][d[0] - 1] = 1;
        }
        int[] group = new int[N]; //每个人属于的组
        for (int i = 0; i < N; i++) {
            if (group[i] == 0 && !dfs(graph, group, i, 1)) { //当某个人没分组时,尝试将其进行分类,但实际上在dfs过程中,与该人相关的所有人都会被分类
                return false;
            }
        }
        return true;
    }
    
    private boolean dfs(int[][] graph, int[] group, int index, int g) { //index:当前标记的人 g:要将当前的人归为g组
        group[index] = g;
        for (int i = 0; i < graph.length; i++) {
            if (graph[index][i] == 1) {
                if (group[i] == g) { //有冲突,返回false
                    return false;
                }
                if (group[i] == 0 && !dfs(graph, group, i, -g)) {  //当前index的敌人也会被标记为与敌人不同的group,直到所有敌人都标记或者有冲突
                    return false;
                }
            }
        }
        return true;
    }

23.(427:Medium)(非常非常经典题)Construct Quad Tree

  • 将区域平均的划分为四块,分别对应四个孩子,然后对每个区域的所有cell都进行遍历访问,观察他们的值是否相同,若均相同则说明该块区域是leaf结点,否则将该块区域继续划分为四块,直到区域内所有cell的值均相同为止。参考代码如下:
//DFS recursive遍历建树
public Node construct(int[][] g) { return build(0, 0, g.length - 1, g.length - 1, g);}

Node build(int r1, int c1, int r2, int c2, int[][] g) {
    if (r1 > r2 || c1 > c2) return null;
    boolean isLeaf = true;
    int val = g[r1][c1];
    for (int i = r1; i <= r2; i++)
        for (int j = c1; j <= c2; j++)  //判断在指定的区域内是否grid的值全部相同,若全相同则说明该层遍历的是叶子结点
            if (g[i][j] != val) {
                isLeaf = false;
                break;
            }
    if (isLeaf) //如果是叶子结点,它的四个孩子均为空,value值与cell的值相同
        return new Node(val == 1, true, null, null, null, null);
    int rowMid = (r1 + r2) / 2, colMid = (c1 + c2) / 2;
    //如果不是叶子结点,则将当前层传入的区域再划分为4块,分别是左上、右上、左下、右下,对应四个孩子
    return new Node(false, false,
            build(r1, c1, rowMid, colMid, g),//top left
            build(r1, colMid + 1, rowMid, c2, g),//top right
            build(rowMid + 1, c1, r2, colMid, g),//bottom left
            build(rowMid + 1, colMid + 1, r2, c2, g));//bottom right
}

24.(386:Medium)(非常经典题)Lexicographical Numbers

  • 该题其实是一个典型的recursive的问题,但因为题目要求我们使用尽量少的空间和时间复杂度,所以我们需要将相同的recursive的思路转化为对应的算术运算逻辑,但通过下面recursive的代码,会对该问题有一个非常直观的认识。
// 下面的解法借用了StringBuilder,比较耗费时间,其实将相同的思路转化为算术运算效率和速度会更高
public List<Integer> lexicalOrder(int n) {
    List<Integer> res = new ArrayList<>();
    StringBuilder sb = new StringBuilder();
    for (int i = 1; i <= 9; i++){
        sb.append(i);
        helper(n, 0, sb, res);
        sb.deleteCharAt(sb.length() - 1);
    }

    return res;
}

public void helper(int n, int start, StringBuilder sb, List<Integer> res){
    int value = Integer.valueOf(sb.toString());
    if(value > n)
        return;

    res.add(value);
    for (int i = start; i <= 9; i++){
        sb.append(i);
        helper(n, 0, sb, res);
        sb.deleteCharAt(sb.length() - 1);
    }
}

25.(802:Medium)(非常经典题)Find Eventual Safe States

  • 我们使用三个值来区分node的状态:0:have not been visited1:safe2:unsafe
  • For DFS,we need to do some optimization. When we travel a path, we 先 mark the node with 2 which represents having been visited,and when we encounter a node which results in a cycle,we return false,all node in the path stays 2 and it represents unsafe.And in the following traveling,whenever we encounter a node which points to a node marked with 2,we know it will results in a cycle,so we can stop traveling.On the contrary,when a node is safe,we can mark it with 1 and whenever we encounter a safe node,we know it will not results in a cycle.
  • 我们在这里不是用一个set集合去存储unsafe的结点,而是先将node的状态设为2,查看它相邻的结点及相邻结点的相邻结点是否为unsafe(cycle),如果相邻结点有一个unsafe,则不改变当前结点unsafe的状态;如果相邻节点均为safe,则将其状态修改为safe。
    public List<Integer> eventualSafeNodes(int[][] graph) {
        List<Integer> res = new ArrayList<>();
        if(graph == null || graph.length == 0)  return res;

        int nodeCount = graph.length;
        int[] color = new int[nodeCount];

        for(int i = 0;i < nodeCount;i++){ //对每一个结点都遍历
            if(dfs(graph, i, color))    res.add(i);
        }

        return res;
    }

    // unsafe返回false,safe返回true
    public boolean dfs(int[][] graph, int start, int[] color){
        if(color[start] != 0)   return color[start] == 1;

        color[start] = 2; //先将它看成unsafe的,如果它与任何unsafe的结点都不相邻,那么再把它改成safe
        //注意,我们在这里不是用一个set集合去存储unsafe的结点,而是将其值设为2,查看它相邻的结点及相邻结点的相邻结点是否为unsafe(cycle)
        for(int newNode : graph[start]){
            if(!dfs(graph, newNode, color))    return false;
        }
        //如果相邻结点均为safe,或无相邻结点,则将其看作safe
        color[start] = 1;

        return true;
    }

26.(529:Medium)(非常非常经典题)Minesweeper

  • 扫雷的题目,虽然这道题看起来有点复杂,但实际上并不是,我们只需要按照题目中的游戏规则,将题目描述转换为对应的code即可。核心思路就是DFS。
  • 很清楚的讲解视频:视频讲解
    // 一个cell四周8个相邻cells的方向
    int[] dx = {-1, 0, 1, -1, 1, 0, 1, -1};
    int[] dy = {-1, 1, 1, 0, -1, -1, 0, 1};
    public char[][] updateBoard(char[][] board, int[] click) {
        int x = click[0], y = click[1];
        // 1. If a mine ('M') is revealed, then the game is over - change it to 'X'.
        if(board[x][y] == 'M'){
            board[x][y] = 'X';
            return board;
        }
        // 2. If an empty square ('E') with no adjacent mines is revealed, then change it to revealed
        // blank ('B') and all of its adjacent unrevealed squares should be revealed recursively.
        // 3. If an empty square ('E') with at least one adjacent mine is revealed, then change it to a
        // digit ('1' to '8') representing the number of adjacent mines.
        // 因为当一个cell是空时,需要递归遍历其相邻的cell,所以将2、3点合到一起写
        dfs(board, x, y);

        // 4. Return the board when no more squares will be revealed.
        return board;
    }

    public void dfs(char[][] board, int x, int y){
        int m = board.length, n = board[0].length;
        //我们只处理没有reveal和在board内的cell
        if(x < 0 || x >= m || y < 0 || y >= n || board[x][y] != 'E')
            return;

        int num = numOfAdjacentMines(board, x, y);

        // 1. No adjacent mine
        if(num == 0){
            board[x][y] = 'B';
            // recursively处理相邻未reveal的cell
            for(int i = 0; i < 8; i++){
                int newX = x + dx[i], newY = y + dy[i];
                dfs(board, newX, newY);
            }
        }
        else { // 2. At least one adjacent mine
            board[x][y] = (char)('0' + num);
        }
    }

    public int numOfAdjacentMines(char[][] board, int x, int y){
        int num =0, m = board.length, n = board[0].length;
        for(int i = 0; i < 8; i++){
            int newX = x + dx[i], newY = y + dy[i];
            if(newX < 0 || newX >= m || newY < 0 || newY >= n)
                continue;
            // 我们在计算相邻的雷时,已经reveal和没有reveal的都应该考虑
            if(board[newX][newY] == 'M' || board[newX][newY] == 'X')
                num++;
        }
        return num;
    }

27.(1192:Hard)(非常非常经典题)Critical Connections in a Network

  • 该题非常好,之前没有遇到过相似的问题,这里有两个参考的讲解链接:链接1链接2
  • 其他人的讲解:讲解
  • 其实最主要的是参考我下边的代码及注释即可,注释解释的非常清楚
    private int id;
    private int[] ids;
    private int[] low;
    private List<Integer>[] graph;

    public List<List<Integer>> criticalConnections(int n, List<List<Integer>> connections) {
        List<List<Integer>> bridges = new ArrayList<>();
        graph = buildGraph(n, connections); //用领接链表建树
        // ids在一些解法当中被称作discovery time,但若这样理解,与下面low的含义有些许冲突,我们不妨将其理解为id,但id是按发现的时间进行赋值的
        // 即发现时间为the time that this vertex was discovered - useful to identify when a child vertex can reach its parents ancestors
        ids = new int[n + 1];
        // to check the lowest vertex that this node can reach "through" its children,我们用low数组来追踪当前的结点通过访问其children可达的最小的id
        low = new int[n + 1];
        id = 1; //id(discovery time)从1开始,将id为0作为visited的标志
        visit(1, -1, bridges); //DFS tree根节点的parent是-1
        return bridges;
    }

    private void visit(int node, int parent, List<List<Integer>> bridges) {
        low[node] = ids[node] = id++; //我们将结点的low值初始化为当前的id值,认为其他结点访问自己的同时,肯定对自己也可达,在之后的过程中进行low的更新
        for (int child : graph[node]) {
            if (child == parent) continue; //我们不考虑child通过访问自己的parent可达结点的情况
            if (ids[child] == 0) { // not visited
                visit(child, node, bridges); //在dfs访问child结点的过程后,其low会更新为该child可以连接到的最小值
                low[node] = Math.min(low[node], low[child]);
                if (ids[node] < low[child]) { //当low[child]比ids[node]大时,说明child是访问不到node的祖先的,只能访问到自己
                    bridges.add(Arrays.asList(node, child));
                }
            } else {
                low[node] = Math.min(low[node], ids[child]); //对于已经访问过的结点,可能其id要比当前node的parent还小,并且通过该结点可达
            }
        }
    }

    private List<Integer>[] buildGraph(int n, List<List<Integer>> edges) {
        List<Integer>[] graph = new List[n + 1];
        for(int i = 0; i < graph.length; i++)
            graph[i] = new ArrayList<>();
        for (List<Integer> edge : edges) {
            int u = edge.get(0);
            int v = edge.get(1);
            graph[u].add(v);
            graph[v].add(u);
        }
        return graph;
    }

28.(756:Hard)(非常经典题)Pyramid Transition Matrix

  • 首先根据allowed triples建立所有的颜色对应情况,然后基于当前层找到所有可能的上层排列(DFS/Backtracking),并进行相似的递归处理直到达到最顶层。

29.(934:Medium)(非常非常经典题)Shortest Bridge

  • Use DFS + BFS to solve this WONDERFUL problem!
  • Step 1: use DFS to mark the first island as visited
  • Step 2: start from the first island, doing BFS level order traversal to find number of bridges (levels) until reach the second island
    public int shortestBridge(int[][] A) {
        int m = A.length, n = A[0].length;
        boolean[][] visited = new boolean[m][n];
        int[][] dirs = new int[][]{{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
        Queue<int[]> q = new LinkedList<>();
        boolean found = false;
        // 1. dfs to find an island, mark it in `visited`,因为输入的矩阵当中有两个island,我们只在dfs的过程当中标记一个island
        for (int i = 0; i < m; i++) {
            if (found) {
                break;
            }
            for (int j = 0; j < n; j++) {
                if (A[i][j] == 1) {
                    dfs(A, visited, q, i, j, dirs);
                    found = true;
                    break;
                }
            }
        }

        // 2. bfs to expand this island,然后去搜索第二个island
        int step = 0;
        while (!q.isEmpty()) {
            int size = q.size();
            while (size-- > 0) {
                int[] cur = q.poll();
                for (int[] dir : dirs) {
                    int i = cur[0] + dir[0];
                    int j = cur[1] + dir[1];
                    if (i >= 0 && j >= 0 && i < m && j < n && !visited[i][j]) {
                        if (A[i][j] == 1) {
                            return step;
                        }
                        q.offer(new int[]{i, j});
                        visited[i][j] = true;
                    }
                }
            }
            step++;
        }
        return -1;
    }

    private void dfs(int[][] A, boolean[][] visited, Queue<int[]> q, int i, int j, int[][] dirs) {
        if (i < 0 || j < 0 || i >= A.length || j >= A[0].length || visited[i][j] || A[i][j] == 0) {
            return;
        }
        visited[i][j] = true;
        q.offer(new int[]{i, j});
        for (int[] dir : dirs) {
            dfs(A, visited, q, i + dir[0], j + dir[1], dirs);
        }
    }
⚠️ **GitHub.com Fallback** ⚠️