有图才有真相

416 阅读9分钟

图一般分为有向图和无向图两种

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
解释:从02有一条路径,但是从01,发现1的出度是0,但是1不等于2

例子2

输入:n = 4, edges = [[0,1],[0,3],[1,2],[2,1]], source = 0, destination = 3
输出:false
解释:因为这里存在一个循环从12,从21,所以返回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;
};