图
图一般分为有向图和无向图两种
785. 判断二分图
题目描述
给定一个无向图graph,当这个图为二分图时返回true。
如果我们能将一个图的节点集合分割成两个独立的子集A和B,并使图中的每一条边的两个节点一个来自A集合,一个来自B集合,我们就将这个图称为二分图。
graph将会以邻接表方式给出,graph[i]表示图中与节点i相连的所有节点。每个节点都是一个在0到graph.length-1之间的整数。这图中没有自环和平行边: graph[i] 中不存在i,并且graph[i]中没有重复的值。
例子1
示例 1:
输入: [[1,3], [0,2], [1,3], [0,2]]
输出: true
解释:
无向图如下:
0----1
| |
| |
3----2
我们可以将节点分成两组: {0, 2} 和 {1, 3}。
例子2
示例 2:
输入: [[1,2,3], [0,2], [0,1,3], [0,2]]
输出: false
解释:
无向图如下:
0----1
| \ |
| \ |
3----2
我们不能将节点分割成两个独立的子集。
注意:
1 graph 的长度范围为 [1, 100]。
2 graph[i] 中的元素的范围为 [0, graph.length - 1]。
3 graph[i] 不会包含 i 或者有重复的值。
4 图是无向的: 如果j 在 graph[i]里边, 那么 i 也会在 graph[j]里边。
思考
1 图一般是使用深度和广度来解决。
这里的关键是考虑如何把问题转换一下,否则感觉还是特别麻烦?
看了题解,这里转成了类似染色,把整个图染成两种颜色,那么就是可以转成两个部分,如果不能分别染成两个颜色,那么就是不能分成两个部分。
深度参考实现1
广度参考实现2
实现1
/**
* @param {number[][]} graph
* @return {boolean}
*/
// 把node染成红色或者黄色
const isValidColor = (graph, colors, color, node) => {
// 如果已经被染色过了
if (colors[node] !== 0) {
// 看下是不是和需要染的颜色一样
return colors[node] === color;
}
// 如果没有被染色过,则把节点染成颜色
colors[node] = color;
// 然后把相邻的节点都染成颜色
for (let next of graph[node]) {
if (!isValidColor(graph, colors, -color, next)) {
return false;
}
}
return true;
};
// Runtime: 88 ms, faster than 70.08% of JavaScript online submissions for Is Graph Bipartite?.
// Memory Usage: 41.1 MB, less than 79.22% of JavaScript online submissions for Is Graph Bipartite.
export default (graph) => {
const len = graph.length;
// 0没有被染色过,1 染成红色, -1 染成黄色
const colors = new Array(len).fill(0);
for (let i = 0; i < len; i++) {
if (colors[i] === 0 && !isValidColor(graph, colors, 1, i)) {
return false;
}
}
return true;
};
实现2
/**
* @param {number[][]} graph
* @return {boolean}
*/
// Runtime: 84 ms, faster than 85.87% of JavaScript online submissions for Is Graph Bipartite?.
// Memory Usage: 41.7 MB, less than 39.34% of JavaScript online submissions for Is Graph Bipartite?.
export default (graph) => {
const len = graph.length;
// 0没有被染色过,1 染成红色, -1 染成黄色
const colors = new Array(len).fill(0);
for (let i = 0; i < len; i++) {
if (colors[i] !== 0) continue;
const queue = [];
queue.push(i);
colors[i] = 1;
while (queue.length) {
const cur = queue.shift();
for (let next of graph[cur]) {
if (colors[next] === 0) {
colors[next] = -colors[cur];
queue.push(next);
} else if (colors[next] != -colors[cur]) {
return false;
}
}
}
}
return true;
};
210. 课程表 II
题目描述
现在你总共有 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] 。
说明:
1 输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
2 你可以假定输入的先决条件中没有重复的边。
注意:
1 这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
2 通过 DFS 进行拓扑排序 - 一个关于Coursera的精彩视频教程(21分钟),介绍拓扑排序的基本概念。
3 拓扑排序也可以通过 BFS 完成。
思考
1 这是典型的拓扑排序,只不过需要复习一下入度,出度的概念,如果一个节点的入度为0,那么肯定可以放入到数组中去
参考实现1
实现1
/**
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {number[]}
*/
// Runtime: 136 ms, faster than 29.13% of JavaScript online submissions for Course Schedule II.
// Memory Usage: 44.3 MB, less than 49.13% of JavaScript online submissions for Course Schedule II.
export default (numCourses, prerequisites) => {
const inDegrees = new Array(numCourses).fill(0);
for (const [val] of prerequisites) {
// 获取每个节点的入度
inDegrees[val]++;
}
const queue = [];
for (let i = 0; i < inDegrees.length; i++) {
if (inDegrees[i] === 0) {
// 把入度为0的节点加入到队列中
queue.push(i);
}
}
const res = [];
while (queue.length) {
const first = queue.shift();
// 节点的个数减去一
numCourses--;
res.push(first);
for (const [val0, val1] of prerequisites) {
if (val1 === first) {
--inDegrees[val0];
if (inDegrees[val0] === 0) {
queue.push(val0);
}
}
}
}
return numCourses === 0 ? res : [];
};
1059. 是否所有的路径从起点到终点
题目描述
给出一个有向边集合graph,和两个节点起始点source和结束点destination,求出是否所有的路径都可以从起始点source到结束点destination,如果符合这三种情况就返回true,否则返回false
1 至少有一条路径从起始点到结束点
2 如果从起始点到达一个出度为0的节点,那么这个节点肯定是结束点
3 从起始点到终点的路径数目是有限的。
例子1
输入:n = 3, edges = [[0,1],[0,2]], source = 0, destination = 2
输出:false
解释:从0到2有一条路径,但是从0到1,发现1的出度是0,但是1不等于2
例子2
输入:n = 4, edges = [[0,1],[0,3],[1,2],[2,1]], source = 0, destination = 3
输出:false
解释:因为这里存在一个循环从1到2,从2到1,所以返回false
思考
1 很简单,直接广度遍历,把是环的情况和到达一个节点但是不是终点的情况就可以了
参考实现1
实现1
export default (n, edges, source, destination) => {
const graph = new Array(n);
for (let i = 0; i < n; i++) {
graph[i] = [];
}
const inDegrees = new Array(n).fill(0);
for (let [key, val] of edges) {
graph[key].push(val);
++inDegrees[val];
}
const queue = [source];
while (queue.length) {
const currNode = queue.shift();
if (graph[currNode].length === 0 && currNode !== destination) {
return fasle;
}
for (let node of graph[currNode]) {
if (inDegrees[node] < 0) {
return false;
}
--inDegrees[node];
queue.push(node);
}
}
return true;
};
1135. 最低成本联通所有城市
题目描述
有标记为从1到n的n个城市,然后给予一个connections,每个connections[i] = [city1, city2, cost]表示从city1到city2的代价是cost和从city2到city1的代价是cost
返回一个可以联通所有城市需要花费的最小代价,如果不能联通所有城市,则返回-1
例子1
输入:N = 3, connections = [[1,2,5],[1,3,6],[2,3,1]]
输出:6
解释:选择从1===2===3
例子2
输入:N = 4, connections = [[1,2,3],[3,4,4]]
输出:-1
解释:无法联通所有城市
思考
1 这是典型的求最小生成树的算法
prim 算法,这里算法很简单,切人的角度是从节点出发,首先任意选择一个节点,放入visited,然后选择所有和visited里边节点联通的代价最小的节点再加入到visited里边,一直循环。
参考实现1
2 克鲁斯卡尔( kruskal )算法
这个算法切人的角度是从边入手,首先选择所有边中代价最小的边,然后把边的两个节点加入到已经访问过的节点集合,然后继续寻找下一个边中代价最小的边,再把边的两个点再加入到已经访问过的集合里边,如果节点已经访问完了,则返回-1,如果没有,则继续寻找下一个代价最小的边,一直循环。
实现1
export default (n, connections) => {
let res = 0;
const visited = [1];
while (visited.length !== n) {
let min = Number.MAX_VALUE;
let minNode;
for (let i = 0; i < visited.length; i++) {
for (let conn of connections) {
const a = conn[0];
const b = conn[1];
const cost = conn[2];
if (visited[i] === a && !visited.includes(b)) {
if (cost < min) {
min = cost;
minNode = b;
}
} else if (visited[i] === b && !visited.includes(a)) {
if (cost < min) {
min = cost;
minNode = a;
}
}
}
}
if (minNode) {
visited.push(minNode);
res += min;
} else {
return -1;
}
}
return res;
};
实现2
class Uf {
constructor(n) {
this.parent = new Array(n + 1).fill(0);
this.size = new Array(n + 1).fill(0);
for (let i = 0; i <= n; i++) {
this.parent[i] = i;
this.size[i] = 1;
}
this.count = n;
}
// 发现父元素,连成一个链表
find(i) {
if (i !== this.parent[i]) {
this.parent[i] = this.find(this.parent[i]);
}
return this.parent[i];
}
// 连接起来
union(i, j) {
// 判断谁是谁的父元素,谁的比重大谁就是父元素
if (this.size[i] > this.size[j]) {
this.parent[j] = i;
this.size[i] += this.size[j];
} else {
this.parent[i] = j;
this.size[j] += this.size[i];
}
this.count--;
}
}
export default (n, connections) => {
connections.sort((a, b) => a[2] - b[2]);
let res = 0;
const uf = new Uf(n);
for (let conn of connections) {
const a = conn[0];
const b = conn[1];
const cost = conn[2];
const pa = uf.find(a);
const pb = uf.find(b);
if (pa !== pb) {
uf.union(pa, pb);
res += cost;
}
if (uf.count === 1) return res;
}
return -1;
};
882. 细分图中的可到达结点
题目描述
给你一个无向图(原始图),图中有 n 个节点,编号从 0 到 n - 1 。你决定将图中的每条边细分为一条节点链,每条边之间的新节点数各不相同。
图用由边组成的二维数组 edges 表示,其中 edges[i] = [ui, vi, cnti] 表示原始图中节点 ui 和 vi 之间存在一条边,cnti 是将边细分后的新节点总数。注意,cnti == 0 表示边不可细分。
要细分边 [ui, vi] ,需要将其替换为 (cnti + 1) 条新边,和 cnti 个新节点。新节点为 x1, x2, ..., xcnti ,新边为 [ui, x1], [x1, x2], [x2, x3], ..., [xcnti+1, xcnti], [xcnti, vi] 。
现在得到一个新的 细分图 ,请你计算从节点 0 出发,可以到达多少个节点?节点 是否可以到达的判断条件 为:如果节点间距离是 maxMoves 或更少,则视为可以到达;否则,不可到达。
给你原始图和 maxMoves ,返回新的细分图中从节点 0 出发 可到达的节点数 。
例子1
输入:edges = [[0,1,10],[0,2,1],[1,2,2]], maxMoves = 6, n = 3
输出:13
解释:边的细分情况如上图所示。
可以到达的节点已经用黄色标注出来。
例子2
输入:edges = [[0,1,4],[1,2,6],[0,2,8],[1,3,1]], maxMoves = 10, n = 4
输出:23
例子3
输入:edges = [[1,2,4],[1,4,5],[1,3,1],[2,3,4],[3,4,5]], maxMoves = 17, n = 5
输出:1
解释:节点 0 与图的其余部分没有连通,所以只有节点 0 可以到达。
提示:
0 <= edges.length <= min(n * (n - 1) / 2, 104)
edges[i].length == 3
0 <= ui < vi < n
图中 不存在平行边
0 <= cnti <= 10^4
0 <= maxMoves <= 10^9
1 <= n <= 3000
思考
1 Dijkstra 无负边单源最短路算法,感兴趣的可以自己去看下。
在本题目中,首先找到所有节点到0节点的最短距离,如果最短距离小于maxMoves,则说明该节点可以到达。
然后再看下所有的边,看下每条边的两个节点,如果这条边的两个顶点可以到达,然后肯定可以访问到这条边上的节点,
参考实现1
实现1
/**
* @param {number[][]} edges
* @param {number} maxMoves
* @param {number} n
* @return {number}
*/
class Heap {
constructor() {
this.heap = [];
}
get length() {
return this.heap.length;
}
compare(i, j) {
if (!this.heap[j]) return false;
return this.heap[i][1] > this.heap[j][1];
}
swap(i, j) {
const temp = this.heap[i];
this.heap[i] = this.heap[j];
this.heap[j] = temp;
}
insert(num) {
this.heap.push(num);
let idx = this.length - 1;
let parent = (idx - 1) >> 1;
// 如果没有到达终点
while (idx !== 0 && this.compare(parent, idx)) {
this.swap(parent, idx);
idx = parent;
parent = (idx - 1) >> 1;
}
}
remove() {
if (this.length === 1) return this.heap.pop();
let res = this.heap[0],
idx = 0,
left = 1 | (idx << 1),
right = (1 + idx) << 1;
this.heap[0] = this.heap.pop();
while (this.compare(idx, left) || this.compare(idx, right)) {
if (this.compare(left, right)) {
this.swap(idx, right);
idx = right;
} else {
this.swap(idx, left);
idx = left;
}
left = 1 | (idx << 1);
right = (1 + idx) << 1;
}
return res;
}
}
export default (edges, maxMoves, n) => {
let res = 0;
// 最小堆
const priorityQueue = new Heap();
const visited = new Array(n).fill(0);
const graph = new Array(n);
for (let i = 0; i < n; i++) {
graph[i] = [];
}
const distance = new Array(n).fill(Number.MAX_SAFE_INTEGER);
for (let i = 0; i < edges.length; i++) {
// 把两者的距离push进去
graph[edges[i][0]].push([edges[i][1], edges[i][2]]);
graph[edges[i][1]].push([edges[i][0], edges[i][2]]);
}
distance[0] = 0;
priorityQueue.insert([0, distance[0]]);
while (priorityQueue.length != 0) {
let cur = priorityQueue.remove();
const curNode = cur[0];
// 如果已经访问过了,则执行下一个循环
if (visited[curNode] === 1) continue;
if (distance[curNode] <= maxMoves) res++;
visited[curNode] = 1;
for (let i of graph[curNode]) {
// 发现从0节点到所有节点的最小距离
if (distance[i[0]] > distance[curNode] + i[1] + 1) {
distance[i[0]] = distance[curNode] + i[1] + 1;
priorityQueue.insert([i[0], distance[i[0]]]);
}
}
}
// 能到到的节点前面在while循环里边已经统计完了,现在需要统计各个边上可以到达的节点
for (let i = 0; i < edges.length; i++) {
const a = maxMoves - distance[edges[i][0]] >= 0 ? maxMoves - distance[edges[i][0]] : 0;
const b = maxMoves - distance[edges[i][1]] >= 0 ? maxMoves - distance[edges[i][1]] : 0;
res += Math.min(edges[i][2], a + b);
}
return res;
};