JavaScript 实现最小生成树算法

啥是生成树?

在图论的数学领域中,如果连通图 G(V,E) 的一个子图是一棵包含 G 的所有顶点及 |V|-1 条边的树,则该子图称为 G(V,E) 的生成树(SpanningTree)。生成树是连通图的包含图中的所有顶点的极小连通子图一个图的生成树可以有多颗。

最小生成树

也称最小权重生成树,在生成树的概念上加一个限制条件,即生成树的所有边的权值总和最小的树,最小生成树也可以有多颗。

应用

  • 乡镇间电缆布线设计
  • 办公楼房网络设计
  • 建筑间电路设计

适用对象

  • 加权无向图
  • 连通图
  • 对于不连通的图,可以求所有是 连通分量 的最小生成树形成最小森林

最小生成树有两个经典的算法: Prim 算法 和 Kruskal 算法。

Prim 算法(节点扩展算法)

基本思想:从图中任意一个顶点开始,每次选择与当前顶点集距离最近的顶点,将对应的边加入到树中,直至所有顶点被处理完。

代码实现:

const INF = Number.MAX_SAFE_INTEGER;
const minKey = (graph, key, visited) => {
  let min = INF;
  let minIndex = 0;
  for (let v = 0; v < graph.length; v++) {
    if (visited[v] === false && key[v] < min) {
      min = key[v];
      minIndex = v;
    }
  }
  return minIndex;
};
export const prim = graph => {
  const parent = [];
  const key = [];
  const visited = [];
  const { length } = graph;
  for (let i = 0; i < length; i++) {
    key[i] = INF;
    visited[i] = false;
  }
  key[0] = 0;
  parent[0] = -1;
  for (let i = 0; i < length - 1; i++) {
    const u = minKey(graph, key, visited);
    visited[u] = true;
    for (let v = 0; v < length; v++) {
      if (graph[u][v] && !visited[v] && graph[u][v] < key[v]) {
        parent[v] = u;
        key[v] = graph[u][v];
      }
    }
  }
  return parent;
};

// test
const graph = [
  [0, 2, 4, 0, 0, 0],
  [2, 0, 2, 4, 2, 0],
  [4, 2, 0, 0, 3, 0],
  [0, 4, 0, 0, 3, 2],
  [0, 2, 3, 3, 0, 2],
  [0, 0, 0, 2, 2, 0]
];

const parent = prim(graph);

console.log('Edge   Weight');
for (let i = 1; i < graph.length; i++) {
  console.log(parent[i] + ' - ' + i + '   ' + graph[i][parent[i]]);
}

// result 
Edge   Weight
0 - 1       2
1 - 2       2
5 - 3       2
1 - 4       2
4 - 5       2

Kruskal 算法(边扩展算法)

与 Prim 算法不同的是,Prim 是以顶点为关键来生成最小树的,而 Kruskal 是以边为关键来生成最小数。

Kruskal 算法的过程:
(1) 将全部边按照权值由小到大排序。
(2) 按顺序(边权由小到大的顺序)考虑每条边,只要这条边和我们已经选择的边不构成环,就保留这条边,否则放弃这条边。

代码实现:

const INF = Number.MAX_SAFE_INTEGER;
const find = (i, parent) => {
  while (parent[i]) {
    i = parent[i];
  }
  return i;
};
const union = (i, j, parent) => {
  if (i !== j) {
    parent[j] = i;
    return true;
  }
  return false;
};
const initializeCost = graph => {
  const cost = [];
  const { length } = graph;
  for (let i = 0; i < length; i++) {
    cost[i] = [];
    for (let j = 0; j < length; j++) {
      if (graph[i][j] === 0) {
        cost[i][j] = INF;
      } else {
        cost[i][j] = graph[i][j];
      }
    }
  }
  return cost;
};
export const kruskal = graph => {
  const { length } = graph;
  const parent = [];
  let ne = 0;
  let a;
  let b;
  let u;
  let v;
  const cost = initializeCost(graph);
  while (ne < length - 1) {
    for (let i = 0, min = INF; i < length; i++) {
      for (let j = 0; j < length; j++) {
        if (cost[i][j] < min) {
          min = cost[i][j];
          a = u = i;
          b = v = j;
        }
      }
    }
    u = find(u, parent);
    v = find(v, parent);
    if (union(u, v, parent)) {
      ne++;
    }
    cost[a][b] = cost[b][a] = INF;
  }
  return parent;
};

// test
const graph = [
  [0, 2, 4, 0, 0, 0],
  [2, 0, 2, 4, 2, 0],
  [4, 2, 0, 0, 3, 0],
  [0, 4, 0, 0, 3, 2],
  [0, 2, 3, 3, 0, 2],
  [0, 0, 0, 2, 2, 0]
];

const parent = prim(graph);

console.log('Edge   Weight');
for (let i = 1; i < graph.length; i++) {
  console.log(parent[i] + ' - ' + i + '   ' + graph[i][parent[i]]);
}

// result 
Edge   Weight
0 - 1       2
1 - 2       2
5 - 3       2
1 - 4       2
4 - 5       2

Kruskal 和Prim 算法对比:
Kruskal 算法主要针对边展开,时间复杂度为 O(elog e),e为图的边数,Prim 算法的时间复杂度为 O(n²),n 为最小生成树的边数。所以,边数少(稀疏图)用Kruskal 算法,边数多(稠密图)用 Prim 算法。

最小生成树各算法可视化演示
最小生成树动画演示--canvas 实现

你可能感兴趣的:(JavaScript 实现最小生成树算法)