力扣解题思路:图 纠错记录

785. 判断二分图


思路:给定一个无向图graph,当这个图为二分图时返回true。如果我们能将一个图的节点集合分割成两个独立的子集A和B,并使图中的每一条边的两个节点一个来自A集合,一个来自B集合,我们就将这个图称为二分图。graph将会以邻接表方式给出,graph[i]表示图中与节点i相连的所有节点。每个节点都是一个在0到graph.length-1之间的整数。这图中没有自环和平行边: graph[i] 中不存在i,并且graph[i]中没有重复的值。
举个例子:

Input: [[1,3], [0,2], [1,3], [0,2]]
Output: true
Explanation:
The graph looks like this:
0----1
|    |
|    |
3----2
We can divide the vertices into two groups: {0, 2} and {1, 3}.

Example 2:
Input: [[1,2,3], [0,2], [0,1,3], [0,2]]
Output: false
Explanation:
The graph looks like this:
0----1
| \  |
|  \ |
3----2
We cannot find a way to divide the set of nodes into two independent subsets.

这一题是一个比较典型的染色法解题,具体步骤是这样,首先我们找到一个没被染色的节点u,把它染上一种颜色,之后遍历所有与它相连的节点v,如果节点v已被染色并且颜色和节点u一样,那么就不是二分图。如果这个节点v没有被染色,先把它染成与节点u不同颜色的颜色,然后遍历所有与节点v相连的节点…如此递归下去。->
然后我们定义颜色,即使用一个int数组表示每个顶点的染色情况:0 表示未被染色, 1表示染成黑色,2表示染成白色。->接下来写主体函数,因为我们要考虑非连通图, 所以要遍历每一个结点:

public boolean isBipartite(int[][] graph) {
    if (graph == null || graph.length == 0) return false;
    int v = graph.length;
    int[] colors = new int[v];
    for (int i = 0; i < v; i++) {
        if (colors[i] == 0 && !dfs(graph, i, colors, 0)) return false;
    }
    return true;
}

然后来写递归函数,如果节点已被被染色的就不要继续染色了(因为这是自底向上的,被染色的点,其相连的节点肯定被染色了),这时候只需要判断colors[i] 和 lastColor是否相等即可,如果相等,证明两个邻接节点颜色一样,这就表明一定不能构成二分图,直接返回false即可,这也是递归函数的出口。
如果说未被染色,则将其染成与相邻结点不同的颜色(lastColor为0时,就染成1),然后继续遍历其相邻节点:

public boolean dfs(int[][] graph, int i, int[] colors, int lastColor) {
    if (colors[i] != 0) return colors[i] != lastColor;
    colors[i] = lastColor == 1 ? 2 : 1;
    for (int j = 0; j < graph[i].length; j++) {
        if (!dfs(graph, graph[i][j], colors, colors[i])) return false;
    }
    return true;
}

207. 课程表


思路:题目:你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1。在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1] 。给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习。
首先对于图的遍历,有DFS和BFS两种方法,所以这里给出两种遍历方法的答案,他们不只是遍历的方法是不同的,整体的思路也是不同的。

(方法1:BFS)第一种方法比较容易理解,由于prerequisites是由许多二元数组组成的数组,也就是说,数组中每个元素相当于是一条有向边,也就是第1个元素指向第0个元素,代表要先学习第1个元素才能学习第0个元素,也就是 第0 <- 第1,那么按照层次遍历的规则,首先找到一个入口。我们可以对比二叉树的层次遍历,这里相当于是多叉树,那么我们首先应该找到树根
那什么是入口呢?
我们可以先思考二叉树的根有什么特点,最明显的就是,二叉树的树根是没有父节点的,通俗的来说,就是入度为0的节点,那么答案就很明显了,我们这个图的入度为0的节点就是我们遍历的起点,也就是入口,当然入口可能并不只有一个,所以首先我们统计入度

for(int[] cp : prerequisites) indegrees[cp[0]]++;

然后,将入度为0的节点入队开始遍历,每遍历一个节点后将其后继节点的入度减一,如果该后继节点的入度为0了,则证明这是新的一层,则将其入队继续遍历,如果最后因为有入度不为0的节点的存在,退出while循环后,遍历的总结点数一定不和numCourses相等,因为存在环,则一定不合理。完整代码如下:

public boolean canFinish(int numCourses, int[][] prerequisites) {
    int[] indegrees = new int[numCourses];
    for(int[] cp : prerequisites) indegrees[cp[0]]++;//计算每个节点的入度【x,y】其中y指向x
    LinkedList<Integer> queue = new LinkedList<>();
    for(int i = 0; i < numCourses; i++){
        if(indegrees[i] == 0) queue.add(i);//将入度为0的节点入队
    }
    while(!queue.isEmpty()) {
        int pre = queue.remove();
        numCourses--;
        for(int[] req : prerequisites) {//找到节点为pre的 后驱节点
            if(req[1] != pre) continue;//节点不为pre的 后驱节点直接跳过
            if(--indegrees[req[0]] == 0) queue.add(req[0]);//如果req[0]的入度减去1之后等于0了,就继续入队
        }
    }
    return numCourses == 0;//直到所有节点最后的入度都为0证明没有环
}

(方法2:DFS)DFS的代码也比较简洁,从上面方法的分析也可以看出,这个题目就是在判断一个有向图是否存在一个环,对于DFS算法我们就不再使用入度来判断了,我们首先使用一个邻接矩阵来保存这个有向图,对每个节点遍历其邻接节点。然后我们定义递归函数的出口,因为我们的目的就是判断函数是否存在环,所以当我们发现图存在环的时候就直接返回false,那么如何在递归中判断图是否存在环呢?
首先,我们借助一个标志列表 flags,用于判断每个节点 i (课程)的状态,如果该节点未被 DFS 访问:i == 0,如果已被其他节点启动的 DFS 访问:i == -1,如果已被当前节点启动的 DFS 访问:i == 1。当 flag[i] == -1,说明当前访问节点已被其他节点启动的 DFS 访问,无需再重复搜索,直接返回 True。当 flag[i] == 1,说明在本轮 DFS 搜索中节点 i 被第 2 次访问,即课程安排图有环 ,直接返回 False。当前节点所有邻接节点已被遍历,并没有发现环,则将当前节点 flag 置为 -1 并返回 True。完整代码如下:

public boolean canFinish(int numCourses, int[][] prerequisites) {
    int[][] adjacency = new int[numCourses][numCourses];
    int[] flags = new int[numCourses];
    for(int[] cp : prerequisites)//计算邻接矩阵,相应位置置1
        adjacency[cp[1]][cp[0]] = 1;
    for(int i = 0; i < numCourses; i++){//从每个节点开始遍历其邻接节点
        if(!dfs(adjacency, flags, i)) return false;
    }
    return true;
}
public boolean dfs(int[][] adjacency, int[] flags, int i) {
    if(flags[i] == 1) return false;
    if(flags[i] == -1) return true;
    flags[i] = 1;
    for(int j = 0; j < adjacency.length; j++) {
        if(adjacency[i][j] == 1 && !dfs(adjacency, flags, j)) return false;//邻接节点被访问过且不满足要求则返回false
    }
    flags[i] = -1;
    return true;
}

记录一下,第二次写这个题,超时了,虽然想到了使用flag标记已经访问过的位置(重复访问则表示成环,成环肯定就是不能完成所有课程),但是没想到仅仅这样就会超时:

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        int[][] adjacency = new int[numCourses][numCourses];
        int[] flags = new int[numCourses];
        for(int[] cp : prerequisites)
            adjacency[cp[1]][cp[0]] = 1;
        for(int i = 0; i < numCourses; i++){
            if(!dfs(adjacency, flags, i)) return false;
        }
        return true;
    }
    public boolean dfs(int[][] adjacency, int[] flags, int i) {
        if(flags[i] == 1) return false;
        flags[i] = 1;
        for(int j = 0; j < adjacency.length; j++) {
            if(adjacency[i][j] == 1 && !dfs(adjacency, flags, j)) return false;
        }
       flags[i] = 0;
        return true;
    }

因为很多路径都被重复访问了,所以我们需要记录某些已经确定可行的路径,也就是要采取记忆化回溯。我们可以直接把flag数组作为记录该路径是否可行的数组,当我们遍历完某个节点发现其邻接节点都没有返回false,这意味着从其他点到达这个点的点也都是可行路径,所以我们标记-1为可行点,在进入dfs函数时直接判断 if(flags[i] == -1) return true; 就可以省去不少没必要的回溯啦~

210. 课程表 II


思路:现在你总共有 n 门课需要选,记为 0 到 n-1。在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1] 给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
这一题的思路基本与上一题一致,但是这一题需要按遍历的顺序将节点保存在结果集中。还是两个老方法,BFS更合适,因为该遍历的顺序就应该是层次遍历。最后需要判断一下是否成环,成环返回空数组。

public int[] findOrder(int numCourses, int[][] prerequisites) {
    int[] input = new int[numCourses];
    int[] res = new int[numCourses];
    Queue<Integer> queue = new LinkedList<>();
    // 统计节点的入度
    for(int[] edge : prerequisites) {
        input[edge[0]]++;
    }
    // 将入度为0的点入队
    for(int i = 0; i < numCourses; i++) {
        if (input[i] == 0) {
            queue.offer(i);
        }
    }
    int idx = 0;
    while(!queue.isEmpty()) {
        int temp = queue.poll();
        res[idx++] = temp;
        // 修改节点入度
        for(int[] edge : prerequisites) {
            if (edge[1] == temp) {
                input[edge[0]]--;
                if (input[edge[0]] == 0) {
                    queue.offer(edge[0]);
                }
            }
        }
    }
    // 出现环了(res中没包括所有点, idx没走完)
    if(idx != numCourses) {
        return new int[] {};
    }
    return res;
}

第二种就是DFS,显然,DFS先访问的一定是位于图形终点的节点,也就是出度为0的节点,然后往回回溯,那么我们应该从结果集后面往前填入节点,最后对每个节点在遍历的同时判断是否成环即可,其他思想与上一题基本一致。

int[] flag;//课程i=0表示尚未访问,=1表示此次dfs已经访问,=-1表示之前的dfs已访问
int[] path;//学习路线
int index;//path下一个节点的位置
public int[] findOrder(int numCourses, int[][] prerequisites) {
    flag = new int[numCourses];
    path = new int[numCourses];
    index=numCourses-1;
    //邻接表 读入数据
    List<List<Integer>> graph = new ArrayList<>(numCourses);
    for(int i=0;i<numCourses;i++){
        graph.add(new ArrayList<Integer>());
    }
    for(int[] cp:prerequisites){
        graph.get(cp[1]).add(cp[0]);
    }
    //每个节点开始dfs 返回是否有环
    for(int i=0;i<numCourses;i++){
        if(hasCircle(i,graph)){
            return new int[0];
        }
    }
    return path;
}
public boolean hasCircle(int course,List<List<Integer>> graph){
    if(flag[course] == 1) return true;
    if(flag[course] == -1) return false;
    flag[course] = 1;
    for(int after:graph.get(course)){
        if(hasCircle(after,graph)){
            return true;
        }
    }
    path[index--] = course;
    flag[course] = -1;
    return false;
}

310. 最小高度树

思路:题目如图在这里插入图片描述

既然要求最小高度,那结点肯定是在最内部的,因为题目说了是无向图,所以越靠近边缘,高度肯定就会越大。(这是自然的,越靠近1边,这边越短,另外一边就越长——无向图是双向的)所有把外围叶子节点通通一层一层剥掉,剥干净之后,剩下的1-2个节点就是我们要找的树根,树根只可能1-2个。

那可能就有人有疑问了,为什么只可能1到2个呢,其实这一题也可以这样理解,我们要求的实际上是这个无向图最长的一条链的中点,所以当然只可能是1~2个呀o( ̄▽ ̄)o

那么如何一层层剥掉呢?那就要用到入度啦~可以利用上一题课程表的思想:

首先构建邻接表并统计入度:

    // 这是定义每个边的数组元素的第0个值和第1个值
    int element0,element1;
    // 这是所有节点的入度列表(被指向的次数)
    int[] inDegree = new int[n];
    List<List<Integer>>adjacencyList = new ArrayList<>();
    // 初始化邻接列表
    for (int i = 0; i < n; i++) {
        adjacencyList.add(new ArrayList<>());
    }
    for (int[] cur : edges) {
        element0 = cur[0];
        element1 = cur[1];
        inDegree[element0]++;
        inDegree[element1]++;
        // 邻接列表要初始化两边(不同方向),因为是无向图,所以方向是双向的
        adjacencyList.get(element0).add(element1);
        adjacencyList.get(element1).add(element0);
    }

找到叶子节点,从叶子节点开始遍历,即入度为1的入队列作为BFS起点:

// 首先把入度为1的元素(叶子节点)放入
    Queue<Integer> queue = new LinkedList<>();
    for (int i = 0; i < n; i++) {
        if (inDegree[i] == 1) {
            queue.add(i);
        }
    }

维护每一层的节点,遍历到最后一层即为一个或者两个中点节点:

// 当前节点的临近列表
    List<Integer> curNodeAdjacentList;
    int size, curNode;
    // 像剥洋葱一样层层把叶子节点去掉
    // 去掉一圈叶子节点,剩下的又变成了叶子节点
    // 叶子节点就是入度为1的节点
    while (!queue.isEmpty()) {
        // 需要记住当前queue的剩余大小
        // 每次都整整清除一圈
        size = queue.size();
        minHeightTreeNode.clear();
        for (int j = 0; j < size; j++) {
            curNode = queue.poll();
            minHeightTreeNode.add(curNode);
            curNodeAdjacentList = adjacencyList.get(curNode);
            for (int node : curNodeAdjacentList) {
                // 把当前节点对应的邻接列表的入度都减1
                inDegree[node]--;
                // 如果被减1的邻接列表的节点就剩下1了,那就证明它就是叶子节点了
                if (inDegree[node] == 1) {
                    queue.add(node);
                }
            }
        }
    }

这里把curNode的邻接节点的入度都减一是否正确呢,当然是对的,虽然他的邻接节点中是存在已经遍历过的叶子节点的,但是之前遍历过的叶子节点入度1-1=0后是不可能再次入队的,所以这一点不影响结果。

完整代码如下:

public List<Integer> findMinHeightTrees(int n, int[][] edges) {
    List<Integer> minHeightTreeNode = new ArrayList<>();
    // 如果n=1,只给定了1,那就返回最小树的根节点0
    if (n == 1) {
        minHeightTreeNode.add(0);
        return minHeightTreeNode;
    }
    // 这是定义每个边的数组元素的第0个值和第1个值
    int element0,element1;
    int[] inDegree = new int[n];
    List<List<Integer>>adjacencyList = new ArrayList<>();
    // 初始化邻接列表
    for (int i = 0; i < n; i++) {
        adjacencyList.add(new ArrayList<>());
    }
    for (int[] cur : edges) {
        element0 = cur[0];
        element1 = cur[1];
        inDegree[element0]++;
        inDegree[element1]++;
        // 邻接列表要初始化两边(不同方向),因为是无向图,所以方向是双向的
        adjacencyList.get(element0).add(element1);
        adjacencyList.get(element1).add(element0);
    }
    Queue<Integer> queue = new LinkedList<>();
    for (int i = 0; i < n; i++) {
        if (inDegree[i] == 1) {
            queue.add(i);
        }
    }
    List<Integer> curNodeAdjacentList;
    int size, curNode;
    while (!queue.isEmpty()) {
        size = queue.size();
        minHeightTreeNode.clear();
        for (int j = 0; j < size; j++) {
            curNode = queue.poll();
            minHeightTreeNode.add(curNode);
            curNodeAdjacentList = adjacencyList.get(curNode);
            for (int node : curNodeAdjacentList) {
                // 把当前节点对应的邻接列表的入度都减1
                inDegree[node]--;
                // 如果被减1的邻接列表的节点就剩下1了,那就证明它就是叶子节点了
                if (inDegree[node] == 1) {
                    queue.add(node);
                }
            }
        }
    }
    return minHeightTreeNode;
}

332. 重新安排行程

思路:
在这里插入图片描述
首先,我们可以确定这一题使用DFS,因为给定了起点,路径都是一样长不存在最短路径这一说,所以DFS解决会更清晰。但是这里会有一个问题,如果存在多种有效行程应该返回的是最小行程而不是任意行程,这里涉及到排序,这就限制了我们对每一个节点的邻接节点的选择,所以我们可以把每个节点的邻接节点放入优先队列中,这样每次取出顶部就是最小的啦~~(以往构建图的邻接关系的时候用的只是普通的ArrayList,这里用到优先队列只是为了每次只取出最小的邻接节点),先构建邻接关系:

    for (List<String> ticket : tickets) {
        String src = ticket.get(0);
        String dst = ticket.get(1);
        if (!map.containsKey(src)) {
            PriorityQueue<String> pq = new PriorityQueue<>();
            map.put(src, pq);
        }
        map.get(src).add(dst);
    }

然后就是DFS遍历:
我开始是这样写的:

private void dfs(String src) {
    PriorityQueue<String> pq = map.get(src);
    //Queue<String> pq = map.get(src);
    if(pq != null && !pq.isEmpty()) dfs(pq.poll());
    resList.addFirst(src);
}

后来发现测试用例[[“JFK”,“KUL”],[“JFK”,“NRT”],[“NRT”,“JFK”]],我的输出为:[“JFK”,“KUL”],而正确为:[“JFK”,“NRT”,“JFK”,“KUL”],因此我这种贪心策略是错的!!因此我们不可以每次只选取邻接节点中最小的一个遍历,而是要遍历所有的可行的路径!!只有当队列中都为空时,才可以将该节点加入resList中!!

private void dfs(String src) {
    PriorityQueue<String> pq = map.get(src);
    //Queue<String> pq = map.get(src);
    while (pq != null && !pq.isEmpty())
        dfs(pq.poll());
    resList.addFirst(src);
}

这是个深度优先搜索的过程,从"JFK"为起点开始递归查找,但这个过程都没有把经过的机场保存到列表中。比如: 例子中的[[“MUC”, “LHR”], [“JFK”, “MUC”], [“SFO”, “SJC”], [“LHR”, “SFO”]],调用过程是这样的:dfs(“JFK”)->dfs(“MUC”)->dfs(“LHR”)-> dfs(“SFO”)->dfs(“SJC”),所以递归到终点才开始保存机场到列表中,“JFK”<-“MUC”<-“LHR”<-“SFO”<-“SJC”,所以需要从终点开始往头部插入。

399. 除法求值

思路:在这里插入图片描述
这个题目我看得以为是解方程,正准备赋值,结果看到标签竟然是图!!!果然这一题不简单,也就是,所给条件,实际上能构成一个有向加权图,举个例子,比如a->b = 2.0 b->c = 3.0,a,b,c看出节点,相处所的值为权值.那么a/c = ?就是相当于,a->c <==> a->b->c = 2.0*3.0= 6,所以我们要把已知条件建图!!

首先构建图,map中存储(出发点,map(到达点,值)),但是为了充分利用题目的信息比如a->b = 2.0,我们可以推出比如b->a = 1/2.0:

public Map<String, Map<String,Double>> doGraph(List<List<String>> equations, double[] values){
    Map<String, Map<String,Double>> graph = new HashMap<>();//有向图
    for(int i = 0 ; i < equations.size(); i++){
        String s = equations.get(i).get(0);
        String t = equations.get(i).get(1);
        double val = values[i];
        Map<String, Double> edge1 = graph.getOrDefault(s, new HashMap<>());
        edge1.put(t, val);
        graph.put(s, edge1);//保存映射
        Map<String, Double> edge2 = graph.getOrDefault(t, new HashMap<>());
        edge2.put(s, 1/val);
        graph.put(t, edge2);//保存反方向的
    } 
    return graph;
}

接下来就是遍历我们所需要求的表达式,也就是一个寻找路径的问题,我们使用DFS来遍历,这里要注意的是,有可能有的表达式注定是无解的,比如图中根本没有所求节点,这时直接返回-1作为无解的标识。此外,我们还需要用visited来记录是否访问过该路径避免死循环:

public double dfs(Map<String, Map<String,Double>> graph, Set<String> visited, String start, String end, double ans){
    if(!graph.containsKey(start) || !graph.containsKey(end)) return -1;//出发点和到达点都不在集合中,证明有问题不用寻找
    Map<String, Double> edges = graph.get(start);//获取到达点集合(相当于邻接节点)
    for(String key : edges.keySet()){
        if(!visited.contains(key)){//如果该节点没有到达过
            visited.add(key);  //则加入
            double v = edges.get(key);//获取该路径值
            if(key.equals(end)) return ans*v;  //如果这个节点的终点等于key,此时表明得出结果,直接返回结果即可
            double d = dfs(graph, visited, key, end, ans*v);//不是终点则继续遍历下一邻接节点
            if(d != -1){
                return d;//存在时返回结果
            }else{
                visited.remove(visited.size()-1);
            }
        }
    }
    return -1;
}

完整代码如下:

    public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
        Map<String, Map<String,Double>> graph = doGraph(equations, values);//集《出发点,集《到达点,值》》
        double[] res = new double[queries.size()];
        int index = 0;
        for(List<String> q : queries){//遍历所有的问题得出解答
            res[index++] = dfs(graph, new HashSet<>(), q.get(0), q.get(1), 1);
        }
        return res;
    }
    public double dfs(Map<String, Map<String,Double>> graph, Set<String> visited, String start, String end, double ans){
        if(!graph.containsKey(start) || !graph.containsKey(end)) return -1;//出发点和到达点都不在集合中,证明有问题不用寻找
        Map<String, Double> edges = graph.get(start);//获取到达点集合(相当于邻接节点)
        for(String key : edges.keySet()){
            if(!visited.contains(key)){//如果该节点没有到达过
                visited.add(key);  //则加入
                double v = edges.get(key);//获取该路径值
                if(key.equals(end)) return ans*v;  //如果这个节点的终点等于key,此时表明得出结果,直接返回结果即可
                double d = dfs(graph, visited, key, end, ans*v);//不是终点则继续遍历下一邻接节点
                if(d != -1){
                    return d;//存在时返回结果
                }else{
                    visited.remove(visited.size()-1);
                }
            }
        }
        return -1;
    }
    public Map<String, Map<String,Double>> doGraph(List<List<String>> equations, double[] values){
        Map<String, Map<String,Double>> graph = new HashMap<>();//有向图
        for(int i = 0 ; i < equations.size(); i++){
            String s = equations.get(i).get(0);
            String t = equations.get(i).get(1);
            double val = values[i];
            Map<String, Double> edge1 = graph.getOrDefault(s, new HashMap<>());
            edge1.put(t, val);
            graph.put(s, edge1);//保存映射
            Map<String, Double> edge2 = graph.getOrDefault(t, new HashMap<>());
            edge2.put(s, 1/val);
            graph.put(t, edge2);//保存反方向的
        } 
        return graph;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值