图论总结与编程练习

目录

一、图论基础分析

(一)基本介绍

(二)JDK中的应用分析

(三)其他框架中的使用介绍

二、相关编程练习题

(一)单词接龙(Word Ladder)

(二)克隆图(Clone Graph)

(三)岛屿数量(Number of Islands)

(四)网络延迟时间(Network Delay Time)

(五)单源最短路径(Dijkstra 算法)

(六)负权最短路径问题(Negative Weight Shortest Path Problem)

(七)具有最小生成树的连通图的最小代价(Prim 算法)

(八)找到最终的安全状态(Find Eventual Safe States)

(九)网络流问题的最大流(Maximum Flow)

(十)图中的可变流量(Graph Valid Tree)

(十一)图中的割边(Minimum Cut)

(十二)隐藏的好友(Friend Circles)

(十三)欧拉路径(Eulerian Path)

(十四)哈密顿路径(Hamiltonian Path)

(十五)判断是否为二分图(Is Graph Bipartite?)

(十六)用颜色填充区域(Coloring A Border)


干货分享,感谢您的阅读!

一、图论基础分析

(一)基本介绍

计算机图论是计算机科学中的一个重要分支,研究的是图的理论和算法。图是由节点(顶点)和连接节点的边构成的数据结构,广泛应用于各种领域,如网络分析、社交网络、路由算法、图像处理等。

图论研究的主要内容包括图的性质、图的表示方法和图的算法。下面我将介绍一些基本概念和常用算法。

图的性质:

  1. 顶点(节点):图中的基本单元,用于表示实体或对象。
  2. 边:连接节点的线段,表示节点之间的关系。
  3. 有向图和无向图:有向图中的边有方向,表示节点之间的单向关系;无向图中的边没有方向,表示节点之间的双向关系。
  4. 加权图:图中的边带有权重或成本,用于表示节点之间的关联程度或路径长度。

图的表示方法:

  1. 邻接矩阵:使用二维数组表示图的连接关系,其中矩阵的行和列表示节点,矩阵的元素表示边的存在与否或权重。
  2. 邻接表:使用链表或数组表示图的连接关系,每个节点都有一个相邻节点列表,用于存储与之相连的节点和边的信息。

常用算法:

  1. 深度优先搜索(DFS):从起始节点开始,尽可能深地探索图的分支,直到无法继续为止,然后回溯到上一个节点继续探索。
  2. 广度优先搜索(BFS):从起始节点开始,逐层地探索图的分支,先访问离起始节点最近的节点,然后依次访问离起始节点更远的节点。
  3. 最短路径算法:用于寻找两个节点之间的最短路径,常用的算法包括迪杰斯特拉算法(Dijkstra's algorithm)和贝尔曼-福特算法(Bellman-Ford algorithm)。
  4. 最小生成树算法:用于找到连接图中所有节点的一棵包含最小权重边的树,常用的算法包括普里姆算法(Prim's algorithm)和克鲁斯卡尔算法(Kruskal's algorithm)。

除了上述算法之外,计算机图论还涉及到诸如拓扑排序、连通性判断、网络流、图着色等其他重要问题和算法。计算机图论在解决实际问题中具有广泛的应用,可以帮助人们理解和分析复杂的关系

(二)JDK中的应用分析

在 JDK(Java Development Kit)源码中,也会涉及到图论相关的应用。以下是一些示例:

  1. 图的表示:在 JDK 中,图的表示通常用邻接表或邻接矩阵来实现。例如,在图论相关的算法中,如拓扑排序(Topological Sort)和最短路径算法,会使用到图的表示方式。
  2. 图的遍历:深度优先搜索(DFS)和广度优先搜索(BFS)是图的遍历算法,它们在 JDK 中的许多地方都有应用。例如,在集合类的遍历中,使用迭代器或递归实现的算法就可以看作是一种深度优先搜索或广度优先搜索。
  3. 最短路径算法:JDK 中的 java.util.concurrent 包中的类使用了最短路径算法来实现一些并发工具,如 Phaser 类中的屏障同步机制。最短路径算法有助于确定线程之间的相对顺序,以及在并发环境中的任务调度和协作。
  4. 最小生成树算法:JDK 中的某些类库也可能使用最小生成树算法。例如,在网络通信中,可能使用最小生成树算法来构建最优的网络拓扑结构,以实现高效的数据传输。

需要注意的是,这些图论算法的具体实现可能会因 JDK 版本和具体的模块而有所不同。在 JDK 源码中,这些算法通常被用于解决与网络、并发和优化相关的问题。

具体举例一个源码的最短路径算法分析案例

在 JDK 中,java.util.concurrent.Phaser 类是一个用于并发控制的工具类,它实现了屏障同步机制。屏障同步机制用于确保多个线程在特定点上等待,并在满足条件后同时继续执行。

Phaser 类使用了最短路径算法来确定线程之间的相对顺序,并在并发环境中进行任务调度和协作。这个类中的内部实现包括一个树状结构,每个节点表示一个阶段(phase),并且节点之间以最短路径连接。

下面是对 Phaser 类的源码分析示例:

  1. 首先,通过查看 Phaser 类的源码,可以了解到它的内部数据结构包括一个阶段(phase)和一个注册线程的集合。
  2. 在 Phaser 类中,最短路径算法被用于确定线程之间的相对顺序。具体而言,Phaser 内部维护了一个树状结构,树中的每个节点表示一个阶段。线程在到达某个阶段时,会根据最短路径选择下一个要进入的阶段。
  3. 最短路径算法的具体实现可能涉及到图论中的迪杰斯特拉算法或贝尔曼-福特算法等。通过分析 Phaser 类的源码,可以深入了解最短路径算法在并发环境中的应用。

总的来说,java.util.concurrent.Phaser 类使用了最短路径算法来实现屏障同步机制,确保多个线程按照特定的顺序执行任务。通过分析该类的源码,可以更好地理解最短路径算法在并发编程中的应用,并学习如何利用图论的概念解决复杂的并发问题。

(三)其他框架中的使用介绍

图论在框架中的使用非常广泛,它可以帮助框架设计者解决各种复杂的问题。以下是图论在框架中的一些常见应用:

  1. 依赖管理:许多框架需要管理各种组件、模块或插件之间的依赖关系。这些依赖关系可以被表示为一个图,其中节点表示组件,边表示依赖关系。通过图论算法,框架可以进行依赖分析、循环依赖检测、构建依赖关系图等。
  2. 路由和导航:在Web框架和路由框架中,图论被广泛用于实现路由和导航功能。URL路径和请求参数可以被表示为图的节点和边,通过图论算法可以确定最短路径、处理路由冲突、支持动态路由等。
  3. 状态机:状态机是许多框架和协议中常见的概念,用于描述系统或协议的状态转换。状态机可以被看作是一个有向图,其中节点表示不同的状态,边表示状态之间的转换。通过图论算法,可以实现状态机的分析、验证、优化等。
  4. 数据流和数据处理:在数据流处理框架中,图论可以用于表示数据流图,其中节点表示数据处理的节点,边表示数据的流向。通过图论算法,可以进行数据流分析、优化、并行计算等。
  5. 分布式系统:在分布式系统框架中,图论被广泛用于拓扑结构的建模和分析。节点表示计算节点或服务器,边表示节点之间的连接。通过图论算法,可以进行分布式系统的拓扑分析、负载均衡、容错处理等。

这些只是图论在框架中的一些常见应用示例。实际上,图论在框架设计中的应用非常灵活多样,可以根据具体的问题和需求进行创新和扩展。框架设计者可以利用图论的概念、算法和工具,解决复杂的依赖关系、路由、状态转换等问题,从而构建更强大和可扩展的框架。

二、相关编程练习题

基本的图论算法题分类大致为

深度优先搜索(DFS)和广度优先搜索(BFS):

  • 岛屿数量(Number of Islands)
  • 单词接龙(Word Ladder)
  • 克隆图(Clone Graph)

最短路径算法:

  • 单源最短路径(Dijkstra 算法)
  • 负权最短路径问题(Bellman-Ford algorithm贝尔曼-福特算法)
  • 网络延迟时间(Network Delay Time)

最小生成树算法:

  • 具有最小生成树的连通图的最小代价(Prim 算法)

拓扑排序:

  • 强连通分量(Strongly Connected Components):
  • 找到最终的安全状态(Find Eventual Safe States)

最大流算法:

  • 网络流问题的最大流(Maximum Flow)
  • 图中的可变流量(Graph Valid Tree)

最小割算法:

  • 图中的割边(Minimum Cut)
  • 隐藏的好友(Friend Circles)
  • 欧拉路径和哈密顿路径:
  • 欧拉路径(Eulerian Path)
  • 哈密顿路径(Hamiltonian Path)

最短路径树:

  • 从根到叶的二进制数之和(Sum Root to Leaf Numbers)
  • 从起点到终点的最小路径数(Unique Paths)

二部图:

  • 判断是否为二分图(Is Graph Bipartite?)
  • 用颜色填充区域(Coloring A Border)

这些题目涵盖了图论中的各个重要概念和算法,包括最大流、最小割、欧拉路径、哈密顿路径、最短路径树、二部图等。通过解决这些问题,你将有机会熟悉和掌握不同类型的图论算法,提升你的算法解决能力和编程技巧。

1、单词接龙(Word Ladder)

题目描述:给定两个单词 beginWord 和 endWord,以及一个字典列表 wordList,找到从 beginWord 到 endWord 的最短转换序列的长度。每次转换只能改变一个字母,并且转换后的单词必须在字典列表 wordList 中。

注意:

  • 每个给定的单词长度相同。
  • 所有单词只包含小写字母。
  • 假设不存在重复的单词。
  • beginWord 和 endWord 不为空,且它们不同。

例如,给定 beginWord = "hit",endWord = "cog",wordList = ["hot","dot","dog","lot","log","cog"],最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog",转换序列的长度为 5。

你需要返回从 beginWord 到 endWord 的最短转换序列的长度。如果不存在这样的转换序列,则返回 0。

解法分析

针对单词接龙(Word Ladder)问题,可以使用广度优先搜索(BFS)算法来找到最短转换序列的长度。以下是该算法的最优解法分析:

  1. 构建图:将字典列表中的单词作为图中的节点,如果两个单词只相差一个字母,则在它们之间添加一条边。可以使用哈希表来存储每个单词对应的邻居列表,以便快速查找。
  2. BFS搜索:从起始单词开始,使用队列进行广度优先搜索。首先将起始单词加入队列,并初始化访问标记和距离为1。然后,从队列中取出一个单词,并遍历它的邻居列表。
  3. 判断终止条件:如果当前单词等于目标单词,说明已经找到了最短转换序列,返回当前距离即可。
  4. 转换单词:对于当前单词的每个字母,尝试用所有可能的小写字母进行替换,生成新的单词。如果新的单词存在于字典列表中,并且之前没有访问过,将其加入队列,并更新其距离和访问标记。
  5. 继续搜索:重复步骤3和步骤4,直到队列为空。如果队列为空时仍未找到目标单词,则说明无法从起始单词转换到目标单词,返回0。

该解法的时间复杂度取决于字典列表中的单词数量和每个单词的长度。在构建图时,需要遍历字典列表,并对每个单词的每个字母进行替换,时间复杂度为 O(M * N),其中 M 是字典列表的长度,N 是单词的长度。在广度优先搜索中,每个单词最多会入队一次,因此总体时间复杂度为 O(M * N)。

由于单词接龙问题可以看作是在图中寻找最短路径的问题,因此广度优先搜索算法是该问题的最优解法之一。使用广度优先搜索可以有效地找到起始单词到目标单词的最短转换序列的长度,时间复杂度相对较低。

代码展示

package org.zyf.javabasic.letcode.graph;

import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;

/**
 * @author yanfengzhang
 * @description 给定两个单词 beginWord 和 endWord,以及一个字典列表 wordList,找到从 beginWord 到 endWord 的最短转换序列的长度。
 * 每次转换只能改变一个字母,并且转换后的单词必须在字典列表 wordList 中。
 * 注意:
 * * 每个给定的单词长度相同。
 * * 所有单词只包含小写字母。
 * * 假设不存在重复的单词。
 * * beginWord 和 endWord 不为空,且它们不同。
 * 例如,给定 beginWord = "hit",endWord = "cog",wordList = ["hot","dot","dog","lot","log","cog"],
 * 最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog",转换序列的长度为 5。
 * 你需要返回从 beginWord 到 endWord 的最短转换序列的长度。如果不存在这样的转换序列,则返回 0。
 * @date 2023/5/2  22:27
 */
public class WordLadder {

    /**
     * 针对单词接龙(Word Ladder)问题,可以使用广度优先搜索(BFS)算法来找到最短转换序列的长度。以下是该算法的最优解法分析:
     * 1. 构建图:将字典列表中的单词作为图中的节点,如果两个单词只相差一个字母,则在它们之间添加一条边。可以使用哈希表来存储每个单词对应的邻居列表,以便快速查找。
     * 2. BFS搜索:从起始单词开始,使用队列进行广度优先搜索。首先将起始单词加入队列,并初始化访问标记和距离为1。然后,从队列中取出一个单词,并遍历它的邻居列表。
     * 3. 判断终止条件:如果当前单词等于目标单词,说明已经找到了最短转换序列,返回当前距离即可。
     * 4. 转换单词:对于当前单词的每个字母,尝试用所有可能的小写字母进行替换,生成新的单词。如果新的单词存在于字典列表中,并且之前没有访问过,将其加入队列,并更新其距离和访问标记。
     * 5. 继续搜索:重复步骤3和步骤4,直到队列为空。如果队列为空时仍未找到目标单词,则说明无法从起始单词转换到目标单词,返回0。
     * 该解法的时间复杂度取决于字典列表中的单词数量和每个单词的长度。在构建图时,需要遍历字典列表,并对每个单词的每个字母进行替换,
     * 时间复杂度为 O(M * N),其中 M 是字典列表的长度,N 是单词的长度。在广度优先搜索中,每个单词最多会入队一次,因此总体时间复杂度为 O(M * N)。
     * 由于单词接龙问题可以看作是在图中寻找最短路径的问题,因此广度优先搜索算法是该问题的最优解法之一。
     * 使用广度优先搜索可以有效地找到起始单词到目标单词的最短转换序列的长度,时间复杂度相对较低。
     */
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        // 将字典列表转换为集合,方便快速查找
        Set<String> wordSet = new HashSet<>(wordList);
        if (!wordSet.contains(endWord)) {
            // 如果目标单词不在字典列表中,无法进行转换,返回0
            return 0;
        }

        // 创建队列用于广度优先搜索
        Queue<String> queue = new LinkedList<>();
        // 将起始单词加入队列
        queue.offer(beginWord);
        // 距离初始化为1
        int level = 1;

        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                String currentWord = queue.poll();

                // 尝试转换单词的每个字母
                char[] wordChars = currentWord.toCharArray();
                for (int j = 0; j < wordChars.length; j++) {
                    char originalChar = wordChars[j];

                    // 替换字母,生成新的单词
                    for (char c = 'a'; c <= 'z'; c++) {
                        if (c == originalChar) {
                            continue; // 跳过相同的字母
                        }

                        wordChars[j] = c;
                        String newWord = String.valueOf(wordChars);

                        if (newWord.equals(endWord)) {
                            // 找到目标单词,返回当前距离加1
                            return level + 1;
                        }

                        if (wordSet.contains(newWord)) {
                            // 将新的单词加入队列
                            queue.offer(newWord);
                            // 避免重复访问
                            wordSet.remove(newWord);
                        }
                    }

                    // 恢复原始字母
                    wordChars[j] = originalChar;
                }
            }
            // 增加距离
            level++;
        }
        // 无法进行转换,返回0
        return 0;
    }

    public static void main(String[] args) {
        WordLadder wordLadder = new WordLadder();

        // 示例测试用例
        String beginWord1 = "hit";
        String endWord1 = "cog";
        List<String> wordList1 = Arrays.asList("hot", "dot", "dog", "lot", "log", "cog");
        int ladderLength1 = wordLadder.ladderLength(beginWord1, endWord1, wordList1);
        System.out.println("Test Case 1:");
        System.out.println("Begin Word: " + beginWord1);
        System.out.println("End Word: " + endWord1);
        System.out.println("Word List: " + wordList1);
        System.out.println("Minimum Length: " + ladderLength1);
        System.out.println();

        // 自定义测试用例
        String beginWord2 = "hot";
        String endWord2 = "dog";
        List<String> wordList2 = Arrays.asList("hot", "dog", "dot");
        int ladderLength2 = wordLadder.ladderLength(beginWord2, endWord2, wordList2);
        System.out.println("Test Case 2:");
        System.out.println("Begin Word: " + beginWord2);
        System.out.println("End Word: " + endWord2);
        System.out.println("Word List: " + wordList2);
        System.out.println("Minimum Length: " + ladderLength2);
        System.out.println();
    }
}

2、克隆图(Clone Graph)

题目描述:给定一个无向连通图中的一个节点的引用 Node,返回该图的深拷贝(克隆)。

图中的每个节点都包含一个值(int)和一个包含其邻居的列表(list of Node)。

深拷贝的图应该以相同的方式构建,其具有与原始图相同的节点数目,并且可以通过 Node 引用找到对应的新节点。新图中的每个节点都应该从原始图中的节点复制其值和邻居信息。

注意:

  • 节点值的范围是整数(int)。
  • 无向图不包含自环(self-loop),即节点不会直接连接到自身。
  • 无向图中两个节点之间最多存在一条边,即无向图是简单图(simple graph)。
  • 在深拷贝的图中,任何两个节点之间都不存在直接的边连接。

例如,给定如下无向图的一个节点引用 1:

1 -- 2

|    |

4 -- 3

返回结果应为克隆的无向图的一个节点引用 1',其结构如下:

1'-- 2'

|    |

4'-- 3'

每个节点都具有相同的节点值和邻居列表。

这道题可以通过深度优先搜索(DFS)或广度优先搜索(BFS)算法来解决。遍历图中的每个节点,并创建对应的新节点,同时复制其值和邻居信息。通过递归或使用队列来实现搜索,并使用哈希表来存储已访问的节点,避免重复创建新节点。最终,返回克隆图的起始节点引用。

解法分析

针对克隆图(Clone Graph)问题,可以使用深度优先搜索(DFS)或广度优先搜索(BFS)算法来解决。以下是该问题的最优解法分析:

  1. 创建哈希表:创建一个哈希表 visited,用于存储已访问的节点。键是原始图中的节点,值是对应的新节点。
  2. DFS或BFS遍历:以给定的节点作为起始节点开始遍历图。对于DFS解法,可以使用递归函数进行深度优先搜索;对于BFS解法,可以使用队列进行广度优先搜索。无论使用哪种方法,都需要同时处理节点的值和邻居信息。
  3. 复制节点:对于当前节点,创建一个新节点,并将其存储在哈希表中。将当前节点的值复制到新节点中,并初始化新节点的邻居列表为空。
  4. 遍历邻居:遍历当前节点的邻居节点列表。如果邻居节点已经在哈希表中存在,说明已经创建过对应的新节点,直接将其添加到当前节点的邻居列表中。否则,递归调用DFS或将邻居节点添加到队列中,进行下一轮遍历。
  5. 返回结果:DFS解法在递归过程中会自动回溯,所以最后只需返回起始节点的克隆节点即可。BFS解法则可以在遍历完成后返回任意一个克隆节点。

该解法的时间复杂度取决于图中节点的数量和边的数量。在遍历过程中,每个节点只会被访问一次,并且每个边也只会被访问一次,因此时间复杂度为 O(N + E),其中 N 是节点的数量,E 是边的数量。

由于克隆图问题涉及图的遍历和节点的复制,深度优先搜索和广度优先搜索是解决该问题的最优解法之一。使用这两种算法可以有效地创建新图,并复制原始图中节点的值和邻居信息。通过使用哈希表来存储已访问的节点,可以避免重复创建新节点,提高算法的效率。

代码展示

package org.zyf.javabasic.letcode.graph;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

/**
 * @author yanfengzhang
 * @description 给定一个无向连通图中的一个节点的引用 Node,返回该图的深拷贝(克隆)。
 * 图中的每个节点都包含一个值(int)和一个包含其邻居的列表(list of Node)。
 * 深拷贝的图应该以相同的方式构建,其具有与原始图相同的节点数目,并且可以通过 Node 引用找到对应的新节点。
 * 新图中的每个节点都应该从原始图中的节点复制其值和邻居信息。
 * 注意:
 * * 节点值的范围是整数(int)。
 * * 无向图不包含自环(self-loop),即节点不会直接连接到自身。
 * * 无向图中两个节点之间最多存在一条边,即无向图是简单图(simple graph)。
 * * 在深拷贝的图中,任何两个节点之间都不存在直接的边连接。
 * 例如,给定如下无向图的一个节点引用 1:
 * 1 -- 2
 * |    |
 * 4 -- 3
 * 返回结果应为克隆的无向图的一个节点引用 1',其结构如下:
 * 1'-- 2'
 * |    |
 * 4'-- 3'
 * 每个节点都具有相同的节点值和邻居列表。
 * @date 2022/6/2  23:36
 */
public class CloneGraph {
    private Map<Node, Node> visited;

    public Node cloneGraph(Node node) {
        visited = new HashMap<>();
        return cloneNode(node);
    }

    private Node cloneNode(Node node) {
        if (node == null) {
            return null;
        }

        if (visited.containsKey(node)) {
            return visited.get(node);
        }

        Node clone = new Node(node.val);
        visited.put(node, clone);

        for (Node neighbor : node.neighbors) {
            clone.neighbors.add(cloneNode(neighbor));
        }

        return clone;
    }

    static class Node {
        public int val;
        public List<Node> neighbors;

        public Node(int val) {
            this.val = val;
            this.neighbors = new ArrayList<>();
        }
    }

    public static void main(String[] args) {
        // 创建原始图
        Node node1 = new Node(1);
        Node node2 = new Node(2);
        Node node3 = new Node(3);
        Node node4 = new Node(4);

        node1.neighbors.add(node2);
        node1.neighbors.add(node4);

        node2.neighbors.add(node1);
        node2.neighbors.add(node3);

        node3.neighbors.add(node2);
        node3.neighbors.add(node4);

        node4.neighbors.add(node1);
        node4.neighbors.add(node3);

        // 克隆图
        CloneGraph cloneGraph = new CloneGraph();
        Node clonedNode = cloneGraph.cloneGraph(node1);

        // 打印原始图和克隆图的节点值和邻居列表
        System.out.println("Original Graph:");
        printGraph(node1);

        System.out.println("Cloned Graph:");
        printGraph(clonedNode);
    }

    private static void printGraph(Node node) {
        if (node == null) {
            return;
        }

        Set<Node> visited = new HashSet<>();
        Queue<Node> queue = new LinkedList<>();
        queue.offer(node);
        visited.add(node);

        while (!queue.isEmpty()) {
            Node currNode = queue.poll();
            System.out.print("Node " + currNode.val + ": [");

            for (int i = 0; i < currNode.neighbors.size(); i++) {
                Node neighbor = currNode.neighbors.get(i);
                System.out.print(neighbor.val);

                if (!visited.contains(neighbor)) {
                    queue.offer(neighbor);
                    visited.add(neighbor);
                }

                if (i != currNode.neighbors.size() - 1) {
                    System.out.print(", ");
                }
            }

            System.out.println("]");
        }
    }
}

3、岛屿数量(Number of Islands)

题目描述:给定一个由 '1'(陆地)和 '0'(水)组成的二维网格的地图,计算岛屿的数量。一个岛被水包围,并且通过水平或垂直方向上相邻的陆地连接形成。你可以假设网格的四个边均被水包围。

示例1:

输入:

11110

11010

11000

00000

输出: 1

示例2:

输入:

11000

11000

00100

00011

输出: 3

岛屿被水平或垂直方向上相邻的陆地连接形成,因此可以使用深度优先搜索(DFS)或广度优先搜索(BFS)算法来解决该问题。遍历网格中的每个格子,如果遇到陆地(值为 '1'),则进行深度优先搜索或广度优先搜索,将相邻的陆地全部标记为已访问的水域(值为 '0')。通过重复这个过程,可以计算出岛屿的数量。

注意:在进行深度优先搜索或广度优先搜索时,需要注意边界条件的处理,以及避免重复访问已经访问过的格子。

解法分析

岛屿数量(Number of Islands)问题的最优解法是使用深度优先搜索(DFS)算法。

算法步骤如下:

  1. 定义一个变量 count,用于记录岛屿的数量。
  2. 遍历二维网格的每个格子。
    • 如果当前格子是陆地(值为 '1'),则进行深度优先搜索。
    • 在深度优先搜索过程中,将当前格子标记为已访问,即将值从 '1' 修改为 '0'。
    • 搜索当前格子的上、下、左、右四个方向的相邻格子,如果相邻格子也是陆地,则继续进行深度优先搜索。
    • 深度优先搜索结束后,将岛屿的数量 count 加1。
  3. 返回岛屿的数量 count。

该解法的时间复杂度是 O(M * N),其中 M 是二维网格的行数,N 是二维网格的列数。因为每个格子最多被访问一次,并且深度优先搜索的时间复杂度为 O(1),所以总体的时间复杂度为 O(M * N)。

深度优先搜索算法通过递归的方式遍历每个相邻的陆地,将它们标记为已访问的水域。通过遍历整个二维网格,可以找到所有的岛屿,并计算出岛屿的数量。该算法简单直观,并且在实际应用中具有较高的效率。

代码展示

package org.zyf.javabasic.letcode.graph;

/**
 * @author yanfengzhang
 * @description 岛屿数量(Number of Islands)是 LeetCode 上的一道经典算法题目,原题目描述如下:
 * 给定一个由 '1'(陆地)和 '0'(水)组成的二维网格的地图,计算岛屿的数量。一个岛被水包围,并且通过水平或垂直方向上相邻的陆地连接形成。你可以假设网格的四个边均被水包围。
 * 示例1:
 * 输入:
 * 11110
 * 11010
 * 11000
 * 00000
 * 输出: 1
 * 示例2:
 * 输入:
 * 11000
 * 11000
 * 00100
 * 00011
 * 输出: 3
 * 岛屿被水平或垂直方向上相邻的陆地连接形成,因此可以使用深度优先搜索(DFS)或广度优先搜索(BFS)算法来解决该问题。遍历网格中的每个格子,如果遇到陆地(值为 '1'),则进行深度优先搜索或广度优先搜索,将相邻的陆地全部标记为已访问的水域(值为 '0')。通过重复这个过程,可以计算出岛屿的数量。
 * 注意:在进行深度优先搜索或广度优先搜索时,需要注意边界条件的处理,以及避免重复访问已经访问过的格子。
 * @date 2023/5/2  22:45
 */
public class NumberOfIslands {
    /**
     * 算法步骤如下:
     * 1. 定义一个变量 count,用于记录岛屿的数量。
     * 2. 遍历二维网格的每个格子。
     * * 如果当前格子是陆地(值为 '1'),则进行深度优先搜索。
     * * 在深度优先搜索过程中,将当前格子标记为已访问,即将值从 '1' 修改为 '0'。
     * * 搜索当前格子的上、下、左、右四个方向的相邻格子,如果相邻格子也是陆地,则继续进行深度优先搜索。
     * * 深度优先搜索结束后,将岛屿的数量 count 加1。
     * 3. 返回岛屿的数量 count。
     * 该解法的时间复杂度是 O(M * N),其中 M 是二维网格的行数,N 是二维网格的列数。
     * 因为每个格子最多被访问一次,并且深度优先搜索的时间复杂度为 O(1),所以总体的时间复杂度为 O(M * N)。
     */
    public int numIslands(char[][] grid) {
        if (grid == null || grid.length == 0 || grid[0].length == 0) {
            return 0;
        }
        // 岛屿数量计数器
        int count = 0;
        // 网格行数
        int rows = grid.length;
        // 网格列数
        int cols = grid[0].length;

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (grid[i][j] == '1') {
                    count++;
                    dfs(grid, i, j);
                }
            }
        }

        return count;
    }

    private void dfs(char[][] grid, int row, int col) {
        // 网格行数
        int rows = grid.length;
        // 网格列数
        int cols = grid[0].length;

        // 检查当前格子是否越界或者不是陆地
        if (row < 0 || col < 0 || row >= rows || col >= cols || grid[row][col] != '1') {
            return;
        }

        // 将当前格子标记为已访问的水域
        grid[row][col] = '0';

        // 对当前格子的上、下、左、右四个相邻格子进行深度优先搜索
        // 上
        dfs(grid, row - 1, col);
        // 下
        dfs(grid, row + 1, col);
        // 左
        dfs(grid, row, col - 1);
        // 右
        dfs(grid, row, col + 1);
    }

    public static void main(String[] args) {
        // 创建二维字符数组表示网格
        char[][] grid = {
                {'1', '1', '1', '1', '0'},
                {'1', '1', '0', '1', '0'},
                {'1', '1', '0', '0', '0'},
                {'0', '0', '0', '0', '0'}
        };

        // 创建 NumberOfIslands 对象并计算岛屿数量
        NumberOfIslands numberOfIslands = new NumberOfIslands();
        int islandCount = numberOfIslands.numIslands(grid);

        // 打印岛屿数量
        System.out.println("Number of Islands: " + islandCount);
    }


}

4、网络延迟时间(Network Delay Time)

题目描述:给定一个由 n 个节点组成的网络,节点标号从 1 到 n。同时给定一个列表 times,其中 times[i] = (u, v, w) 表示信号从节点 u 发送到节点 v 需要的时间为 w。假设从给定节点 k 发送信号,返回所有 n 个节点接收信号所需的最短时间。如果无法使所有节点接收信号,返回 -1。

限制条件:

  • 网络中节点的数量在范围 [1, 100] 内。
  • 网络中边的数量在范围 [0, 10^4] 内。
  • 每条边 times[i] = (u, v, w) 满足 1 <= u, v <= n 且 0 <= w <= 100。

解法分析

解决网络延迟时间(Network Delay Time)问题的最优解法思路是使用迪杰斯特拉算法(Dijkstra's algorithm)。该算法可以在给定节点到其他所有节点的路径中找到最短路径。

以下是解题的最优解法思路:

  1. 创建一个距离字典 distances,用于存储从源节点到每个节点的最短路径距离。初始时,将源节点的距离设为0,其他节点的距离设为正无穷大。
  2. 创建一个优先队列 pq,用于存储待处理的节点。将源节点加入队列,并将其距离设为0。
    • 进入循环,直到队列为空:
    • 从队列中取出一个节点 node。
      • 遍历与 node 相邻的节点:
      • 计算从源节点到相邻节点的新距离 newDistance,即经过当前节点 node 到达相邻节点的距离。
      • 如果 newDistance 小于相邻节点的当前距离,则更新相邻节点的距离为 newDistance,并将相邻节点加入队列 pq。
  3. 遍历距离字典 distances,找出最大的距离。如果最大距离为正无穷大,则表示无法使所有节点接收信号,返回 -1;否则,返回最大距离。

使用迪杰斯特拉算法,可以在 O(E log V) 的时间复杂度内解决该问题,其中 E 是边的数量,V 是节点的数量。

需要注意的是,在实现中,可以使用邻接表或邻接矩阵来表示图的连接关系,并根据题目的要求进行相应的处理和计算。

这是一种常用的最短路径算法,并且在解决网络延迟时间问题时非常有效。

代码展示

以下是使用迪杰斯特拉算法解决网络延迟时间问题的 Java 代码:

package org.zyf.javabasic.letcode.graph;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;

/**
 * @author yanfengzhang
 * @description 给定一个由 n 个节点组成的网络,节点标号从 1 到 n。
 * 同时给定一个列表 times,其中 times[i] = (u, v, w) 表示信号从节点 u 发送到节点 v 需要的时间为 w。
 * 假设从给定节点 k 发送信号,返回所有 n 个节点接收信号所需的最短时间。如果无法使所有节点接收信号,返回 -1。
 * 限制条件:
 * * 网络中节点的数量在范围 [1, 100] 内。
 * * 网络中边的数量在范围 [0, 10^4] 内。
 * * 每条边 times[i] = (u, v, w) 满足 1 <= u, v <= n 且 0 <= w <= 100。
 * @date 2023/6/2  23:53
 */
public class NetworkDelayTime {

    /**
     * 1. 创建一个距离字典 distances,用于存储从源节点到每个节点的最短路径距离。初始时,将源节点的距离设为0,其他节点的距离设为正无穷大。
     * 2. 创建一个优先队列 pq,用于存储待处理的节点。将源节点加入队列,并将其距离设为0。
     * * 进入循环,直到队列为空:
     * * 从队列中取出一个节点 node。
     * * 遍历与 node 相邻的节点:
     * * 计算从源节点到相邻节点的新距离 newDistance,即经过当前节点 node 到达相邻节点的距离。
     * * 如果 newDistance 小于相邻节点的当前距离,则更新相邻节点的距离为 newDistance,并将相邻节点加入队列 pq。
     * 3. 遍历距离字典 distances,找出最大的距离。如果最大距离为正无穷大,则表示无法使所有节点接收信号,返回 -1;否则,返回最大距离。
     * 使用迪杰斯特拉算法,可以在 O(E log V) 的时间复杂度内解决该问题,其中 E 是边的数量,V 是节点的数量。
     * 需要注意的是,在实现中,可以使用邻接表或邻接矩阵来表示图的连接关系,并根据题目的要求进行相应的处理和计算。
     */
    public int networkDelayTime(int[][] times, int n, int k) {
        // 构建邻接表,用于存储节点的连接关系和权重
        Map<Integer, List<int[]>> graph = new HashMap<>();
        for (int[] edge : times) {
            int u = edge[0];
            int v = edge[1];
            int w = edge[2];
            graph.putIfAbsent(u, new ArrayList<>());
            graph.get(u).add(new int[]{v, w});
        }

        // 创建距离字典,初始时将所有节点的距离设为正无穷大
        Map<Integer, Integer> distances = new HashMap<>();
        for (int i = 1; i <= n; i++) {
            distances.put(i, Integer.MAX_VALUE);
        }

        // 创建优先队列,用于按照距离从小到大处理节点
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);

        // 将源节点加入队列,并将其距离设为0
        pq.offer(new int[]{k, 0});
        distances.put(k, 0);

        // 使用迪杰斯特拉算法计算最短路径
        while (!pq.isEmpty()) {
            int[] curr = pq.poll();
            int node = curr[0];
            int distance = curr[1];

            // 如果当前距离大于节点的已知最短距离,则跳过处理
            if (distance > distances.get(node)) {
                continue;
            }

            // 遍历与当前节点相邻的节点
            if (graph.containsKey(node)) {
                for (int[] neighbor : graph.get(node)) {
                    int nextNode = neighbor[0];
                    int nextDistance = distance + neighbor[1];

                    // 如果新距离小于节点的当前距离,则更新距离并加入队列
                    if (nextDistance < distances.get(nextNode)) {
                        pq.offer(new int[]{nextNode, nextDistance});
                        distances.put(nextNode, nextDistance);
                    }
                }
            }
        }

        // 找出最大的距离,如果存在正无穷大,则返回 -1
        int maxDistance = 0;
        for (int distance : distances.values()) {
            maxDistance = Math.max(maxDistance, distance);
        }
        return maxDistance == Integer.MAX_VALUE ? -1 : maxDistance;
    }

    public static void main(String[] args) {
        int[][] times = {{2, 1, 1}, {2, 3, 1}, {3, 4, 1}};
        int n = 4;
        int k = 2;
        int result = new NetworkDelayTime().networkDelayTime(times, n, k);
        System.out.println("Network Delay Time: " + result);
    }

}

5、单源最短路径(Dijkstra 算法)

题目描述:给定一个带权重的有向图,找到从源节点到目标节点的最短路径,即权重之和最小的路径。

具体要求如下:

  • 图中的每个边都有一个非负权重值,表示从一个节点到另一个节点的代价。
  • 源节点是给定的起始节点。
  • 目标节点是给定的终止节点。
  • 如果不存在从源节点到目标节点的路径,则返回一个特定的标识值,如无穷大或者-1。

该问题的输入通常是表示图的数据结构(如邻接表或邻接矩阵)以及源节点和目标节点的标识。

需要注意的是,Dijkstra 算法要求图中的边权重必须为非负值。如果图中存在负权边,可以考虑使用其他算法,如 Bellman-Ford 算法。

通过应用 Dijkstra 算法,可以有效地求解单源最短路径问题,并找到最短路径的权重之和。

解法分析

Dijkstra 算法是解决单源最短路径问题的一种常见且有效的算法。它采用贪心策略,逐步确定从源节点到其他节点的最短路径。

以下是 Dijkstra 算法的最优解法思路:

  1. 创建一个距离数组 dist,用于保存从源节点到每个节点的当前最短路径的距离。初始化所有节点的距离为无穷大,源节点的距离为0。
  2. 创建一个优先队列(如最小堆) pq,用于选择距离最小的节点进行扩展。
  3. 将源节点加入优先队列 pq。
    • 当优先队列 pq 非空时,执行以下步骤:
    • 从优先队列 pq 中取出距离最小的节点 u。
      • 遍历节点 u 的所有邻居节点 v,更新其距离:
      • 如果从源节点到节点 u 的距离加上节点 u 到节点 v 的边的权重小于节点 v 的当前距离,则更新节点 v 的距离为新的最短路径距离,并将节点 v 加入优先队列 pq。
  4. 遍历完所有节点后,距离数组 dist 中保存的即为从源节点到每个节点的最短路径距离。

Dijkstra 算法的最优解法使用了优先队列来选择当前距离最小的节点进行扩展,以保证每次扩展都是选择距离最短的路径。通过逐步更新节点的距离,最终得到从源节点到其他节点的最短路径。

Dijkstra 算法的时间复杂度取决于优先队列的实现方式,一般为 O((V + E) log V),其中 V 是节点数,E 是边数。

需要注意的是,如果图中存在负权边,Dijkstra 算法不适用,可以考虑使用其他算法,如 Bellman-Ford 算法或使用适应负权边的改进算法,如 Dijkstra 的变种算法。

代码展示

package org.zyf.javabasic.letcode.graph;

import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;

/**
 * @author yanfengzhang
 * @description 给定一个带权重的有向图,找到从源节点到目标节点的最短路径,即权重之和最小的路径。
 * 具体要求如下:
 * * 图中的每个边都有一个非负权重值,表示从一个节点到另一个节点的代价。
 * * 源节点是给定的起始节点。
 * * 目标节点是给定的终止节点。
 * * 如果不存在从源节点到目标节点的路径,则返回一个特定的标识值,如无穷大或者-1。
 * 该问题的输入通常是表示图的数据结构(如邻接表或邻接矩阵)以及源节点和目标节点的标识。
 * @date 2023/5/2  23:02
 */
public class SingleSourceShortestPath {

    /**
     * Dijkstra 算法是解决单源最短路径问题的一种常见且有效的算法。它采用贪心策略,逐步确定从源节点到其他节点的最短路径。
     * 以下是 Dijkstra 算法的最优解法思路:
     * 1. 创建一个距离数组 dist,用于保存从源节点到每个节点的当前最短路径的距离。初始化所有节点的距离为无穷大,源节点的距离为0。
     * 2. 创建一个优先队列(如最小堆) pq,用于选择距离最小的节点进行扩展。
     * 3. 将源节点加入优先队列 pq。
     * * 当优先队列 pq 非空时,执行以下步骤:
     * * 从优先队列 pq 中取出距离最小的节点 u。
     * * 遍历节点 u 的所有邻居节点 v,更新其距离:
     * * 如果从源节点到节点 u 的距离加上节点 u 到节点 v 的边的权重小于节点 v 的当前距离,则更新节点 v 的距离为新的最短路径距离,并将节点 v 加入优先队列 pq。
     * 4. 遍历完所有节点后,距离数组 dist 中保存的即为从源节点到每个节点的最短路径距离。
     * Dijkstra 算法的最优解法使用了优先队列来选择当前距离最小的节点进行扩展,以保证每次扩展都是选择距离最短的路径。通过逐步更新节点的距离,最终得到从源节点到其他节点的最短路径。
     * Dijkstra 算法的时间复杂度取决于优先队列的实现方式,一般为 O((V + E) log V),其中 V 是节点数,E 是边数。
     * 需要注意的是,如果图中存在负权边,Dijkstra 算法不适用,可以考虑使用其他算法,如 Bellman-Ford 算法或使用适应负权边的改进算法,如 Dijkstra 的变种算法。
     */
    public int[] dijkstra(int[][] graph, int source) {
        // 图中节点的数量

        int n = graph.length;
        // 保存从源节点到每个节点的最短路径距离
        int[] dist = new int[n];
        // 初始化距离为无穷大
        Arrays.fill(dist, Integer.MAX_VALUE);
        // 源节点到自身的距离为0
        dist[source] = 0;

        // 优先队列,按照距离排序
        PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));
        // 将源节点加入优先队列
        pq.offer(new int[]{source, 0});

        while (!pq.isEmpty()) {
            // 取出距离最小的节点
            int[] curr = pq.poll();

            int node = curr[0];
            int distance = curr[1];

            // 遍历当前节点的邻居节点
            for (int i = 0; i < n; i++) {
                // 当前节点到邻居节点存在边
                if (graph[node][i] > 0) {
                    int newDistance = distance + graph[node][i];
                    // 如果新路径距离更短,则更新距离并将邻居节点加入优先队列
                    if (newDistance < dist[i]) {
                        dist[i] = newDistance;
                        pq.offer(new int[]{i, newDistance});
                    }
                }
            }
        }
        // 返回源节点到每个节点的最短路径距离
        return dist;
    }

    public static void main(String[] args) {
        int[][] graph = {
                {0, 4, 2, 0, 0},
                {4, 0, 1, 5, 0},
                {2, 1, 0, 8, 10},
                {0, 5, 8, 0, 2},
                {0, 0, 10, 2, 0}
        };
        // 源节点的索引
        int source = 0;

        int[] dist = new SingleSourceShortestPath().dijkstra(graph, source);

        System.out.println("最短路径距离:");
        for (int i = 0; i < dist.length; i++) {
            System.out.println("从节点 " + source + " 到节点 " + i + " 的距离为:" + dist[i]);
        }
    }
}

6、负权最短路径问题(Negative Weight Shortest Path Problem)

题目描述:要求在带有负权边的有向图中,找到从源节点到目标节点的路径中权重之和最小的路径。具体要求如下:

  • 图中的每个边都有一个权重值,可以为正、负或零。
  • 源节点是给定的起始节点。
  • 目标节点是给定的终止节点。
  • 需要找到从源节点到目标节点的路径中权重之和最小的路径。

该问题的输入通常是表示图的数据结构(如邻接表或邻接矩阵)以及源节点和目标节点的标识。

负权最短路径问题与最短路径问题类似,但不同之处在于允许存在负权边。这使得问题更具挑战性,因为负权边可能导致无限循环或无法收敛的情况。因此,传统的最短路径算法,如 Dijkstra 算法,不能直接应用于负权最短路径问题。

解决负权最短路径问题的常见算法包括 Bellman-Ford 算法和 Floyd-Warshall 算法。这些算法能够处理负权边,但其时间复杂度较高。

需要注意的是,在某些情况下,负权边可能导致没有有限路径从源节点到目标节点,或者存在无穷多个路径。因此,在使用负权最短路径算法时,需要考虑这些特殊情况,并确定是否存在有效的路径。

解法分析

在负权最短路径问题中,最优解法可以使用 Bellman-Ford 算法或 Floyd-Warshall 算法。

  1. Bellman-Ford 算法:
    • Bellman-Ford 算法是一种适用于有向图的负权最短路径算法。
    • 算法通过迭代更新节点的最短路径估计,直到收敛或发现负权回路。
    • 对于一个包含 V 个节点和 E 条边的图,Bellman-Ford 算法的时间复杂度为 O(VE)。
    • Bellman-Ford 算法可以处理负权边和负权回路,因此适用于解决负权最短路径问题。
  2. Floyd-Warshall 算法:
    • Floyd-Warshall 算法是一种适用于有向图的所有节点对最短路径算法。
    • 算法通过动态规划的思想,逐步计算任意两个节点之间的最短路径。
    • 对于一个包含 V 个节点的图,Floyd-Warshall 算法的时间复杂度为 O(V^3)。
    • Floyd-Warshall 算法可以处理负权边,但不能处理负权回路。

选择使用 Bellman-Ford 算法还是 Floyd-Warshall 算法取决于具体情况:

  • 如果只需要求解从一个源节点到目标节点的最短路径,则可以使用 Bellman-Ford 算法。
  • 如果需要求解图中任意两个节点之间的最短路径,则可以使用 Floyd-Warshall 算法。

需要注意的是,负权最短路径问题的解不一定是唯一的。在存在多个最短路径的情况下,算法的输出可能会有所不同。

代码展示

以下是使用 Bellman-Ford 算法解决负权最短路径问题的 Java 代码:

package org.zyf.javabasic.letcode.graph;

import java.util.Arrays;

/**
 * @author yanfengzhang
 * @description 要求在带有负权边的有向图中,找到从源节点到目标节点的路径中权重之和最小的路径。
 * 具体要求如下:
 * * 图中的每个边都有一个权重值,可以为正、负或零。
 * * 源节点是给定的起始节点。
 * * 目标节点是给定的终止节点。
 * * 需要找到从源节点到目标节点的路径中权重之和最小的路径。
 * *
 * 该问题的输入通常是表示图的数据结构(如邻接表或邻接矩阵)以及源节点和目标节点的标识。
 * 负权最短路径问题与最短路径问题类似,但不同之处在于允许存在负权边。这
 * 使得问题更具挑战性,因为负权边可能导致无限循环或无法收敛的情况。因此,传统的最短路径算法,如 Dijkstra 算法,不能直接应用于负权最短路径问题。
 * 解决负权最短路径问题的常见算法包括 Bellman-Ford 算法和 Floyd-Warshall 算法。这些算法能够处理负权边,但其时间复杂度较高。
 * 需要注意的是,在某些情况下,负权边可能导致没有有限路径从源节点到目标节点,或者存在无穷多个路径。
 * 因此,在使用负权最短路径算法时,需要考虑这些特殊情况,并确定是否存在有效的路径。
 * @date 2022/4/2  23:19
 */
public class NegativeWeightShortestPath {

    /**
     * Bellman-Ford 算法:
     * Bellman-Ford 算法是一种适用于有向图的负权最短路径算法。
     * 算法通过迭代更新节点的最短路径估计,直到收敛或发现负权回路。
     * 对于一个包含 V 个节点和 E 条边的图,Bellman-Ford 算法的时间复杂度为 O(VE)。
     * Bellman-Ford 算法可以处理负权边和负权回路,因此适用于解决负权最短路径问题。
     */
    public int[] bellmanFord(int[][] graph, int source) {
        // 图中节点的数量
        int V = graph.length;
        // 保存从源节点到每个节点的最短路径距离
        int[] dist = new int[V];
        // 初始化距离为无穷大
        Arrays.fill(dist, Integer.MAX_VALUE);
        // 源节点到自身的距离为0
        dist[source] = 0;

        // 进行 V-1 轮松弛操作
        for (int i = 0; i < V - 1; i++) {
            // 遍历所有边
            for (int u = 0; u < V; u++) {
                for (int v = 0; v < V; v++) {
                    if (graph[u][v] != 0 && dist[u] != Integer.MAX_VALUE && dist[u] + graph[u][v] < dist[v]) {
                        // 更新节点 v 的最短路径距离
                        dist[v] = dist[u] + graph[u][v];
                    }
                }
            }
        }

        // 检测负权回路
        for (int u = 0; u < V; u++) {
            for (int v = 0; v < V; v++) {
                if (graph[u][v] != 0 && dist[u] != Integer.MAX_VALUE && dist[u] + graph[u][v] < dist[v]) {
                    // 存在负权回路,无法求解最短路径
                    return new int[0];
                }
            }
        }

        // 返回源节点到每个节点的最短路径距离
        return dist;
    }

    public static void main(String[] args) {
        int[][] graph = {
                {0, 4, 2, 0, 0},
                {4, 0, -1, 5, 0},
                {2, -1, 0, 8, 10},
                {0, 5, 8, 0, -6},
                {0, 0, 10, -6, 0}
        };

        // 源节点的索引
        int source = 0;
        int[] dist = new NegativeWeightShortestPath().bellmanFord(graph, source);

        if (dist.length == 0) {
            System.out.println("存在负权回路,无法求解最短路径");
        } else {
            System.out.println("最短路径距离:");
            for (int i = 0; i < dist.length; i++) {
                System.out.println("从节点 " + source + " 到节点 " + i + " 的距离为:" + dist[i]);
            }
        }
    }
}

7、具有最小生成树的连通图的最小代价(Prim 算法)

题目描述:给定一个带有权重的无向连通图,你需要找到一个生成树,使得这个生成树的权重之和最小。

注意:

  • 该图包含 N 个节点,编号为 0 到 N-1,其中 N 为图中节点的数量。
  • 生成树必须包含图中的所有节点。
  • 图的权重以一个二维数组 graph 表示,graph[i][j] 表示节点 i 和节点 j 之间的边的权重。
  • 如果节点 i 和节点 j 之间没有边,则 graph[i][j] = graph[j][i] = -1

示例 1:

输入:

graph = [    [-1, 2, -1, 6, -1],

    [2, -1, 3, 8, 5],

    [-1, 3, -1, -1, 7],

    [6, 8, -1, -1, 9],

    [-1, 5, 7, 9, -1]

]

输出: 16

解释: 生成树的边包括 (0, 1), (1, 2), (1, 3), (1, 4),总权重为 2 + 3 + 8 + 5 = 16。

注意:

  • 你可以假设图是连通的,并且图中不存在环。
  • 该算法的实现使用了 Prim 算法,通过贪心策略逐步添加权重最小的边来构建生成树

解法分析

最小生成树问题可以使用 Prim 算法解决,该算法基于贪心策略,逐步构建生成树,选择当前权重最小的边来扩展生成树。

具体步骤如下:

  1. 创建一个空的生成树集合和一个空的已访问节点集合。
  2. 选择一个起始节点,将其添加到已访问节点集合中。
    • 重复以下步骤,直到已访问节点集合包含所有节点:
    • 遍历已访问节点集合中的每个节点,找到与其相连且未访问过的节点中权重最小的边。
    • 将找到的边添加到生成树集合中,并将对应的节点添加到已访问节点集合中。
  3. 计算生成树集合中所有边的权重之和,即为最小生成树的代价。

最优解法的时间复杂度为 O(V^2),其中 V 是节点的数量。在实现过程中,通常会使用优先队列(Priority Queue)来快速选择权重最小的边,可以将时间复杂度优化到 O(E log V),其中 E 是边的数量。

注意:在图中存在多个连通分量的情况下,需要对每个连通分量分别进行最小生成树的构建。

代码展示

package org.zyf.javabasic.letcode.graph;

import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;

/**
 * @author yanfengzhang
 * @description 给定一个带有权重的无向连通图,你需要找到一个生成树,使得这个生成树的权重之和最小。
 * 注意:
 * * 该图包含 N 个节点,编号为 0 到 N-1,其中 N 为图中节点的数量。
 * * 生成树必须包含图中的所有节点。
 * * 图的权重以一个二维数组 graph 表示,graph[i][j] 表示节点 i 和节点 j 之间的边的权重。
 * * 如果节点 i 和节点 j 之间没有边,则 graph[i][j] = graph[j][i] = -1。
 * 示例 1:
 * 输入:
 * graph = [    [-1, 2, -1, 6, -1],
 *     [2, -1, 3, 8, 5],
 *     [-1, 3, -1, -1, 7],
 *     [6, 8, -1, -1, 9],
 *     [-1, 5, 7, 9, -1]
 * ]
 * 输出: 16
 * 解释: 生成树的边包括 (0, 1), (1, 2), (1, 3), (1, 4),总权重为 2 + 3 + 8 + 5 = 16。
 * 注意:
 * * 你可以假设图是连通的,并且图中不存在环。
 * * 该算法的实现使用了 Prim 算法,通过贪心策略逐步添加权重最小的边来构建生成树。
 * @date 2021/10/5  22:18
 */
public class MinimumSpanningTree {

    /**
     * 最小生成树问题可以使用 Prim 算法解决,该算法基于贪心策略,逐步构建生成树,选择当前权重最小的边来扩展生成树。
     * 具体步骤如下:
     * 1. 创建一个空的生成树集合和一个空的已访问节点集合。
     * 2. 选择一个起始节点,将其添加到已访问节点集合中。
     *     * 重复以下步骤,直到已访问节点集合包含所有节点:
     *     * 遍历已访问节点集合中的每个节点,找到与其相连且未访问过的节点中权重最小的边。
     *     * 将找到的边添加到生成树集合中,并将对应的节点添加到已访问节点集合中。
     * 3. 计算生成树集合中所有边的权重之和,即为最小生成树的代价。
     * 最优解法的时间复杂度为 O(V^2),其中 V 是节点的数量。
     * 在实现过程中,通常会使用优先队列(Priority Queue)来快速选择权重最小的边,可以将时间复杂度优化到 O(E log V),其中 E 是边的数量。
     * 注意:在图中存在多个连通分量的情况下,需要对每个连通分量分别进行最小生成树的构建。
     */
    public int minimumCost(int[][] graph) {
        // 节点的数量
        int N = graph.length;
        // 记录节点是否已访问
        boolean[] visited = new boolean[N];
        // 记录每个节点与已访问节点集合的最小边权重
        int[] minWeight = new int[N];
        // 初始化最小权重为最大值
        Arrays.fill(minWeight, Integer.MAX_VALUE);
        // 起始节点的最小权重为0
        minWeight[0] = 0;

        // 最小生成树的总代价
        int totalCost = 0; 

        // 优先队列用于快速选择权重最小的边
        PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));
        // 起始节点入队
        pq.offer(new int[]{0, 0}); 

        while (!pq.isEmpty()) {
            // 弹出权重最小的边
            int[] curr = pq.poll(); 
            int node = curr[0];
            int weight = curr[1];

            if (visited[node]) {
                // 跳过已访问的节点
                continue; 
            }

            // 标记节点为已访问
            visited[node] = true;
            // 更新总代价
            totalCost += weight; 

            // 遍历与当前节点相连的边
            for (int i = 0; i < N; i++) {
                if (graph[node][i] != -1 && !visited[i]) {
                    if (graph[node][i] < minWeight[i]) {
                        // 更新节点与已访问节点集合的最小权重
                        minWeight[i] = graph[node][i];
                        // 将边加入优先队列
                        pq.offer(new int[]{i, graph[node][i]}); 
                    }
                }
            }
        }

        return totalCost;
    }

    public static void main(String[] args) {
        // 创建一个有权重的连通图
        int[][] graph = {
                {0, 2, 0, 6, 0},
                {2, 0, 3, 8, 5},
                {0, 3, 0, 0, 7},
                {6, 8, 0, 0, 9},
                {0, 5, 7, 9, 0}
        };

        MinimumSpanningTree mst = new MinimumSpanningTree();
        int minCost = mst.minimumCost(graph);
        System.out.println("Minimum cost of spanning tree: " + minCost);
    }
}

8、找到最终的安全状态(Find Eventual Safe States)

题目描述:给定一个有向图,其中节点的数量为 n,边的数量为 m。节点编号从 0 到 n-1。给定一个数组 graph,其中 graph[i] 是节点 i 的所有出边的目标节点。求解图中所有最终安全的节点。

最终安全的节点是指在某个节点出发,通过任意路径无法到达环中的节点。换句话说,如果节点在有向图中没有出边,或者从该节点出发能够到达的所有节点都是最终安全的,则该节点是最终安全的。

解法分析

可以使用深度优先搜索(DFS)来解决该问题。从每个节点开始进行DFS,同时使用一个标志数组 visited 来记录节点的访问状态。初始状态下,所有节点的 visited 值都设为0,表示未访问。

在DFS的过程中,需要处理以下三种情况:

  1. 当访问到一个已访问过的节点时,说明存在环路,将当前路径上的所有节点标记为不安全节点,即将它们的 visited 值设为-1。
  2. 当访问到一个已标记为不安全节点的节点时,直接返回。
  3. 当访问到一个未访问过的节点时,将其 visited 值设为1,表示正在访问,然后继续对它的邻居节点进行DFS。

最后,遍历所有节点,将 visited 值为1的节点添加到结果列表中,即为最终安全的节点。

代码展示

package org.zyf.javabasic.letcode.graph;

import java.util.ArrayList;
import java.util.List;

/**
 * @author yanfengzhang
 * @description 给定一个有向图,其中节点的数量为 n,边的数量为 m。
 * 节点编号从 0 到 n-1。给定一个数组 graph,其中 graph[i] 是节点 i 的所有出边的目标节点。
 * 求解图中所有最终安全的节点。
 * 最终安全的节点是指在某个节点出发,通过任意路径无法到达环中的节点。
 * 换句话说,如果节点在有向图中没有出边,或者从该节点出发能够到达的所有节点都是最终安全的,则该节点是最终安全的。
 * @date 2020/2/21  23:44
 */
public class FindEventualSafeStates {

    /**
     * 最优解法分析:
     * 可以使用深度优先搜索(DFS)来解决该问题。从每个节点开始进行DFS,同时使用一个标志数组 visited 来记录节点的访问状态。
     * 初始状态下,所有节点的 visited 值都设为0,表示未访问。
     * 在DFS的过程中,需要处理以下三种情况:
     * 1. 当访问到一个已访问过的节点时,说明存在环路,将当前路径上的所有节点标记为不安全节点,即将它们的 visited 值设为-1。
     * 2. 当访问到一个已标记为不安全节点的节点时,直接返回。
     * 3. 当访问到一个未访问过的节点时,将其 visited 值设为1,表示正在访问,然后继续对它的邻居节点进行DFS。
     * 最后,遍历所有节点,将 visited 值为1的节点添加到结果列表中,即为最终安全的节点。
     */
    public List<Integer> eventualSafeNodes(int[][] graph) {
        int n = graph.length;
        // 标记节点的访问状态:0-未访问,1-正在访问,-1-不安全节点
        int[] visited = new int[n];
        List<Integer> result = new ArrayList<>();

        for (int i = 0; i < n; i++) {
            if (isSafe(graph, i, visited)) {
                result.add(i);
            }
        }

        return result;
    }

    private boolean isSafe(int[][] graph, int node, int[] visited) {
        if (visited[node] == 1) {
            // 当前节点正在访问,存在环路,不安全
            return false;
        }
        if (visited[node] == -1) {
            // 当前节点已标记为不安全节点,安全
            return true;
        }

        // 标记当前节点为正在访问状态
        visited[node] = 1;

        for (int neighbor : graph[node]) {
            if (!isSafe(graph, neighbor, visited)) {
                // 邻居节点存在环路,当前节点不安全
                return false;
            }
        }

        // 当前节点及其邻居节点都是安全的
        visited[node] = -1;

        return true;
    }

    public static void main(String[] args) {
        FindEventualSafeStates solution = new FindEventualSafeStates();

        // 示例 1
        int[][] graph1 = {{1, 2}, {2, 3}, {5}, {0}, {5}, {}, {}};
        List<Integer> result1 = solution.eventualSafeNodes(graph1);
        // 预期输出:[2, 4, 5, 6]
        System.out.println("Example 1: " + result1);

        // 示例 2
        int[][] graph2 = {{1, 2, 3, 4}, {1, 2}, {3, 4}, {0, 4}, {}};
        List<Integer> result2 = solution.eventualSafeNodes(graph2);
        // 预期输出:[4]
        System.out.println("Example 2: " + result2);

        // 示例 3
        int[][] graph3 = {};
        List<Integer> result3 = solution.eventualSafeNodes(graph3);
        // 预期输出:[]
        System.out.println("Example 3: " + result3);
    }
}

9、网络流问题的最大流(Maximum Flow)

题目描述:给定一个网络,包含源节点s和汇节点t,以及一些中间节点。每条边都有一个容量,表示该边允许通过的最大流量。

你的任务是找到从源节点s到汇节点t的最大流量。

请实现一个函数 int maxFlow(int[][] graph, int s, int t),其中 graph 是一个二维数组,表示网络的邻接矩阵,graph[i][j] 表示节点i到节点j的边的容量。s 表示源节点的索引,t 表示汇节点的索引。

函数应返回从源节点到汇节点的最大流量。

示例:

输入:

graph = [

    [0, 3, 0, 3, 0, 0],

    [0, 0, 4, 0, 0, 0],

    [0, 0, 0, 1, 2, 0],

    [0, 0, 0, 0, 2, 6],

    [0, 0, 0, 0, 0, 1],

    [0, 0, 0, 0, 0, 0]

]

s = 0

t = 5

输出:5

解释:

从源节点0到汇节点5的最大流量为5。

请注意,这只是一个示例题目描述,实际题目可能会有更多的细节和限制条件。

解法分析

网络流问题的最优解法通常使用增广路径算法,其中最常用的是Edmonds-Karp算法。下面是对最大流问题的最优解法的分析:

  1. 初始化网络流:将所有边的流量初始化为0。
  2. 寻找增广路径:使用广度优先搜索(BFS)或深度优先搜索(DFS)来寻找从源节点s到汇节点t的增广路径。增广路径是指在剩余图中存在一条路径,该路径上所有边的剩余容量大于0。
  3. 计算路径上的最大可增加流量:在找到增广路径后,遍历路径上的边,找到剩余容量的最小值。这个值表示路径上可以增加的最大流量。
  4. 更新路径上的流量:遍历路径上的边,将其流量增加或减少最大可增加流量。
  5. 重复步骤2-4:继续寻找增广路径并更新流量,直到不存在从源节点s到汇节点t的增广路径。
  6. 计算最大流量:遍历从源节点s出发的所有边,累加它们的流量,得到最大流量。

Edmonds-Karp算法的时间复杂度为O(V*E^2),其中V是节点的数量,E是边的数量。这个算法保证在有向图中找到最大流。

注意:除了Edmonds-Karp算法,还有其他的最优解法,如Dinic算法和Ford-Fulkerson算法等,它们在不同的情况下可能具有更好的性能。选择最合适的算法取决于具体的问题和输入规模。

代码展示

package org.zyf.javabasic.letcode.graph;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;

/**
 * @author yanfengzhang
 * @description 给定一个网络,包含源节点s和汇节点t,以及一些中间节点。每条边都有一个容量,表示该边允许通过的最大流量。
 * 你的任务是找到从源节点s到汇节点t的最大流量。
 * 请实现一个函数 int maxFlow(int[][] graph, int s, int t),其中 graph 是一个二维数组,表示网络的邻接矩阵,
 * graph[i][j] 表示节点i到节点j的边的容量。s 表示源节点的索引,t 表示汇节点的索引。
 * 函数应返回从源节点到汇节点的最大流量。
 * 示例:
 * 输入:
 * graph = [
 *     [0, 3, 0, 3, 0, 0],
 *     [0, 0, 4, 0, 0, 0],
 *     [0, 0, 0, 1, 2, 0],
 *     [0, 0, 0, 0, 2, 6],
 *     [0, 0, 0, 0, 0, 1],
 *     [0, 0, 0, 0, 0, 0]
 * ]
 * s = 0
 * t = 5
 * 输出:5
 * 解释:
 * 从源节点0到汇节点5的最大流量为5。
 * 请注意,这只是一个示例题目描述,实际题目可能会有更多的细节和限制条件。
 * @date 2021/7/15  23:49
 */
public class MaximumFlow {

    /**
     * 使用BFS找到从源节点到汇节点的增广路径
     */
    private boolean bfs(int[][] residualGraph, int[] parent, int source, int target) {
        int vertices = residualGraph.length;
        boolean[] visited = new boolean[vertices];
        Arrays.fill(visited, false);

        // 创建一个队列用于BFS遍历
        Queue<Integer> queue = new LinkedList<>();
        queue.add(source);
        visited[source] = true;
        parent[source] = -1;

        while (!queue.isEmpty()) {
            int current = queue.poll();

            // 遍历当前节点的邻接节点
            for (int next = 0; next < vertices; next++) {
                if (!visited[next] && residualGraph[current][next] > 0) {
                    queue.add(next);
                    visited[next] = true;
                    parent[next] = current;
                }
            }
        }

        // 如果在BFS过程中能够到达汇节点,则存在增广路径
        return visited[target];
    }

    /**
     * 使用Edmonds-Karp算法计算最大流
     */
    public int maxFlow(int[][] graph, int source, int target) {
        int vertices = graph.length;

        // 创建一个剩余图,初始化为输入图
        int[][] residualGraph = new int[vertices][vertices];
        for (int i = 0; i < vertices; i++) {
            for (int j = 0; j < vertices; j++) {
                residualGraph[i][j] = graph[i][j];
            }
        }

        // 初始化最大流为0
        int maxFlow = 0;

        // 创建一个用于存储增广路径的数组
        int[] parent = new int[vertices];

        // 不断寻找增广路径,并更新剩余图中的流量,直到不存在增广路径
        while (bfs(residualGraph, parent, source, target)) {
            // 计算增广路径上的最大可增加流量
            int minCapacity = Integer.MAX_VALUE;
            for (int v = target; v != source; v = parent[v]) {
                int u = parent[v];
                minCapacity = Math.min(minCapacity, residualGraph[u][v]);
            }

            // 更新路径上的流量
            for (int v = target; v != source; v = parent[v]) {
                int u = parent[v];
                residualGraph[u][v] -= minCapacity;
                residualGraph[v][u] += minCapacity;
            }

            // 增加最大流量
            maxFlow += minCapacity;
        }

        // 返回最大流量
        return maxFlow;
    }

    public static void main(String[] args) {
        int[][] graph = {
                {0, 3, 0, 3, 0, 0},
                {0, 0, 4, 0, 0, 0},
                {0, 0, 0, 1, 2, 0},
                {0, 0, 0, 0, 2, 6},
                {0, 0, 0, 0, 0, 1},
                {0, 0, 0, 0, 0, 0}
        };
        int source = 0;
        int target = 5;

        MaximumFlow maxFlow = new MaximumFlow();
        int maxFlowValue = maxFlow.maxFlow(graph, source, target);

        System.out.println("The maximum flow from source " + source + " to target " + target + " is: " + maxFlowValue);
    }


}

10、图中的可变流量(Graph Valid Tree)

题目描述:给定一个无向图,判断它是否是一棵有效的树。一个有效的树满足以下两个条件:

  1. 图中的所有节点都通过边连接在一起,形成一个联通图。
  2. 不存在环,即图中没有回路。

这个问题可以用于判断一些实际应用场景中的拓扑结构是否满足树的条件,例如电路布线、网络拓扑等。

解法分析

使用邻接表表示图,并使用深度优先搜索(DFS)算法进行遍历和环的检测。具体步骤如下:

  1. 首先创建一个邻接表 adjacencyList,表示图中每个节点的邻接节点列表。
  2. 使用输入的边信息构建邻接表。
  3. 创建一个布尔数组 visited,用于记录节点是否被访问过,初始值为 false。
  4. 调用 hasCycle 方法进行环的检测,从节点 0 开始,初始父节点为 -1。
  5. 在 hasCycle 方法中,首先将当前节点标记为已访问,并遍历当前节点的邻接节点。
  6. 如果邻接节点已被访问过且不是当前节点的父节点,说明存在环,返回 true。
  7. 如果邻接节点没有被访问过,则递归调用 hasCycle 方法,并将邻接节点作为新的当前节点,当前节点作为新的父节点。
  8. 如果遍历完所有节点后都没有找到环,则返回 false,表示图中没有环。

代码展示

package org.zyf.javabasic.letcode.graph;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author yanfengzhang
 * @description 给定一个无向图,判断它是否是一棵有效的树。一个有效的树满足以下两个条件:
 * 1. 图中的所有节点都通过边连接在一起,形成一个联通图。
 * 2. 不存在环,即图中没有回路。
 * 这个问题可以用于判断一些实际应用场景中的拓扑结构是否满足树的条件,例如电路布线、网络拓扑等。
 * @date 2022/11/12  23:58
 */
public class GraphValidTree {

    /**
     * 使用邻接表表示图,并使用深度优先搜索(DFS)算法进行遍历和环的检测。具体步骤如下:
     * 1. 首先创建一个邻接表 adjacencyList,表示图中每个节点的邻接节点列表。
     * 2. 使用输入的边信息构建邻接表。
     * 3. 创建一个布尔数组 visited,用于记录节点是否被访问过,初始值为 false。
     * 4. 调用 hasCycle 方法进行环的检测,从节点 0 开始,初始父节点为 -1。
     * 5. 在 hasCycle 方法中,首先将当前节点标记为已访问,并遍历当前节点的邻接节点。
     * 6. 如果邻接节点已被访问过且不是当前节点的父节点,说明存在环,返回 true。
     * 7. 如果邻接节点没有被访问过,则递归调用 hasCycle 方法,并将邻接节点作为新的当前节点,当前节点作为新的父节点。
     * 8. 如果遍历完所有节点后都没有找到环,则返回 false,表示图中没有环。
     */
    public boolean isValidTree(int n, int[][] edges) {
        // 创建邻接表表示图
        List<List<Integer>> adjacencyList = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            adjacencyList.add(new ArrayList<>());
        }

        // 构建邻接表
        for (int[] edge : edges) {
            int u = edge[0];
            int v = edge[1];
            adjacencyList.get(u).add(v);
            adjacencyList.get(v).add(u);
        }

        // 记录节点是否被访问过
        boolean[] visited = new boolean[n];
        Arrays.fill(visited, false);

        // 检测是否存在环
        if (hasCycle(adjacencyList, visited, 0, -1)) {
            return false;
        }

        // 检测图是否是联通的
        for (int i = 0; i < n; i++) {
            if (!visited[i]) {
                return false;
            }
        }

        return true;
    }

    // 使用深度优先搜索检测是否存在环
    private boolean hasCycle(List<List<Integer>> adjacencyList, boolean[] visited, int node, int parent) {
        visited[node] = true;

        // 遍历当前节点的邻接节点
        for (int neighbor : adjacencyList.get(node)) {
            // 如果邻接节点已经被访问过,并且不是当前节点的父节点,说明存在环
            if (visited[neighbor] && neighbor != parent) {
                return true;
            }

            // 如果邻接节点没有被访问过,则继续递归检测
            if (!visited[neighbor] && hasCycle(adjacencyList, visited, neighbor, node)) {
                return true;
            }
        }

        return false;
    }

    public static void main(String[] args) {
        GraphValidTree graphValidTree = new GraphValidTree();

        // 创建一个示例图
        int n = 5;
        int[][] edges = {{0, 1}, {0, 2}, {0, 3}, {1, 4}};

        boolean isValid = graphValidTree.isValidTree(n, edges);

        System.out.println("图是否是一棵有效的树: " + isValid);
    }

}

11、图中的割边(Minimum Cut)

题目描述:给定一个无向图和它的顶点数量 n,以及图中的边集合 edges,每条边由两个顶点的索引组成。找到一条边,删除它后,将图分成两个或多个连通分量,且删除后的连通分量数最小。

问题的目标是找到这条割边,或者判断给定的图中是否存在割边。

请注意,最小割问题通常使用更复杂的算法来解决,例如图的最大流算法(Ford-Fulkerson 算法或 Edmonds-Karp 算法)结合深度优先搜索(DFS)或广度优先搜索(BFS)。

解法分析

最小割问题是一个经典的图论问题,其最优解法可以使用图的最大流算法来解决。以下是最小割问题的最优解法分析:

  1. 使用图的最大流算法:最小割问题可以转化为图的最大流问题。首先,我们将给定的无向图转换为有向图,对于每条边 (u, v),我们添加两条有向边 (u, v) 和 (v, u),并将它们的容量都设为1。然后,我们使用最大流算法(如Ford-Fulkerson算法或Edmonds-Karp算法)找到图的最大流。
  2. 寻找割边:在最大流算法找到最大流后,我们需要寻找割边。割边是指从源节点(或起始节点)可达的节点与从汇点(或终止节点)可达的节点之间的边。在最大流算法中,我们可以通过标记可达节点的方法来寻找割边。首先,从源节点开始,通过遍历流量不为0的边,标记所有可达的节点。然后,我们遍历所有的边,找到一条连接一个被标记节点和一个未标记节点的边,即为割边。
  3. 输出结果:根据题目的要求,我们可以返回割边的具体信息,如两个顶点的索引或边的具体描述,或者只返回是否存在割边的布尔值。

最大流算法的时间复杂度取决于具体的实现方法,常见的算法复杂度为 O(E * V^2),其中 E 是边的数量,V 是顶点的数量。因此,最小割问题的解法在一般情况下具有较高的时间复杂度。

需要注意的是,最小割问题的最优解法涉及到图的算法和数据结构,实现起来可能较为复杂。因此,对于具体的问题和应用场景,需要仔细分析和选择合适的算法来解决。

代码展示

package org.zyf.javabasic.letcode.graph;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

/**
 * @author yanfengzhang
 * @description 图中的割边问题,也被称为最小割问题(Minimum Cut),是指在无向图中找到一条边,删除这条边后将图分成两个或多个连通分量,且删除后的连通分量数最小。以下是一个简化的描述:
 * 给定一个无向图和它的顶点数量 n,以及图中的边集合 edges,每条边由两个顶点的索引组成。找到一条边,删除它后,将图分成两个或多个连通分量,且删除后的连通分量数最小。
 * 问题的目标是找到这条割边,或者判断给定的图中是否存在割边。
 * 请注意,最小割问题通常使用更复杂的算法来解决,例如图的最大流算法(Ford-Fulkerson 算法或 Edmonds-Karp 算法)结合深度优先搜索(DFS)或广度优先搜索(BFS)。
 * @date 2022/10/2  20:04
 */
public class MinimumCut {
    /**
     * 最小割问题是一个经典的图论问题,其最优解法可以使用图的最大流算法来解决。以下是最小割问题的最优解法分析:
     * 1. 使用图的最大流算法:最小割问题可以转化为图的最大流问题。
     * 首先,我们将给定的无向图转换为有向图,对于每条边 (u, v),我们添加两条有向边 (u, v) 和 (v, u),并将它们的容量都设为1。
     * 然后,我们使用最大流算法(如Ford-Fulkerson算法或Edmonds-Karp算法)找到图的最大流。
     * 2. 寻找割边:在最大流算法找到最大流后,我们需要寻找割边。割边是指从源节点(或起始节点)可达的节点与从汇点(或终止节点)可达的节点之间的边。
     * 在最大流算法中,我们可以通过标记可达节点的方法来寻找割边。首先,从源节点开始,通过遍历流量不为0的边,标记所有可达的节点。
     * 然后,我们遍历所有的边,找到一条连接一个被标记节点和一个未标记节点的边,即为割边。
     * 3. 输出结果:根据题目的要求,我们可以返回割边的具体信息,如两个顶点的索引或边的具体描述,或者只返回是否存在割边的布尔值。
     * 最大流算法的时间复杂度取决于具体的实现方法,常见的算法复杂度为 O(E * V^2),其中 E 是边的数量,V 是顶点的数量。
     * 因此,最小割问题的解法在一般情况下具有较高的时间复杂度。
     */
    public int findMinCut(int n, int[][] edges) {
        // 构建有向图
        int[][] graph = new int[n][n];
        for (int[] edge : edges) {
            int u = edge[0];
            int v = edge[1];
            int w = edge[2];
            graph[u][v] += w;
            graph[v][u] += w;
        }

        // 使用Ford-Fulkerson算法求最大流
        int[] parent = new int[n];
        int maxFlow = 0;
        while (bfs(graph, 0, n - 1, parent)) {
            int pathFlow = Integer.MAX_VALUE;
            for (int v = n - 1; v != 0; v = parent[v]) {
                int u = parent[v];
                pathFlow = Math.min(pathFlow, graph[u][v]);
            }
            for (int v = n - 1; v != 0; v = parent[v]) {
                int u = parent[v];
                graph[u][v] -= pathFlow;
                graph[v][u] += pathFlow;
            }
            maxFlow += pathFlow;
        }

        return maxFlow;
    }

    public List<int[]> findCutEdges(int n, int[][] edges) {
        // 构建有向图
        int[][] graph = new int[n][n];
        for (int[] edge : edges) {
            int u = edge[0];
            int v = edge[1];
            int w = edge[2];
            graph[u][v] += w;
            graph[v][u] += w;
        }

        // 使用Ford-Fulkerson算法求最大流
        int[] parent = new int[n];
        int maxFlow = 0;
        while (bfs(graph, 0, n - 1, parent)) {
            int pathFlow = Integer.MAX_VALUE;
            for (int v = n - 1; v != 0; v = parent[v]) {
                int u = parent[v];
                pathFlow = Math.min(pathFlow, graph[u][v]);
            }
            for (int v = n - 1; v != 0; v = parent[v]) {
                int u = parent[v];
                graph[u][v] -= pathFlow;
                graph[v][u] += pathFlow;
            }
            maxFlow += pathFlow;
        }

        // 标记可达节点
        boolean[] visited = new boolean[n];
        dfs(graph, 0, visited);

        // 找到割边
        List<int[]> cutEdges = new ArrayList<>();
        for (int[] edge : edges) {
            int u = edge[0];
            int v = edge[1];
            if (visited[u] && !visited[v] || !visited[u] && visited[v]) {
                cutEdges.add(new int[]{u, v});
            }
        }

        return cutEdges;
    }

    private boolean bfs(int[][] graph, int s, int t, int[] parent) {
        int n = graph.length;
        boolean[] visited = new boolean[n];
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(s);
        visited[s] = true;
        parent[s] = -1;
        while (!queue.isEmpty()) {
            int u = queue.poll();
            for (int v = 0; v < n; v++) {
                if (!visited[v] && graph[u][v] > 0) {
                    queue.offer(v);
                    visited[v] = true;
                    parent[v] = u;
                }
            }
        }
        return visited[t];
    }

    private void dfs(int[][] graph, int u, boolean[] visited) {
        visited[u] = true;
        for (int v = 0; v < graph.length; v++) {
            if (graph[u][v] > 0 && !visited[v]) {
                dfs(graph, v, visited);
            }
        }
    }

    public static void main(String[] args) {
        int n = 6;
        int[][] edges = {{0, 1, 2}, {0, 2, 3}, {1, 2, 1}, {1, 3, 4}, {2, 4, 5}, {3, 4, 2}, {3, 5, 1}, {4, 5, 4}};
        MinimumCut minimumCut = new MinimumCut();
        int minCut = minimumCut.findMinCut(n, edges);
        List<int[]> cutEdges = minimumCut.findCutEdges(n, edges);
        System.out.println("最小割值为:" + minCut);
        System.out.println("割边为:");
        for (int[] edge : cutEdges) {
            System.out.println(Arrays.toString(edge));
        }
    }
}

12、隐藏的好友(Friend Circles)

题目描述:给定一个n x n的矩阵M,表示一个社交关系图。M[i][j] = 1表示第i个人和第j个人是直接朋友关系,M[i][j] = 0表示不是直接朋友关系。你需要计算出总共有多少个朋友圈。

例如,给定下面的矩阵M:

[[1,1,0],

 [1,1,0],

 [0,0,1]]

该矩阵表示有3个人,第0个人和第1个人是直接朋友,所以他们属于一个朋友圈;第2个人与自己独立,所以属于另一个朋友圈。因此,总共有2个朋友圈。

要求编写一个函数 int findCircleNum(int[][] M),返回朋友圈的数量。

注意:

  • n在[1,200]的范围内。
  • M[i][i] = 1,对角线上的元素表示每个人和自己是直接朋友关系。

解法分析

Friend Circles问题可以使用并查集(Union Find)算法来解决,以下是最优解法的分析:

  1. 创建一个大小为n的数组parent,用于记录每个人所属的朋友圈编号。
  2. 初始化parent数组,将每个人的朋友圈编号初始化为自身的编号,即parent[i] = i。
  3. 遍历矩阵M的每个元素M[i][j],当M[i][j]为1时,表示第i个人和第j个人是直接朋友关系。
  4. 对于每对直接朋友关系的人,使用并查集将他们合并到同一个朋友圈中。
  5. 合并操作的实现是通过找到根节点的方式,比较两个人所属朋友圈的根节点,将一个朋友圈的根节点指向另一个朋友圈的根节点。
  6. 统计并查集中根节点的数量,即为朋友圈的数量。

使用并查集算法可以快速合并朋友圈,使得查找和合并操作的时间复杂度为O(1)。因此,该解法的时间复杂度为O(n^2),其中n为人数。

代码展示

package org.zyf.javabasic.letcode.graph;

/**
 * @author yanfengzhang
 * @description 给定一个n x n的矩阵M,表示一个社交关系图。M[i][j] = 1表示第i个人和第j个人是直接朋友关系,M[i][j] = 0表示不是直接朋友关系。
 * 你需要计算出总共有多少个朋友圈。
 * 例如,给定下面的矩阵M:
 * [[1,1,0],
 * [1,1,0],
 * [0,0,1]]
 * 该矩阵表示有3个人,第0个人和第1个人是直接朋友,所以他们属于一个朋友圈;第2个人与自己独立,所以属于另一个朋友圈。因此,总共有2个朋友圈。
 * 要求编写一个函数 int findCircleNum(int[][] M),返回朋友圈的数量。
 * 注意:
 * * n在[1,200]的范围内。
 * * M[i][i] = 1,对角线上的元素表示每个人和自己是直接朋友关系。
 * @date 2021/5/5  20:40
 */
public class FriendCircles {
    /**
     * Friend Circles问题可以使用并查集(Union Find)算法来解决,以下是最优解法的分析:
     * 1. 创建一个大小为n的数组parent,用于记录每个人所属的朋友圈编号。
     * 2. 初始化parent数组,将每个人的朋友圈编号初始化为自身的编号,即parent[i] = i。
     * 3. 遍历矩阵M的每个元素M[i][j],当M[i][j]为1时,表示第i个人和第j个人是直接朋友关系。
     * 4. 对于每对直接朋友关系的人,使用并查集将他们合并到同一个朋友圈中。
     * 5. 合并操作的实现是通过找到根节点的方式,比较两个人所属朋友圈的根节点,将一个朋友圈的根节点指向另一个朋友圈的根节点。
     * 6. 统计并查集中根节点的数量,即为朋友圈的数量。
     * 使用并查集算法可以快速合并朋友圈,使得查找和合并操作的时间复杂度为O(1)。因此,该解法的时间复杂度为O(n^2),其中n为人数。
     */
    public int findCircleNum(int[][] M) {
        int n = M.length;
        int[] parent = new int[n];
        int count = n;

        // 初始化parent数组
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }

        // 遍历矩阵M,进行合并操作
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (M[i][j] == 1) {
                    int root1 = findRoot(parent, i);
                    int root2 = findRoot(parent, j);
                    if (root1 != root2) {
                        parent[root1] = root2;
                        count--;
                    }
                }
            }
        }

        return count;
    }

    private int findRoot(int[] parent, int i) {
        while (parent[i] != i) {
            // 路径压缩,将i的父节点直接指向父节点的父节点
            parent[i] = parent[parent[i]];
            i = parent[i];
        }
        return i;
    }

    public static void main(String[] args) {
        int[][] M = {
                {1, 1, 0},
                {1, 1, 0},
                {0, 0, 1}
        };

        int result = new FriendCircles().findCircleNum(M);
        System.out.println("朋友圈的数量:" + result);
    }
}

13、欧拉路径(Eulerian Path)

题目描述:给定一个有向图,图中可能存在一个或多个欧拉路径(Eulerian Path)。欧拉路径是指从图中的一个顶点出发,沿着边遍历图中的每个边恰好一次,并且最终回到起点的路径。要求编写一个函数boolean hasEulerianPath(int[][] graph)来判断给定的有向图是否存在欧拉路径。

函数参数graph是一个二维数组,表示有向图的邻接矩阵。graph[i][j]为1表示存在一条从顶点i到顶点j的有向边,为0表示不存在。图中的顶点编号从0到n-1。

如果给定的有向图存在欧拉路径,则返回true;否则返回false。

注意:

  • 图中可能存在多个连通分量,每个连通分量都需要满足存在欧拉路径的条件。
  • 图中可能存在孤立的顶点,即没有任何出度和入度的顶点。

这道题可以使用图的遍历算法来解决,可以通过深度优先搜索(DFS)或广度优先搜索(BFS)来查找欧拉路径。

解法分析

判断给定的有向图是否存在欧拉路径的最优解法可以基于欧拉路径的性质进行判断。

欧拉路径的性质:

  • 对于一个有向图,存在欧拉路径的充分必要条件是:该图是强连通的(Strongly Connected)且满足以下两个条件之一:
  • 所有顶点的出度和入度相等,即每个顶点的出度等于入度;
  • 除了起点和终点之外,所有顶点的出度等于入度,而起点的出度比入度大1,终点的入度比出度大1。

解题思路:

  1. 首先,遍历图中的每个顶点,统计每个顶点的出度和入度。
  2. 如果图中存在孤立的顶点(出度和入度均为0),则直接返回false,因为无法从这些顶点出发或到达这些顶点。
  3. 否则,如果图是强连通的,并且满足上述欧拉路径的两个条件之一,则返回true;否则返回false。

该解法的时间复杂度为O(V + E),其中V是顶点的数量,E是边的数量。

请注意,在实际编写代码时,需要使用适当的数据结构(如邻接矩阵或邻接表)来表示图,并使用图的遍历算法(如DFS或BFS)来实现上述解法。

代码展示

package org.zyf.javabasic.letcode.graph;

/**
 * @author yanfengzhang
 * @description 给定一个有向图,图中可能存在一个或多个欧拉路径(Eulerian Path)。欧拉路径是指从图中的一个顶点出发,沿着边遍历图中的每个边恰好一次,并且最终回到起点的路径。要求编写一个函数boolean hasEulerianPath(int[][] graph)来判断给定的有向图是否存在欧拉路径。
 * 函数参数graph是一个二维数组,表示有向图的邻接矩阵。graph[i][j]为1表示存在一条从顶点i到顶点j的有向边,为0表示不存在。图中的顶点编号从0到n-1。
 * 如果给定的有向图存在欧拉路径,则返回true;否则返回false。
 * 注意:
 * * 图中可能存在多个连通分量,每个连通分量都需要满足存在欧拉路径的条件。
 * * 图中可能存在孤立的顶点,即没有任何出度和入度的顶点。
 * 这道题可以使用图的遍历算法来解决,可以通过深度优先搜索(DFS)或广度优先搜索(BFS)来查找欧拉路径。
 * @date 2019/3/5  20:46
 */
public class EulerianPath {

    /**
     * 判断给定的有向图是否存在欧拉路径的最优解法可以基于欧拉路径的性质进行判断。下面是最优解法的分析:
     * 1. 欧拉路径的性质:
     * * 对于一个有向图,存在欧拉路径的充分必要条件是:该图是强连通的(Strongly Connected)且满足以下两个条件之一:
     * * 所有顶点的出度和入度相等,即每个顶点的出度等于入度;
     * * 除了起点和终点之外,所有顶点的出度等于入度,而起点的出度比入度大1,终点的入度比出度大1。
     * 2. 解题思路:
     * * 首先,遍历图中的每个顶点,统计每个顶点的出度和入度。
     * * 如果图中存在孤立的顶点(出度和入度均为0),则直接返回false,因为无法从这些顶点出发或到达这些顶点。
     * * 否则,如果图是强连通的,并且满足上述欧拉路径的两个条件之一,则返回true;否则返回false。
     * 该解法的时间复杂度为O(V + E),其中V是顶点的数量,E是边的数量。
     */
    public static boolean hasEulerianPath(int[][] graph) {
        // 图中顶点的数量
        int n = graph.length;
        // 记录每个顶点的出度
        int[] outDegrees = new int[n];
        // 记录每个顶点的入度
        int[] inDegrees = new int[n];
        // 统计出度与入度不相等的顶点数量
        int count = 0;

        // 统计每个顶点的出度和入度
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                outDegrees[i] += graph[i][j];
                inDegrees[j] += graph[i][j];
            }
            if (outDegrees[i] != inDegrees[i]) {
                count++;
            }
        }

        // 判断是否存在孤立的顶点(出度和入度均为0)
        for (int i = 0; i < n; i++) {
            if (outDegrees[i] == 0 && inDegrees[i] == 0) {
                return false;
            }
        }

        // 判断是否是强连通的图并满足欧拉路径的条件
        boolean isConnected = isStronglyConnected(graph, 0, new boolean[n]);
        if (isConnected && (count == 0 || count == 2)) {
            return true;
        } else {
            return false;
        }
    }

    // 深度优先搜索判断图是否是强连通的
    private static boolean isStronglyConnected(int[][] graph, int v, boolean[] visited) {
        visited[v] = true;
        for (int i = 0; i < graph.length; i++) {
            if (graph[v][i] == 1 && !visited[i]) {
                isStronglyConnected(graph, i, visited);
            }
        }
        for (boolean flag : visited) {
            if (!flag) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        int[][] graph = {
                {0, 1, 0, 0, 1},
                {1, 0, 1, 1, 0},
                {0, 1, 0, 1, 0},
                {0, 1, 1, 0, 0},
                {1, 0, 0, 0, 0}
        };

        boolean hasEulerianPath = hasEulerianPath(graph);
        System.out.println("是否存在欧拉路径: " + hasEulerianPath);
    }

}

14、哈密顿路径(Hamiltonian Path)

题目描述:给定一个无向图,判断是否存在一条哈密顿路径,使得路径经过图中的每个顶点恰好一次。如果存在,返回true;否则,返回false。

请注意,哈密顿路径是一个经典的NP完全问题,因此目前尚没有已知的多项式时间复杂度的解法。最优解法是基于回溯法(Backtracking)的深度优先搜索(DFS)算法,通过枚举所有可能的路径来判断是否存在哈密顿路径。

解法分析

哈密顿路径是一个经典的NP完全问题,因此没有已知的多项式时间复杂度的解法。最优解法是基于回溯法(Backtracking)的深度优先搜索(DFS)算法。

以下是最优解法的分析:

解题思路:

  1. 使用回溯法(Backtracking)和深度优先搜索(DFS)来枚举所有可能的路径。
  2. 从任意一个顶点开始,递归地尝试访问该顶点的所有未访问过的相邻顶点,直到达到路径的长度为图中顶点的数量或无法继续前进为止。
  3. 在搜索过程中,需要记录已经访问过的顶点,并避免重复访问。
  4. 如果在搜索过程中找到了一条包含所有顶点的路径,即存在哈密顿路径,返回true;否则,返回false。

算法实现:

  1. 使用邻接矩阵或邻接表来表示图的连接关系。
  2. 使用一个布尔数组来记录顶点的访问状态。
  3. 从每个顶点开始进行深度优先搜索,判断是否存在哈密顿路径。

由于哈密顿路径是一个NP完全问题,解决大规模的哈密顿路径问题往往是非常耗时的,因此实际应用中常常需要根据具体情况进行问题的简化、剪枝等策略来提高算法的效率。

代码展示

以下是基于回溯法的深度优先搜索(DFS)算法的Java代码示例:

package org.zyf.javabasic.letcode.graph;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author yanfengzhang
 * @description 给定一个无向图,判断是否存在一条哈密顿路径,使得路径经过图中的每个顶点恰好一次。如果存在,返回true;否则,返回false。
 * 请注意,哈密顿路径是一个经典的NP完全问题,因此目前尚没有已知的多项式时间复杂度的解法。
 * 最优解法是基于回溯法(Backtracking)的深度优先搜索(DFS)算法,通过枚举所有可能的路径来判断是否存在哈密顿路径。
 * @date 2021/12/5  23:49
 */
public class HamiltonianPath {

    /**
     * 哈密顿路径是一个经典的NP完全问题,因此没有已知的多项式时间复杂度的解法。最优解法是基于回溯法(Backtracking)的深度优先搜索(DFS)算法。
     * 以下是最优解法的分析:
     * 1. 解题思路:
     * * 使用回溯法(Backtracking)和深度优先搜索(DFS)来枚举所有可能的路径。
     * * 从任意一个顶点开始,递归地尝试访问该顶点的所有未访问过的相邻顶点,直到达到路径的长度为图中顶点的数量或无法继续前进为止。
     * * 在搜索过程中,需要记录已经访问过的顶点,并避免重复访问。
     * * 如果在搜索过程中找到了一条包含所有顶点的路径,即存在哈密顿路径,返回true;否则,返回false。
     * 2. 算法实现:
     * * 使用邻接矩阵或邻接表来表示图的连接关系。
     * * 使用一个布尔数组来记录顶点的访问状态。
     * * 从每个顶点开始进行深度优先搜索,判断是否存在哈密顿路径。
     * 由于哈密顿路径是一个NP完全问题,解决大规模的哈密顿路径问题往往是非常耗时的,因此实际应用中常常需要根据具体情况进行问题的简化、剪枝等策略来提高算法的效率。
     */
    public static boolean hasHamiltonianPath(int[][] graph) {
        // 图中顶点的数量
        int n = graph.length;
        // 记录顶点的访问状态
        boolean[] visited = new boolean[n];
        // 存储哈密顿路径的顶点序列
        List<Integer> path = new ArrayList<>();

        // 从每个顶点开始进行深度优先搜索
        for (int i = 0; i < n; i++) {
            // 初始化访问状态
            Arrays.fill(visited, false);
            // 清空路径
            path.clear();
            // 将当前顶点加入路径
            path.add(i);
            // 标记当前顶点为已访问
            visited[i] = true;
            if (dfs(graph, i, visited, path)) {
                // 如果存在哈密顿路径,返回true
                return true;
            }
        }

        // 未找到哈密顿路径,返回false
        return false;
    }

    private static boolean dfs(int[][] graph, int current, boolean[] visited, List<Integer> path) {
        if (path.size() == graph.length) {
            // 如果路径长度等于顶点数量,说明找到了哈密顿路径
            return true;
        }

        for (int i = 0; i < graph.length; i++) {
            // 如果存在边且未访问过
            if (graph[current][i] == 1 && !visited[i]) {
                // 标记顶点为已访问
                visited[i] = true;
                // 将顶点加入路径
                path.add(i);
                if (dfs(graph, i, visited, path)) {
                    // 继续深度优先搜索
                    return true;
                }
                // 回溯
                // 恢复顶点的访问状态
                visited[i] = false;
                // 移除顶点
                path.remove(path.size() - 1);
            }
        }

        // 未找到哈密顿路径
        return false;
    }

    public static void main(String[] args) {
        int[][] graph = {
                {0, 1, 1, 1, 0},
                {1, 0, 1, 0, 1},
                {1, 1, 0, 1, 0},
                {1, 0, 1, 0, 1},
                {0, 1, 0, 1, 0}
        };

        boolean hasHamiltonianPath = hasHamiltonianPath(graph);
        System.out.println("是否存在哈密顿路径: " + hasHamiltonianPath);
    }
}

15、判断是否为二分图(Is Graph Bipartite?)

题目描述:给定一个无向图,判断它是否是一个二分图。

如果一个图可以被分割成两个独立的子集,且图中的每条边连接两个不同的子集中的节点,则该图是一个二分图。

你可以假设图中没有自环(即图中没有自己连接自己的边)和重复的边。

示例 1:输入: [[1,3], [0,2], [1,3], [0,2]].  输出: true

解释: 无向图如下所示,可以将节点分成两个独立的子集 {0, 2} 和 {1, 3}。

0----1

|\   |

| \  |

|  \ |

3----2

示例 2:输入: [[1,2,3], [0,2], [0,1,3], [0,2]]    输出: false

解释: 无向图如下所示,无法将节点分成两个独立的子集。

0----1

| \  |

|  \ |

3----2

解法分析

判断一个图是否为二分图,可以使用深度优先搜索(DFS)或广度优先搜索(BFS)的方式进行遍历和染色。

具体解决方法如下:

  1. 定义一个数组 colors,用于记录每个节点的染色情况。初始时,将所有节点的颜色设置为未染色,可以用整数 0 表示。
  2. 对于图中的每个节点,如果该节点还未染色,则从该节点开始进行染色操作。
    • 使用 DFS 或 BFS 遍历图的节点,并进行染色:
    • 选择一个初始节点,并将其染色为 1。
    • 遍历该节点的所有相邻节点,如果相邻节点未染色,则将其染色为与当前节点不同的颜色(即 1 的补)。
    • 继续递归或迭代遍历相邻节点的相邻节点,并进行染色操作。
    • 如果在染色过程中,发现相邻节点已经染色,并且颜色与当前节点相同,则说明该图不是二分图,返回 false。
  3. 如果所有节点都被染色并且满足染色规则,则说明该图是二分图,返回 true。

该算法的时间复杂度是 O(V + E),其中 V 表示节点的数量,E 表示边的数量。空间复杂度是 O(V),用于存储节点的染色情况。

代码展示

package org.zyf.javabasic.letcode.graph;

/**
 * @author yanfengzhang
 * @description 给定一个无向图,判断它是否是一个二分图。
 * 如果一个图可以被分割成两个独立的子集,且图中的每条边连接两个不同的子集中的节点,则该图是一个二分图。
 * 你可以假设图中没有自环(即图中没有自己连接自己的边)和重复的边。
 * 示例 1:输入: [[1,3], [0,2], [1,3], [0,2]].  输出: true
 * 解释: 无向图如下所示,可以将节点分成两个独立的子集 {0, 2} 和 {1, 3}。
 * 0----1
 * |\   |
 * | \  |
 * |  \ |
 * 3----2
 * 示例 2:输入: [[1,2,3], [0,2], [0,1,3], [0,2]]    输出: false
 * 解释: 无向图如下所示,无法将节点分成两个独立的子集。
 * 0----1
 * | \  |
 * |  \ |
 * 3----2
 * @date 2022/8/25  23:03
 */
public class IsGraphBipartite {

    /**
     * 判断一个图是否为二分图,可以使用深度优先搜索(DFS)或广度优先搜索(BFS)的方式进行遍历和染色。
     * 具体解决方法如下:
     * 1. 定义一个数组 colors,用于记录每个节点的染色情况。初始时,将所有节点的颜色设置为未染色,可以用整数 0 表示。
     * 2. 对于图中的每个节点,如果该节点还未染色,则从该节点开始进行染色操作。
     * * 使用 DFS 或 BFS 遍历图的节点,并进行染色:
     * * 选择一个初始节点,并将其染色为 1。
     * * 遍历该节点的所有相邻节点,如果相邻节点未染色,则将其染色为与当前节点不同的颜色(即 1 的补)。
     * * 继续递归或迭代遍历相邻节点的相邻节点,并进行染色操作。
     * * 如果在染色过程中,发现相邻节点已经染色,并且颜色与当前节点相同,则说明该图不是二分图,返回 false。
     * 3. 如果所有节点都被染色并且满足染色规则,则说明该图是二分图,返回 true。
     * 该算法的时间复杂度是 O(V + E),其中 V 表示节点的数量,E 表示边的数量。空间复杂度是 O(V),用于存储节点的染色情况。
     */
    public boolean isBipartite(int[][] graph) {
        // 图中节点的数量
        int n = graph.length;
        // 记录节点的染色情况
        int[] colors = new int[n];

        for (int i = 0; i < n; i++) {
            if (colors[i] == 0 && !dfs(graph, i, 1, colors)) {
                return false;
            }
        }

        return true;
    }

    private boolean dfs(int[][] graph, int node, int color, int[] colors) {
        // 将当前节点染色
        colors[node] = color;

        for (int neighbor : graph[node]) {
            if (colors[neighbor] == color) {
                // 如果相邻节点已经染色并且颜色与当前节点相同,则返回 false
                return false;
            }

            if (colors[neighbor] == 0 && !dfs(graph, neighbor, -color, colors)) {

                return false;
            }
        }

        return true;
    }

    public static void main(String[] args) {
        // 定义图的邻接表表示
        int[][] graph = {
                {1, 3},
                {0, 2},
                {1, 3},
                {0, 2}
        };
        // 判断图是否为二分图
        boolean isBipartite = new IsGraphBipartite().isBipartite(graph);

        // 输出结果
        if (isBipartite) {
            System.out.println("The graph is bipartite.");
        } else {
            System.out.println("The graph is not bipartite.");
        }
    }

}

16、用颜色填充区域(Coloring A Border)

题目描述:给定一个二维网格 grid,每个单元格的值表示该位置的颜色。现在,我们要对网格中的一个区域进行填充操作。如果一个单元格与边界相连(上、下、左、右四个方向之一),且其颜色与边界上的单元格颜色不同,那么该单元格应被填充为新的颜色。

要求实现一个函数 colorBorder(int[][] grid, int row, int col, int newColor),其中 grid 表示二维网格,row 和 col 表示要填充的区域的起始位置,newColor 表示新的颜色。函数应该返回填充后的二维网格。

解法分析

最优解法使用深度优先搜索(DFS)来遍历区域,并根据给定的条件进行颜色填充。

具体步骤如下:

  1. 定义一个二维数组 visited,用于记录每个单元格是否已经被访问。
  2. 以 (row, col) 为起始位置进行 DFS 遍历,同时传入原始颜色和新的颜色。
    • 在 DFS 的过程中,检查当前位置是否满足填充条件:
    • 如果当前位置已经被访问过,或者当前位置的颜色与原始颜色不同,或者当前位置是边界位置,则不进行填充操作,直接返回。
    • 否则,将当前位置标记为已访问,并将其颜色填充为新的颜色。
  3. 对当前位置的上、下、左、右四个相邻位置进行递归调用 DFS,以填充其相邻位置。
  4. 如果 DFS 结束后,当前位置是边界位置,则将其颜色填充为新的颜色。

该算法的时间复杂度为 O(R * C),其中 R 和 C 分别是二维网格的行数和列数。

代码展示

package org.zyf.javabasic.letcode.graph;

/**
 * @author yanfengzhang
 * @description 给定一个二维网格 grid,每个单元格的值表示该位置的颜色。
 * 现在,我们要对网格中的一个区域进行填充操作。如果一个单元格与边界相连(上、下、左、右四个方向之一),
 * 且其颜色与边界上的单元格颜色不同,那么该单元格应被填充为新的颜色。
 * 要求实现一个函数 colorBorder(int[][] grid, int row, int col, int newColor),
 * 其中 grid 表示二维网格,row 和 col 表示要填充的区域的起始位置,newColor 表示新的颜色。
 * 函数应该返回填充后的二维网格。
 * @date 2018/6/5  21:07
 */
public class ColoringABorder {
    private int[][] directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

    public int[][] colorBorder(int[][] grid, int row, int col, int newColor) {
        int m = grid.length;
        int n = grid[0].length;
        int originalColor = grid[row][col];
        boolean[][] visited = new boolean[m][n];
        dfs(grid, row, col, originalColor, newColor, visited);
        return grid;
    }

    private void dfs(int[][] grid, int row, int col, int originalColor, int newColor, boolean[][] visited) {
        // 检查越界情况或已经访问过的单元格
        if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length || visited[row][col]) {
            return;
        }

        // 检查是否需要填充边界
        if (row == 0 || row == grid.length - 1 || col == 0 || col == grid[0].length - 1 || grid[row][col] != originalColor) {
            grid[row][col] = newColor;
        }

        visited[row][col] = true;

        // 遍历相邻的四个方向
        for (int[] direction : directions) {
            int newRow = row + direction[0];
            int newCol = col + direction[1];
            dfs(grid, newRow, newCol, originalColor, newColor, visited);
        }
    }

    public static void main(String[] args) {
        int[][] grid = {
                {1, 1, 1},
                {1, 1, 0},
                {1, 0, 1}
        };
        int row = 1;
        int col = 1;
        int newColor = 2;

        int[][] result = new ColoringABorder().colorBorder(grid, row, col, newColor);

        // 输出结果
        for (int[] rowArr : result) {
            for (int cell : rowArr) {
                System.out.print(cell + " ");
            }
            System.out.println();
        }
    }
}

评论 45
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张彦峰ZYF

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值