10. グラフ理論
1.1. グラフの概要
グラフとは何ですか?
- グラフ構造は、ツリー構造に似たデータ構造です。
- グラフ理論は数学の一分野であり、数学ではツリーはグラフの一種です。
- グラフ理論はグラフを研究対象とし、頂点と辺から構成されるグラフの数学的理論と手法を研究します。
- 主な研究目的は、物間の接続、頂点は物を表し、辺は2 つの物間の関係を表します。
図の特徴:
- 頂点のセット: V (Vertex) は通常、頂点の集合を表すために使用されます。
- エッジのセット: E (エッジ) は通常、エッジのセットを表すために使用されます。
- エッジは頂点と頂点の間の接続です。
- エッジは有向または無向にすることができます。たとえば、A----B は無指向性を意味し、A ---> B は有向性を意味します。
グラフの一般的な用語:
-
頂点:グラフ内のノードを表します。
-
エッジ:頂点と頂点の間の接続を表します。
-
隣接する頂点:エッジによって接続されている頂点は、隣接する頂点と呼ばれます。
-
次数:頂点の次数は、隣接する頂点の数です。
-
道:
- 単純なパス:単純なパスには重複する頂点は必要ありません。
- ループ:最初の頂点が最後の頂点と同じであるパスはループと呼ばれます。
-
無向グラフ:グラフ内のすべてのエッジには方向がありません。
-
有向グラフ:グラフ内のすべてのエッジは有向です。
-
重み付けされていないグラフ:重み付けされていないグラフのエッジには重みの意味がありません。
-
重み付きグラフ:重み付きグラフのエッジには特定の重みの意味があります。
1.2. グラフの表現
隣接行列
グラフを表現する一般的な方法は、隣接行列です。
-
2 次元配列を使用して隣接行列を表すことができます。
-
隣接行列は、各ノードを整数に関連付けます。整数は、配列の添字値として機能します。
-
2 次元配列を使用して頂点間の接続を表現します。
図1に示すように:
- 2 次元配列の0 は接続がないことを意味し、1は接続があることを意味します。
- 例: A[ 0 ] [ 3 ] = 1。これは、A と C の間に接続があることを意味します。
- 隣接行列の対角線上の値はすべて 0 であり、A - A、B - B などの自己ループが接続されていないことを示します (自分自身と自分自身の間に接続がありません)。
- 無向グラフの場合、隣接行列は対角要素がすべて 0 である対称行列である必要があります。
隣接行列の問題:
- グラフがスパース グラフの場合、隣接行列に多数の 0 が存在し、ストレージ スペースが無駄になります。
隣接リスト
グラフを表現するもう 1 つの一般的な方法は、隣接リストです。
- 隣接リストは、グラフ内の各頂点と、その頂点に隣接する頂点のリストで構成されます。
- このリストは、配列/リンク リスト/辞書 (ハッシュ テーブル) など、さまざまな方法で保存できます。
図1に示すように:
- この図では、A が B、C、D に隣接していることがはっきりとわかります。A頂点に隣接するこれらの頂点 (エッジ) を表現したい場合は、対応する配列/リンクに A の値として格納できます。リスト /dictionary。
- その後、キー (キー) A を使用して、対応するデータを非常に簡単に取得できます。
隣接リストの問題:
- 隣接リストは単純にout 次数、つまり他の頂点を指す頂点の数を取得できます。
- ただし、隣接テーブルの次数を計算することは非常に困難です(頂点を指す他の頂点の数を頂点の次数と呼びます)。このとき、効果的に次数を計算するには逆隣接リストを構築する必要があります。
2. パッケージ構造
実装プロセスでは、エッジを表すために隣接リストが使用され、隣接リストを格納するために辞書クラスが使用されます。
2.1. ディクショナリクラスとキュークラスの追加
まず、以前に実装され、後で使用されるディクショナリ クラスとキュー クラスを導入する必要があります。
//封装字典类
function Dictionary(){
//字典属性
this.items = {}
//字典操作方法
//一.在字典中添加键值对
Dictionary.prototype.set = function(key, value){
this.items[key] = value
}
//二.判断字典中是否有某个key
Dictionary.prototype.has = function(key){
return this.items.hasOwnProperty(key)
}
//三.从字典中移除元素
Dictionary.prototype.remove = function(key){
//1.判断字典中是否有这个key
if(!this.has(key)) return false
//2.从字典中删除key
delete this.items[key]
return true
}
//四.根据key获取value
Dictionary.prototype.get = function(key){
return this.has(key) ? this.items[key] : undefined
}
//五.获取所有keys
Dictionary.prototype.keys = function(){
return Object.keys(this.items)
}
//六.size方法
Dictionary.prototype.keys = function(){
return this.keys().length
}
//七.clear方法
Dictionary.prototype.clear = function(){
this.items = {}
}
}
// 基于数组封装队列类
function Queue() {
// 属性
this.items = []
// 方法
// 1.将元素加入到队列中
Queue.prototype.enqueue = element => {
this.items.push(element)
}
// 2.从队列中删除前端元素
Queue.prototype.dequeue = () => {
return this.items.shift()
}
// 3.查看前端的元素
Queue.prototype.front = () => {
return this.items[0]
}
// 4.查看队列是否为空
Queue.prototype.isEmpty = () => {
return this.items.length == 0;
}
// 5.查看队列中元素的个数
Queue.prototype.size = () => {
return this.items.length
}
// 6.toString方法
Queue.prototype.toString = () => {
let resultString = ''
for (let i of this.items){
resultString += i + ' '
}
return resultString
}
}
2.2. グラフクラスの作成
まずグラフ クラス Graph を作成し、基本属性を追加してから、グラフ クラスの共通メソッドを実装します。
//封装图类
function Graph (){
//属性:顶点(数组)/边(字典)
this.vertexes = [] //顶点
this.edges = new Dictionary() //边
}
2.3. 頂点とエッジを追加する
写真が示すように:
グラフの頂点を格納する配列オブジェクトの vertexes を作成し、グラフのエッジを格納する辞書オブジェクトのedges を作成します。ここで、キーは頂点で、値はキー頂点の隣接する頂点を格納する配列です。
コード:
//添加方法
//一.添加顶点
Graph.prototype.addVertex = function(v){
this.vertexes.push(v)
this.edges.set(v, []) //将边添加到字典中,新增的顶点作为键,对应的值为一个存储边的空数组
}
//二.添加边
Graph.prototype.addEdge = function(v1, v2){//传入两个顶点为它们添加边
this.edges.get(v1).push(v2)//取出字典对象edges中存储边的数组,并添加关联顶点
this.edges.get(v2).push(v1)//表示的是无向表,故要添加互相指向的两条边
}
2.4. 文字列出力への変換
toString メソッドをグラフ クラス Graph に追加して、グラフ内の各頂点を隣接リストの形式で出力します。
コード:
//三.实现toString方法:转换为邻接表形式
Graph.prototype.toString = function (){
//1.定义字符串,保存最终结果
let resultString = ""
//2.遍历所有的顶点以及顶点对应的边
for (let i = 0; i < this.vertexes.length; i++) {//遍历所有顶点
resultString += this.vertexes[i] + '-->'
let vEdges = this.edges.get(this.vertexes[i])
for (let j = 0; j < vEdges.length; j++) {//遍历字典中每个顶点对应的数组
resultString += vEdges[j] + ' ';
}
resultString += '\n'
}
return resultString
}
2.5. グラフの走査
グラフ走査のアイデア:
- グラフの走査の考え方はツリーの走査の考え方と同じです。つまり、グラフ内のすべての頂点を1 回訪問する必要があり、繰り返し訪問することはできません(上記の toString メソッドは訪問を繰り返します)。
グラフを走査するための 2 つのアルゴリズム:
- 幅優先検索 (Breadth-First Search、BFSと呼ばれます)。
- 深さ優先検索 (深さ優先検索、略してDFS );
- どちらの走査アルゴリズムでも、最初に訪問する頂点を指定する必要があります。
頂点が訪問されたかどうかを記録するために、3 つの色を使用して頂点の状態を示します
- 白: 頂点が訪問されていないことを示します。
- 灰色: 頂点は訪問されたが、その隣接する頂点は完全には訪問されていないことを示します。
- 黒: 頂点が訪問され、その隣接する頂点がすべて訪問されたことを示します。
まず、initializeColor メソッドをカプセル化して、グラフ内のすべての頂点を白に初期化します。コードの実装は次のとおりです。
//四.初始化状态颜色
Graph.prototype.initializeColor = function(){
let colors = []
for (let i = 0; i < this.vertexes.length; i++) {
colors[this.vertexes[i]] = 'white';
}
return colors
}
幅優先検索アルゴリズムの考え方:
- 幅優先検索アルゴリズムは、一度にグラフの 1 つの層を訪問するのと同じように、指定された最初の頂点からグラフを横断し、最初に隣接するすべての頂点を訪問します。
- また、グラフ内の各頂点を最初に広く、次に深く走査するとも言えます。
実装のアイデア:
幅優先検索アルゴリズムは、キューに基づいて簡単に実装できます。
- まずキュー Q (テールイン、ヘッドアウト) を作成します。
- カプセル化されたinitializeColorメソッドを呼び出して、すべての頂点を白に初期化します。
- 最初の頂点 A を指定し、A を灰色(訪問ノード) としてマークし、A をキュー Q に入れます。
- キュー Q が空でない限り、キュー内の要素をループし、次の操作を実行します。
- まず、Q の先頭から灰色の A を削除します。
- Aが取り出された後、Aの隣接する未訪問頂点(白)がキューQの最後尾から順番にキューに追加され、灰色に変化する。このようにして、灰色の隣接する頂点が繰り返しキューに追加されることはありません。
- A のすべての隣接ノードが Q に追加されると、A は黒になり、次のサイクルで Q から削除されます。
コード:
//五.实现广度搜索(BFS)
//传入指定的第一个顶点和处理结果的函数
Graph.prototype.bfs = function(initV, handler){
//1.初始化颜色
let colors = this.initializeColor()
//2.创建队列
let que = new Queue()
//3.将顶点加入到队列中
que.enqueue(initV)
//4.循环从队列中取出元素,队列为空才停止
while(!que.isEmpty()){
//4.1.从队列首部取出一个顶点
let v = que.dequeue()
//4.2.从字典对象edges中获取和该顶点相邻的其他顶点组成的数组
let vNeighbours = this.edges.get(v)
//4.3.将v的颜色变为灰色
colors[v] = 'gray'
//4.4.遍历v所有相邻的顶点vNeighbours,并且加入队列中
for (let i = 0; i < vNeighbours.length; i++) {
const a = vNeighbours[i];
//判断相邻顶点是否被探测过,被探测过则不加入队列中;并且加入队列后变为灰色,表示被探测过
if (colors[a] == 'white') {
colors[a] = 'gray'
que.enqueue(a)
}
}
//4.5.处理顶点v
handler(v)
//4.6.顶点v所有白色的相邻顶点都加入队列后,将顶点v设置为黑色。此时黑色顶点v位于队列最前面,进入下一次while循环时会被取出
colors[v] = 'black'
}
}
詳細なプロセス:
最初に指定した頂点が A の場合の走査プロセスは次のとおりです。
- 図 a に示すように、A に隣接し、辞書エッジ内でまだ訪問されていない白い頂点 B、C、D をキュー que に入れて灰色に変え、次に A を黒に変えてキューから削除します。
- 次に、図 b に示すように、B に隣接し、辞書エッジ内でまだ訪問されていない白い頂点 E と F がキュー que に入れられて灰色になり、次に B が黒になってキューから削除されます。
- 図cのように辞書辺から取り出したCに隣接する未訪問の白い頂点G(A,Dも隣接していますが灰色になっているのでキューに追加しません)をキューqueに入れてターンします。グレー、次に C を黒に変えてデキューします。
- 次に、図 d に示すように、辞書辺から取り出した D に隣接する未訪問の白頂点 H をキュー que に入れて灰色にし、D を黒にしてキューから外します。
このサイクルは、キュー内の要素が 0 になるまで、つまりすべての頂点が黒く塗りつぶされてキューから削除されるまで停止しません。この時点で、グラフ内のすべての頂点が走査されています。
幅優先検索の順序は、反復することなくすべての頂点を横断することがわかります。
深さ優先検索アルゴリズムの考え方:
- 深さ優先検索アルゴリズムは、指定された最初の頂点からグラフを横断し、パスの最後の頂点に到達するまでパスに沿って横断します。
- 次に、元のパスに沿って戻り、次のパスを探索します。つまり、グラフ内の各頂点を深く、次に広くたどります。
実装のアイデア:
- スタック構造を使用して深さ優先検索アルゴリズムを実装できます。
- 深さ優先探索アルゴリズムの走査順序は、二分探索ツリーの前順序走査に似ており、再帰を使用して実装することもできます(再帰の本質は、関数 stackの呼び出しです)。
再帰に基づいて深さ優先検索アルゴリズムを実装します。再帰メソッド dfsVisit を呼び出す dfs メソッドを定義し、グラフ内の各頂点を再帰的に訪問する dfsVisit メソッドを定義します。
dfs メソッドでは次のようになります。
- まず、initializeColor メソッドを呼び出して、すべての頂点を白に初期化します。
- 次に、dfsVisit メソッドを呼び出してグラフの頂点を移動します。
dfsVisit メソッド内:
- まず、指定された入力ノード v を灰色でマークします。
- 次に、頂点 V を処理します。
- 次に、V の隣接する頂点を訪問します。
- 最後に、頂点 v を黒としてマークします。
コード:
//六.实现深度搜索(DFS)
Graph.prototype.dfs = function(initV, handler){
//1.初始化顶点颜色
let colors = this.initializeColor()
//2.从某个顶点开始依次递归访问
this.dfsVisit(initV, colors, handler)
}
//为了方便递归调用,封装访问顶点的函数,传入三个参数分别表示:指定的第一个顶点、颜色、处理函数
Graph.prototype.dfsVisit = function(v, colors, handler){
//1.将颜色设置为灰色
colors[v] = 'gray'
//2.处理v顶点
handler(v)
//3.访问V的相邻顶点
let vNeighbours = this.edges.get(v)
for (let i = 0; i < vNeighbours.length; i++) {
let a = vNeighbours[i];
//判断相邻顶点是否为白色,若为白色,递归调用函数继续访问
if (colors[a] == 'white') {
this.dfsVisit(a, colors, handler)
}
}
//4.将v设置为黑色
colors[v] = 'black'
}
詳細なプロセス:
ここでは主に、コードの 3 番目のステップである、指定された頂点の隣接する頂点へのアクセスについて説明します。
- 指定された頂点 A を例にとると、まず、頂点とそれに対応する隣接頂点を格納する辞書オブジェクトのエッジから、頂点 A の隣接頂点で構成される配列を取り出します。
- ステップ 1 : 頂点 A が灰色になり、最初の for ループに入り、A の隣接する白色の頂点 B、C、D をたどります。for ループの最初のループ (B を実行) で、頂点 B は次の条件を満たします: Colors == " 「white」、再帰をトリガーし、このメソッドを再度呼び出します。
- ステップ 2 : 頂点 B がグレーに変わり、2 番目の for ループに入り、B の隣接する白色の頂点 E、F を移動します。for ループの最初のサイクル (E の実行) で、頂点 E は次の条件を満たします: color == "white" 、再帰をトリガーし、メソッドを再度呼び出します。
- ステップ 3 : 頂点 E がグレーに変わり、3 番目の for ループに入り、E の隣接する白の頂点を横断します: I; for ループの最初のサイクル (I を実行) で、頂点 I は次の条件を満たします: color == "white"、trigger再帰、メソッドを再度呼び出します。
- ステップ 4 : 頂点 I が灰色になり、4 番目の for ループに入る 頂点 I の隣接頂点 E が color == "white" を満たさないため、再帰呼び出しを停止します。
- ステップ 5 : 再帰が終了したら、最後まで戻り、最初に 3 番目の for ループに戻り、2 番目、3 番目... ループを実行し続けます。各ループの実行プロセスは上記と同じです。グラフのすべての頂点を訪問するまで続きます。
-
次の図は、グラフ内の各頂点を移動する完全なプロセスを示しています。
- Discovery は頂点が訪問されたことを示し、ステータスが灰色になります。
- 探索は、頂点とその頂点に隣接するすべての頂点の両方が訪問されたことを示し、ステータスは黒になります。
- 処理関数ハンドラーは頂点がグレーに変わった後に呼び出されるため、ハンドラー メソッドの出力順序は頂点が見つかった順序 (A、B、E、I、F、C、D、G、H) になります。