785. 判断二分图(Medium)
给定一个无向图graph,当这个图为二分图时返回true。
如果我们能将一个图的节点集合分割成两个独立的子集A和B,并使图中的每一条边的两个节点一个来自A集合,一个来自B集合,我们就将这个图称为二分图。
graph将会以邻接表方式给出,graph[i]表示图中与节点i相连的所有节点。每个节点都是一个在0到graph.length-1之间的整数。这图中没有自环和平行边: graph[i] 中不存在i,并且graph[i]中没有重复的值。
示例 1:
输入: [[1,3], [0,2], [1,3], [0,2]]
输出: true
解释:
无向图如下:
0----1
| |
| |
3----2
我们可以将节点分成两组: {0, 2} 和 {1, 3}。
示例 2:
输入: [[1,2,3], [0,2], [0,1,3], [0,2]]
输出: false
解释:
无向图如下:
0----1
| \ |
| \ |
3----2
我们不能将节点分割成两个独立的子集。
注意:
- graph 的长度范围为 [1, 100]。
- graph[i] 中的元素的范围为 [0, graph.length - 1]。
- graph[i] 不会包含 i 或者有重复的值。
- 图是无向的: 如果j 在 graph[i]里边, 那么 i 也会在 graph[j]里边。
bfs染色法
从某个节点开始,将这个节点染成白色,然后遍历这个节点的所有相邻节点,如果是二分图,相邻节点的颜色都应该不同。
- 如果相邻节点无色,则染成黑色
- 如果相邻节点是黑色,那么该图不是二分图
- 如果相邻节点为白色,不变 当遍历完所有节点的时候,所有相邻节点都是不同颜色,则该图为二分图
public boolean isBipartite(int[][] graph) {
// 0表示未访问,1或者-1表示两种不同的颜色
int[] visited = new int[graph.length];
Queue<Integer> queue = new LinkedList<>();
// 图中可能存在多个连通域,所以需要判断是否存在顶点未被访问,如果存在就进行一轮 BFS 染色
for (int i = 0; i < graph.length; i++) {
if (visited[i] != 0) {
continue;
}
queue.offer(i);
visited[i] = 1;
// 出队一个节点,就将其所有相连节点染成相反的颜色,并入队
while (!queue.isEmpty()) {
Integer v = queue.poll();
for (int w : graph[v]) {
// 如果当前顶点的某个相连节点已染过色,且和顶点颜色相同,说明不是二分图
if (visited[v] == visited[w]) {
return false;
}
if (visited[w] == 0) {
visited[w] = -visited[v];
queue.offer(w);
}
}
}
}
return true;
}
207. 课程表(Medium)
你这个学期必须选修 numCourse
门课程,记为 0
到 numCourse-1
。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0
,你需要先完成课程 1
,我们用一个匹配来表示他们:[0,1]
给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?
示例 1:
输入: 2, [[1,0]]
输出: true
解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。
示例 2:
输入: 2, [[1,0],[0,1]]
输出: false
解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。
提示:
- 输入的先决条件是由 边缘列表 表示的图形,而不是 邻接矩阵 。详情请参见图的表示法。
- 你可以假定输入的先决条件中没有重复的边。
1 <= numCourses <= 10^5
深度优先遍历
当一个课程a是另外一个课程b的学习前提,课程b又是课程a的学习前提的时候,是不可能完成所有的课程学习的,所以关键是判断是否为有向无环图。具体思路如下:
- 构建一个数组
flags
记录课程的状态:- 未被 DFS 访问:
i == 0
; - 已被其他节点启动的 DFS 访问:
i == -1
; - 已被当前节点启动的 DFS 访问:
i == 1
。
- 未被 DFS 访问:
- 对所有的课程进行 DFS,判断每个课程起点的 DFS 是否有环:
- 终止条件:
- 当标志为 1 的时候,说明当前课程在本轮 DFS 被第二次访问,有环
- 当标志为 -1,说明当前课程在其他轮 DFS 访问过,不存在环,直接返回 true
- 把当前课程的标志设为 1,标志被本轮 DFS 访问过
- 递归访问当前节点
i
的所有邻接节点j
,当发现环直接返回false
; - 当前节点的所有节点都被遍历,没有发现环,将当前节点标志设为 1 并返回
true
- 终止条件:
- 整个课程图没有环,返回
true
public boolean canFinish(int numCourses, int[][] prerequisites) {
if (numCourses <= 0) {
return false;
}
int plen = prerequisites.length;
if (plen == 0) {
return true;
}
// 初始化有向图
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}
int[] flags = new int[numCourses];
// 有向图的key前驱节点,value是后继节点的结合
for (int[] cp : prerequisites) {
graph.get(cp[1]).add(cp[0]);
}
for (int i = 0; i < numCourses; i++) {
if (dfs(i, graph, flags)) {
return false;
}
}
return true;
}
/**
*
* @param i 当前访问的课程结点
* @param graph
* @param flags 如果 == 1 表示正在访问中,== -1表示已经访问过了
* @return true表示有环
*/
private boolean dfs(int i, List<List<Integer>> graph, int[] flags) {
if (flags[i] == 1) {
return true;
}
if (flags[i] == -1) {
return false;
}
// 表示正在访问中
flags[i] = 1;
// 寻找后继节点是否有环
for (Integer j : graph.get(i)) {
if (dfs(j, graph, flags)) {
return false;
}
}
// i 的所有后继结点都访问完了,都没有存在环,则这个结点就可以被标记为已经访问结束
flags[i] = -1;
return false;
}
拓扑排序
拓扑排序实际上应用的是贪心算法,每一步最优,则最后的结果就是最优的。
具体到拓扑排序就是每一步都删除入度为 0 的节点,依次得到的结点序列就是拓扑排序的结点序列。如果最后还剩下有节点,就说明不能完成全部课程的学习。
流程:
- 开始排序前,扫描数组,填充邻接表和入度数组,将入度为 0 的节点加入到队列(为什么使用队列)
如果不使用队列,要想得到当前入度为 00 的结点,就得遍历一遍入度数组。使用队列即用空间换时间。
- 只要队列非空,就从队列取出节点,将这个节点输出到结果集中,并将这个节点的所有相邻节点入度减 1,如果相邻节点减 1 之后入度为0,则加入队列
- 队列为空的时候,检查结果集的数量和节点个数是否相等
public boolean canFinish(int numCourses, int[][] prerequisites) {
if (numCourses <= 0) {
return false;
}
int plen = prerequisites.length;
if (plen == 0) {
return true;
}
int[] inDegree = new int[numCourses];
HashSet<Integer>[] adj = new HashSet[numCourses];
for (int i = 0; i < numCourses; i++) {
adj[i] = new HashSet<>();
}
// 填充邻接表和入度数组
for (int[] p : prerequisites) {
inDegree[p[0]]++;
adj[p[1]].add(p[0]);
}
Queue<Integer> queue = new LinkedList<>();
// 首先加入入度为0的节点
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) {
queue.add(i);
}
}
// 记录已经出队的数量
int cnt = 0;
while (!queue.isEmpty()) {
Integer top = queue.poll();
cnt += 1;
// 遍历出队的所有后继节点
for (int successor : adj[top]) {
inDegree[successor]--;
if (inDegree[successor] == 0) {
queue.add(successor);
}
}
}
return cnt == numCourses;
}
210. 课程表 II(Medium)
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。
可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
示例 1:
输入: 2, [[1,0]]
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:
输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
说明:
- 输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
- 你可以假定输入的先决条件中没有重复的边。
拓扑排序
根据拓扑排序的应用:一般用来“排序”具有依赖关系的任务可知,本题适合使用拓扑排序
解决,拓扑排序的详细解法见上题的BFS拓扑排序
public int[] findOrder(int numCourses, int[][] prerequisites) {
if (numCourses <= 0) {
return new int[0];
}
// index: 节点; value:与节点相连的后继节点
HashSet<Integer>[] adj = new HashSet[numCourses];
for (int i = 0; i < numCourses; i++) {
adj[i] = new HashSet<>();
}
// [1,0] 0->1
// index:节点; value:入度
int[] inDegree = new int[numCourses];
for (int[] p : prerequisites) {
adj[p[1]].add(p[0]);
inDegree[p[0]]++;
}
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) {
queue.offer(i);
}
}
int[] res = new int[numCourses];
// 当前结果集列表里的元素个数,正好可以作为下标
int count = 0;
while (!queue.isEmpty()) {
Integer top = queue.poll();
res[count++] = top;
// 将所有相连的节点入度减1,减完之后的节点入度如果为0,加入队列
Set<Integer> successors = adj[top];
for (Integer nextCourse : successors) {
inDegree[nextCourse]--;
if (inDegree[nextCourse] == 0) {
queue.offer(nextCourse);
}
}
}
if (count == numCourses) {
return res;
}
return new int[0];
}
684. 冗余连接(Medium)
在本问题中, 树指的是一个连通且无环的无向图。
输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组。每一个边的元素是一对[u, v] ,满足 u < v,表示连接顶点u 和v的无向图的边。
返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v。
示例 1:
输入: [[1,2], [1,3], [2,3]]
输出: [2,3]
解释: 给定的无向图为:
1
/ \
2 - 3
示例 2:
输入: [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]
解释: 给定的无向图为:
5 - 1 - 2
| |
4 - 3
注意:
- 输入的二维数组大小在 3 到 1000。
- 二维数组中的整数在1到N之间,其中N是输入数组的大小。
并查集
并查集的主要用于处理连接问题,判断两点之间有无连接。对于无环图来说,每合并一个点上去,都是不同的根在合并,而一旦出现合并的两个点具有同一个根时,说明他俩早已在同一条路径中,故而他两肯定形成环路。所以只需要遍历边,将所有的点合并,返回最后拥有相同根的两个点即可。
int[] result = new int[2];
public int[] findRedundantConnection(int[][] edges) {
// index: 当前节点 vector:当前节点所在集合的代表节点
int[] father = new int[edges.length + 1];
// 初始化每个节点的代表节点为自身
for (int i = 0; i < father.length; i++) {
father[i] = i;
}
for (int[] edge : edges) {
union(father, edge[0], edge[1]);
}
return result;
}
// 路径压缩,即找到 x 的根节点
private int findXFather(int[] father, int x) {
while (father[x] != x) {
father[x] = father[father[x]];
x = father[x];
}
return x;
}
// 合并两个能连接上的点,father合并为最后确定的father
private void union(int[] father, int x, int y) {
// 找到两节点的根节点
int xFather = findXFather(father, x);
int yFather = findXFather(father, y);
// 两节点所在集合的代表节点相同表示出现了环,更新结果集
if (xFather == yFather) {
result[0] = x;
result[1] = y;
} else {
father[xFather] = yFather;
}
}