blog

"データ構造" JavaScriptでのグラフ実装

グラフは有限個のノードとそれらを結ぶ辺で構成されます。 ノード間を結ぶ辺は双方向。ノードとノードを結ぶ辺は単方向です。ノード1からノード2にしか行けず、ノード2からノード1に行くことはできません。 各...

Feb 13, 2020 · 10 min. read
シェア

グラフとは

グラフは有限個のノードとそれらを結ぶエッジで構成されます。

無向グラフ

ノード間のエッジは双方向です。ノード1はノード2へ、ノード2はノード1へトラバースできます。

有向グラフ

ノード間を結ぶエッジは一方通行です。ノード1からノード2にしか行くことができず、ノード2からノード1に行くことはできません。

グラフの表現方法

隣接結合表

各ノードを格納するために配列を使用し、配列の長さはノードの数です。配列の各ビットには、i 番目のノードに隣接するノードのリストが格納されます。

コロケーション行列

matrix[i][j] は、ノード i から j がエッジを持つことを表します。

無向グラフの実装

隣接表の実装

/**
 *  
 */
class Graph {
 constructor () {
 // key ~ ノード
 // value ~ 隣接ノードのリスト
 this.adjacencyList = new Map();
 }
 /**
 * ノードを追加する
 */
 addNode (node) {
 if (!this.adjacencyList.has(node)) {
 this.adjacencyList.set(node, []);
 } else {
 throw new Error('ノードはすでに存在する')
 }
 }
 /**
 *  
 * to  
 * form  
 */
 addEdge (to, from) {
 if (
 this.adjacencyList.has(to) &&
 this.adjacencyList.has(from)
 ) {
 // 重み付けを取り除く
 let toAdjacent = this.adjacencyList.get(to)
 let fromAdjacent = this.adjacencyList.get(from)
 toAdjacent = [...new Set([...toAdjacent, from])];
 fromAdjacent = [... new Set([...fromAdjacent, to])];
 // 辺が2辺を結ぶ無向グラフ
 this.adjacencyList.set(to, toAdjacent);
 this.adjacencyList.set(from, fromAdjacent); 
 }
 }
 print () {
 const nodes = this.adjacencyList.keys();
 for (let node of nodes) {
 const neighbors = this.adjacencyList.get(node);
 const neighborsString = neighbors.join(' ');
 console.log(` ${node}, 隣接ノード${neighborsString}`);
 }
 }
}

グラフの作成例


const graph = new Graph()
// ノードを追加する
graph.addNode('A')
graph.addNode('B')
graph.addNode('C')
graph.addNode('D')
graph.addNode('E')
graph.addNode('F')
//  
graph.addEdge('A', 'B')
graph.addEdge('A', 'D')
graph.addEdge('A', 'E')
graph.addEdge('B', 'A')
graph.addEdge('B', 'C')
graph.addEdge('C', 'B')
graph.addEdge('C', 'E')
graph.addEdge('C', 'F')
graph.addEdge('D', 'A')
graph.addEdge('D', 'E')
graph.addEdge('E', 'A')
graph.addEdge('E', 'C')
graph.addEdge('E', 'D')
graph.addEdge('E', 'F')
graph.addEdge('F', 'C')
graph.addEdge('F', 'E')
//  
graph.print()
// ノードA、隣接ノードB D E
// ノードB、隣接ノードA C
// ノードC、隣接ノードB E F
// ノードD、隣接ノードA E
// ノードE、隣接ノードA C D F
// ノードF、隣接ノードC E

グラフの幅優先走査


/**
 *  
 */
class Graph {
 
 // ...
 /**
 * グラフを幅優先で走査する
 * node 探索の開始点
 */
 bfs (node) {
 if (this.adjacencyList.has(node)) {
 const result = [];
 const hash = {};
 const queue = [];
 const nodes = this.adjacencyList.keys();
 for (let node of nodes) {
 hash[node] = false;
 }
 hash[node] = true;
 queue.push(node);
 result.push(node);
 while (queue.length > 0) {
 let q = queue.shift();
 let list = this.adjacencyList.get(q);
 for (let i = 0; i < list.length; i++) {
 let temp = list[i]
 if (!hash[temp]) {
 result.push(temp)
 hash[temp] = true
 queue.push(temp)
 }
 }
 }
 return result;
 }
 return null
 }
}

グラフの深さ優先走査

/**
 *  
 */
class Graph {
 
 // ...
 /**
 * グラフを深さ優先で走査する
 * node 探索の開始点
 */
 dfs (node) {
 const result = [];
 const hash = {};
 const nodes = this.adjacencyList.keys();
 for (let node of nodes) {
 hash[node] = false;
 }
 const DFS = (node) => {
 let list = this.adjacencyList.get(node);
 for (let i = 0; i < list.length; i++) {
 let temp = list[i];
 if (!hash[temp]) {
 hash[temp] = true;
 result.push(temp);
 DFS(temp);
 }
 }
 }
 if (this.adjacencyList.has(node)) {
 hash[node] = true;
 result.push(node);
 DFS(node);
 return result;
 }
 return null;
 }
}

コロケーション行列の実装

/**
 *  
 */
class Graph {
 constructor () {
 this.matrix = [];
 }
 /**
 * ノードを追加する
 */
 addNode () {
 this.matrix.push([]);
 const L = this.matrix.length;
 for (let i = 0; i < L; i++) {
 let start = this.matrix[i].length;
 while (start < L) {
 this.matrix[i][start] = 0;
 start += 1
 }
 }
 }
 /**
 *  
 */
 addEdge (to, from) {
 if (
 (to < this.matrix.length && to >= 0) && 
 (from < this.matrix.length && from >= 0)
 ) {
 this.matrix[to][from] = 1;
 this.matrix[from][to] = 1;
 }
 }
 print () {
 const L = this.matrix.length;
 for (let i = 0; i < L; i++) {
 const current = i;
 let neighbors = this.matrix[i].map((m, index) => {
 if (
 index !== i &&
 m === 1
 ) {
 return index;
 }
 return null;
 });
 neighbors = neighbors.filter(n => n !== null);
 const neighborsString = neighbors.join(' ');
 console.log(` ${current}, 隣接ノード${neighborsString}`);
 }
 }
}

グラフの幅優先走査


/**
 *  
 */
class Graph {
 // .... 
 
 bfs (node) {
 if (node >= 0 && node < this.matrix.length) {
 const result = [];
 const hash = {};
 const queue = [];
 for (let i = 0; i < this.matrix.length; i++) {
 hash[i] = false;
 }
 hash[node] = true;
 result.push(node);
 queue.push(node);
 while(queue.length > 0) {
 let q = queue.shift();
 let list = this.matrix[q];
 for (let i = 0; i < list.length; i++) {
 if (
 list[i] === 1 &&
 !hash[i]
 ) {
 result.push(i);
 hash[i] = true;
 queue.push(i);
 }
 }
 }
 return result;
 }
 return -1;
 }
 dfs (node) {
 if (node >= 0 && node < this.matrix.length) {
 }
 }
}

グラフの深さ優先走査


/**
 *  
 */
class Graph {
 // ... 
 dfs (node) {
 if (node >= 0 && node < this.matrix.length) {
 const result = [];
 const hash = {};
 for (let i = 0; i < this.matrix.length; i++) {
 hash[i] = false;
 }
 hash[node] = true;
 result.push(node);
 const DFS = (node) => {
 let list = this.matrix[node];
 for (let i = 0; i < list.length; i++) {
 if (
 list[i] === 1 &&
 !hash[i]
 ) {
 result.push(i);
 hash[i] = true;
 DFS(i);
 }
 }
 };
 DFS(node);
 return result;
 }
 return -1;
 }
}

有向グラフの実装

カラーテーブルによる有向グラフの実現

/**
 *  
 */
class Graph {
 constructor () {
 this.adjacencyList = new Map();
 }
 addNode (node) {
 if (!this.adjacencyList.has(node)) {
 this.adjacencyList.set(node, []);
 } else {
 throw new Error('ノードはすでに存在する')
 }
 }
 addEdge (to, from) {
 if (
 this.adjacencyList.has(to) &&
 this.adjacencyList.has(from)
 ) {
 let toAdjacent = this.adjacencyList.get(to)
 toAdjacent = [...new Set([...toAdjacent, from])];
 // 有向グラフ。> from
 this.adjacencyList.set(to, toAdjacent);
 } 
 }
 print () {
 const nodes = this.adjacencyList.keys();
 for (let node of nodes) {
 const neighbors = this.adjacencyList.get(node);
 const neighborsString = neighbors.join(' ');
 console.log(` ${node}, 隣接ノード${neighborsString}`);
 }
 }
}

コロケーション行列を用いた有向グラフの実現


/**
 *  
 */
class Graph {
 constructor () {
 this.matrix = [];
 }
 addNode () {
 this.matrix.push([]);
 const L = this.matrix.length;
 for (let i = 0; i < L; i++) {
 let start = this.matrix[i].length;
 while (start < L) {
 this.matrix[i][start] = 0;
 start += 1
 }
 }
 }
 addEdge (to, from) {
 if (
 (to < this.matrix.length && to >= 0) && 
 (from < this.matrix.length && from >= 0)
 ) {
 this.matrix[to][from] = 1;
 }
 }
 print () {
 const L = this.matrix.length;
 for (let i = 0; i < L; i++) {
 const current = i;
 let neighbors = this.matrix[i].map((m, index) => {
 if (
 index !== i &&
 m === 1
 ) {
 return index;
 }
 return null;
 });
 neighbors = neighbors.filter(n => n !== null);
 const neighborsString = neighbors.join(' ');
 console.log(` ${current}, 隣接ノード${neighborsString}`);
 }
 }
}

加重グラフの実装

辺に重みを持つグラフ

襟テーブル実現の加重図

各ノードを格納するために配列が使用され、配列の長さはノードの数です。配列の長さはノードの数で、配列の各ビットには、i 番目のノードに隣接するノードのリストと、現在のノードに隣接する各ノードの重みが格納されます。


class Graph {
 constructor () {
 // key ~ ノード
 // [value, weight] ~ 隣接ノードと重みのリスト
 this.adjacencyList = new Map();
 }
 addNode (node) {
 if (!this.adjacencyList.has(node)) {
 this.adjacencyList.set(node, []);
 } else {
 throw new Error('ノードはすでに存在する')
 }
 }
 /**
 *  
 * to  
 * form  
 * weight  
 */
 addEdge (to, from, weight) {
 if (
 this.adjacencyList.has(to) &&
 this.adjacencyList.has(from) &&
 typeof weight === 'number'
 ) {
 let toAdjacent = this.adjacencyList.get(to);
 let fromAdjacent = this.adjacencyList.get(from);
 toAdjacent = [...toAdjacent, [from, weight]];
 fromAdjacent = [...fromAdjacent, [to, weight]];
 this.adjacencyList.set(to, toAdjacent);
 this.adjacencyList.set(from, fromAdjacent); 
 }
 }
}

重み付きグラフのコロケーション行列実装

Lはノードの数。 matrix[i][j]は、ノードiからjのエッジの重み値を表します。

class Graph {
 constructor () {
 this.matrix = [];
 }
 addNode () {
 this.matrix.push([]);
 const L = this.matrix.length;
 for (let i = 0; i < L; i++) {
 let start = this.matrix[i].length;
 while (start < L) {
 this.matrix[i][start] = '*';
 start += 1
 }
 }
 }
 /**
 * to 端点1
 * from  
 * weight 端点1と端点2の重み
 */
 addEdge (to, from, weight) {
 if (
 (to < this.matrix.length && to >= 0) && 
 (from < this.matrix.length && from >= 0) &&
 typeof weight === 'number'
 ) {
 this.matrix[to][from] = weight;
 this.matrix[from][to] = weight;
 }
 }
}

最短経路問題

重みのないグラフの最短経路問題は、BFSを使えば十分です。


/**
 * 無向グラフにおいて、ある頂点から他の頂点への最短経路を求める
 * graph  
 * node 開始頂点
 * 重みなしグラフ、辺の距離はデフォルトで1である
 */
const shortestPath = (graph, node) => {
 // bfsを使用する
 if (
 graph &&
 graph.adjacencyList &&
 graph.adjacencyList.has(node)
 ) {
 const hash = {};
 const queue = [];
 const nodes = graph.adjacencyList.keys();
 for (let node of nodes) {
 hash[node] = false;
 }
 hash[node] = 0;
 queue.push(node);
 while (queue.length > 0) {
 let q = queue.shift();
 let list = graph.adjacencyList.get(q);
 for (let i = 0; i < list.length; i++) {
 let temp = list[i]
 if (hash[temp] !== false && hash[temp] > hash[q] + 1) {
 hash[temp] = hash[q] + 1;
 }
 if (hash[temp] === false) {
 hash[temp] = hash[q] + 1;
 queue.push(temp)
 }
 }
 }
 return hash;
 }
 return -1;
}

重み付きグラフの最短経路問題

Dijkstra

ダイクストラ・アルゴリズムは、グラフのノード間の最短経路を求めるアルゴリズム。1956年にオランダの科学者、エッガース・ダイクストラが提唱。

まず、重み付きグラフを作成します。

// 加重グラフの作成
const graph = new Graph();
// ノードを追加する
graph.addNode('start')
graph.addNode('A')
graph.addNode('B')
graph.addNode('C')
graph.addNode('D')
graph.addNode('finish')
// 重みと同様に辺を追加する
graph.addEdge('start', 'A', 5)
graph.addEdge('start', 'B', 2)
graph.addEdge('A', 'B', 8)
graph.addEdge('A', 'C', 4)
graph.addEdge('A', 'D', 2)
graph.addEdge('B', 'D', 7)
graph.addEdge('C', 'D', 6)
graph.addEdge('C', 'finish', 3)
graph.addEdge('D', 'finish', 1)
/**
 * Dijkstra  
 * @param graph  
 * @param startNode 先頭のノード
 * @param endNode の末尾にあるノード
 */
const Dijkstra = (graph, startNode, endNode) => {
 if (
 graph.adjacencyList.has(startNode) &&
 graph.adjacencyList.has(endNode) &&
 endNode !== startNode
 ) {
 const queue = new Queue();
 const path = [];
 const timeHash = {};
 const backHash = {};
 
 const nodes = graph.adjacencyList.keys();
 for (let node of nodes) {
 // デフォルトの距離は無限大
 timeHash[node] = Number.POSITIVE_INFINITY
 }
 // 始点の距離を0とする
 timeHash[startNode] = 0;
 queue.enqueue([startNode, 0]);
 while (!queue.isEmpty()) {
 const [currentNode] = queue.dequeue();
 const list = graph.adjacencyList.get(currentNode);
 for (let i = 0; i < list.length; i++) {
 const [nextNode, nextNodeWeight] = list[i];
 let time = timeHash[currentNode] + nextNodeWeight;
 // 常に最短経路をとる
 if (time < timeHash[nextNode]) {
 timeHash[nextNode] = time;
 backHash[nextNode] = currentNode;
 queue.enqueue([nextNode, time]);
 }
 }
 }
 let lastNode = endNode
 // バックトラックオブジェクトを使い、最短経路を照会る
 while (startNode !== lastNode) {
 path.unshift(lastNode)
 lastNode = backHash[lastNode];
 }
 path.unshift(startNode);
 return `最短経路: ${path.join(' --> ')}, 所要時間: ${timeHash[endNode]}`
 }
 return -1
}
// 最短経路: start --> A --> D --> finish, 所要時間: 8
Dijkstra(graph, 'start', 'finish')
Read next

ACM-ICPC列挙法の基本アルゴリズム

#include&lt;iostream&gt;\n名前空間 std;\nint main\n for(j

Feb 13, 2020 · 3 min read