【データ構造とアルゴリズム】グラフ理論と関連アルゴリズム

グラフの基本的な紹介

線形テーブルは直接の先行者と直接の後続者の間の関係に限定されており、ツリーは 1 つの直接の先行者、つまり親ノードのみを持つことができます。多対多の関係を表す必要がある場合、ここではグラフを使用します。

グラフは、ノードが 0 個以上の隣接する要素を持つことができるデータ構造です。2 つのノード間の接続はエッジと呼ばれます。ノードは頂点とも呼ばれます。図に示すように:

ここに画像の説明を挿入

簡単に言うと、グラフは、頂点と頂点間のエッジの有限で空でないセットで構成されるセットです。通常、G(V,E) のように表現されます。ここで、G はグラフを表し、V は頂点の集合を表し、E はエッジの集合を表します。

次に、図に含まれるいくつかの共通の概念について説明します。

  1. 节点(Vertex): エンティティを表すために使用される図内の基本要素。

  2. 边(Edge): 2 つのノードを接続する線分。ノード間の関係を表すために使用されます。

  3. 度(Degree): 次数は頂点に含まれるエッジの数を示します。有向グラフでは出次数と入次数にも分けられます。出次数は頂点から出ていくエッジの数を示し、入次数は頂点に入るエッジの数を示します。

  4. 有向图和无向图: エッジに方向があるかどうかによって、グラフが有向か無向かが決まります。有向枝は枝の始点と終点を示し、無向枝の2つのノードには始点と終点の区別がない。
    ここに画像の説明を挿入

  5. 带权图和无权图: エッジに重み値があるかどうかによって、グラフが重み付きグラフか重みなしグラフかが決まります。重み付けされたエッジにはエッジの重みを表す値があり、重み付けされていないエッジの重みは 1 とみなされます。
    ここに画像の説明を挿入

  6. 路径: グラフ内の一連の頂点間の一連のエッジはパスと呼ばれます。

  7. 连通性: 任意の 2 つのノード間にパスが存在する場合、グラフは接続されます。それ以外の場合は、接続されていないグラフになります。

  8. 海量图: ノードとエッジの数は膨大で、コンピューターのメモリのサイズをはるかに超えており、グラフを処理するには特別なアルゴリズムとストレージ構造が必要です。

グラフの表現

グラフを表現するには 2 つの方法があります。

  • 2次元配列表現(隣接行列)
  • リンクリスト表現(隣接リスト)

隣接行列

隣接行列はグラフの格納構造であり、2 次元配列を使用してグラフ内のノード間のエッジを表します。

ここに画像の説明を挿入
具体的には:

  1. 隣接行列の行数と列数がグラフ内のノード数nとなります。
  2. マトリックスの各要素はエッジを表します。ノード i とノード j の間にエッジがある場合、行列の i 行 j 列の要素は 1、それ以外の場合は 0 になります。
  3. 重み付きグラフの場合、行列の要素は 1 ではなく、対応する重みになります。
  4. ノードはそれ自体に接続されないため、行列の主対角要素はすべて 0 になります。
  5. 隣接行列は密なグラフを表現するのに適しており、空間計算量は O(n2) です。グラフがまばらな場合、スペース使用率は比較的低くなります。

隣接マトリックスの利点は次のとおりです。

  1. 2 つのノード間にエッジがあるかどうかは O(1) 時間で判断できます。
  2. 幅優先検索、深さ優先検索などのいくつかのアルゴリズムを実装すると便利です。

隣接マトリックスの欠点は次のとおりです。

  1. 空間の複雑さは高く、グラフがまばらな場合は空間の無駄が発生します。
  2. 動的グラフを表現するのは難しく、エッジの追加や削除が頻繁に行われる場合には、隣接行列を頻繁に再構築する必要があります。

隣接リスト

隣接リストはグラフのもう 1 つの保存構造であり、リンク リストを使用して各ノードの隣接ノードを保存します。

ここに画像の説明を挿入

具体的には:

  1. 隣接リストは配列とリンク リストで構成されます。配列の各要素はリンク リストであり、対応するノードの隣接ノードのリストを表します。
  2. 配列のインデックスはノードのシリアル番号で、配列のサイズはノード数 n です。
  3. ノード i がノード j とのエッジを持つ場合、i に対応するリンク リストに j を追加します。
  4. 重み付きグラフの場合、リンク リスト内の各ノードの要素は、隣接ノードのシリアル番号を格納するだけでなく、隣接ノードのシリアル番号と対応する重みを含む構造を格納します。
  5. 隣接リストはスパース グラフに適しており、空間複雑度は O(n+e) (e はエッジの数) です。グラフが密集している場合、スペース使用率は高くなります。

隣接リストの利点は次のとおりです。
6. 空間の複雑さが低く、疎なグラフを表現するのに適しています。
7. 動的グラフの追加・削除操作が容易に実現できます。

隣接リストの欠点は次のとおりです。
8. O(1) 時間以内に 2 つのノード間にエッジがあるかどうかを判断するのは難しく、リンクされたリストを走査する必要があります。
9. 幅優先検索や深さ優先検索などの一部のアルゴリズムを実装するのは不便です。

グラフの深さ優先トラバーサル (DFS)

いわゆるグラフトラバーサルはノードへのアクセスです。グラフには非常に多くのノードがあるため、これらのノードを移動するには特定の戦略が必要です。一般に、次の 2 つのアクセス戦略があります。

  • 深度优先遍历(DFS)
  • 广度优先遍历(BFS)

概要

深さ優先トラバーサル。最初のアクセス ノードから開始します。最初のアクセス ノードには複数の隣接ノードがある場合があります。深さ優先トラバーサルの戦略は、最初に最初の隣接ノードを訪問し、次に訪問した隣接ノードを最初のノードとして使用して最初の隣接ノードを訪問します。

これは次のように理解できます。現在のノードを訪問した後は常に、最初に現在のノードの最初の隣接ノードを訪問します。このようなアクセス戦略は、ノードのすべての隣接ノードに水平にアクセスするのではなく、垂直に深く掘り下げることであることがわかります。明らかに、深さ優先検索は再帰的なプロセスです。

ここに画像の説明を挿入

一般に:深さ優先検索 (深さ優先検索) は、グラフ走査アルゴリズムです。特定のノードから開始され、現在のパス上のすべてのノードが通過されるまで、できるだけ深く検索されます。その後、後戻りして、次のパスをできるだけ深く探索し続けます

深さ優先検索の主な用途は次のとおりです。グラフの接続性チェック、トポロジカルソート、カット数の解決、二部グラフの最大一致数の解決など。

実装手順

深さ優先検索のプロセスは再帰的に実装できます。主な手順は次のとおりです。

  1. ターゲット グラフのノード v から開始して、そのノードにアクセスします。
  2. v の隣接ノード w がまだ訪問されていない場合は、w を再帰的に訪問します。
  3. w が訪問されている場合は、ノード v に戻り、v の別の未訪問の隣接ノードを訪問します。
  4. v のすべての隣接ノードを訪問した場合は、v の前のノードに戻ります。
  5. すべてのノードにアクセスするまで、手順 3 と 4 を繰り返します。

再帰が使用されているため、スタックのアイデアを使用して以下を理解できます。

ここに画像の説明を挿入
ここに画像の説明を挿入

ここに画像の説明を挿入
ここに画像の説明を挿入

ここに画像の説明を挿入
ここに画像の説明を挿入

コード

ここに画像の説明を挿入

public class Graph {
    
    

	//存储顶点集合
	private ArrayList<String> vertexList; 
	//存储图对应的邻结矩阵
	private int[][] edges; 
	 //表示边的数目
	private int numOfEdges;
	//定义给数组 boolean[], 记录某个结点是否被访问
	private boolean[] isVisited;

    //dfs部分
    public void dfs(){
    
    
        //初始化访问数组
        isVisited = new boolean[vertexList.size()];
        for (int i = 0;i < isVisited.length;i++) {
    
    
            if (!isVisited[i]) {
    
    
                dfs(i);
            }
        }
    }

    public void dfs(int index){
    
    
        //打印出当前节点
        System.out.print(getValueByIndex(index) + " -> ");
        //设置当前节点已被访问
        isVisited[index] = true;
        //找出该节点的第一个邻接点
        int firstNeighbor = getFirstNeighbor(index);
        //说明存在第一个临界点
        if (firstNeighbor != -1) {
    
    
            dfs(firstNeighbor);
        }
    }

    /**
     * 找到index节点的第一个临界点,如果没有返回-1
     * @param index
     * @return
     */
    public int getFirstNeighbor(int index) {
    
    
        for (int i = 0; i < edges.length; i++) {
    
    
            if (edges[index][i] == 1 && !isVisited[i]) {
    
    
                return i;
            }
        }
        return -1;
    }
	
	//返回结点i(下标)对应的数据 0->"A" 1->"B" 2->"C"
	public String getValueByIndex(int i) {
    
    
		return vertexList.get(i);
	}
	

}

知らせ:

  • getFirstNeighbor メソッドでは、ここでの j は 0 から開始する必要があり、人によってはインデックス + 1 から開始する傾向があります。これは、トラバースが A から開始しないという状況を考慮していません。
  • ノード リストをトラバースする理由は、それが切断されたグラフである可能性があり、1 つのノードからトラバースしてもグラフ全体のすべてのノードをトラバースできない可能性があるためです。
    ここに画像の説明を挿入

グラフの幅優先トラバーサル (BFS)

概要

幅優先検索はグラフ走査アルゴリズムです。特定のノードから開始して、最初にそのノードに隣接するすべてのノードを訪問し、次に隣接するノードの隣接ノードを訪問するというように、すべてのノードが通過されるまで続きます。

幅優先検索は、水面の波紋のように層ごとに外側に拡張します。
ここに画像の説明を挿入

深さ優先検索と比較した場合、幅優先検索の特徴は次のとおりです。

  1. 開始ノードに近いノードが最初にアクセスされます。深さ優先検索では、可能な限り深く、場合によっては開始点からさらに遠くまで検索します。
  2. キューを使用して実装されるため、スペースの複雑さが高くなります。深さ優先検索では、空間の複雑さが低い再帰的スタックが使用されます。

幅優先検索は主に次の目的で使用されます。グラフの最短経路問題、位相的ソート待って。

実装手順

幅優先検索のプロセスはキューを使用して実装できます。主な手順は次のとおりです。

  1. グラフ内の特定のノード v から開始して、そのノードにアクセスし、それをキューに入れます。
  2. チームのヘッド ノードを取り出し、このノードの未訪問の隣接ノードをすべて訪問し、隣接ノードをキューに入れます。
  3. キューが空になるまでステップ 2 を繰り返します。
  4. グラフ内に未訪問のノードがある場合は、未訪問のノードの 1 つから開始して、ステップ 1 ~ 3 を繰り返します。

ここに画像の説明を挿入
ここに画像の説明を挿入
ここに画像の説明を挿入
ここに画像の説明を挿入
ここに画像の説明を挿入
ここに画像の説明を挿入

コード

ここに画像の説明を挿入

public class Graph {
    
    

	//存储顶点集合
	private ArrayList<String> vertexList; 
	//存储图对应的邻结矩阵
	private int[][] edges; 
	 //表示边的数目
	private int numOfEdges;
	//定义给数组 boolean[], 记录某个结点是否被访问
	private boolean[] isVisited;

    /**
     * 在bfs中负责找到指定节点的所有未遍历节点
     * @param index
     */
    public void getNeighbors(int index, Queue<Integer> queue){
    
    
        for (int i = 0; i < edges.length; i++) {
    
    
            if (edges[index][i] == 1 && !isVisited[i]) {
    
    
                queue.add(i);
                //将入队节点标为已访问
                isVisited[i] = true;
            }
        }
    }

    public void bfs(){
    
    
        //初始化访问数组
        isVisited = new boolean[vertexList.size()];
        for (int i = 0;i < isVisited.length;i++) {
    
    
            if (!isVisited[i]) {
    
    
                bfs(i);
            }
        }
    }

    public void bfs(int index) {
    
    
        //创建队列
        Queue<Integer> queue = new LinkedList<>();
        //首先将起始节点加入队列,并设为已访问
        queue.add(index);
        isVisited[index] = true;
        //每次弹出的队头
        Integer head;
        while (!queue.isEmpty()) {
    
    
            //弹出头节点
            head = queue.poll();
            //并将其临界点全部放入队列
            getNeighbors(head,queue);
            //打印该节点
            System.out.print(getValueByIndex(head) + " -> ");
        }

    }
	
	//返回结点i(下标)对应的数据 0->"A" 1->"B" 2->"C"
	public String getValueByIndex(int i) {
    
    
		return vertexList.get(i);
	}
	
	//返回结点的个数
	public int getNumOfVertex() {
    
    
		return vertexList.size();
	}


}

グラフの一般的なコードのまとめ

public class Graph {
    
    

	private ArrayList<String> vertexList; //存储顶点集合
	private int[][] edges; //存储图对应的邻结矩阵
	private int numOfEdges; //表示边的数目
	private boolean[] isVisited; //记录某个结点是否被访问
	
	public static void main(String[] args) {
    
    
		//测试一把图是否创建ok
		int n = 8;  //结点的个数
		//String Vertexs[] = {"A", "B", "C", "D", "E"};
		String Vertexs[] = {
    
    "1", "2", "3", "4", "5", "6", "7", "8"};
		
		//创建图对象
		Graph graph = new Graph(n);
		//循环的添加顶点
		for(String vertex: Vertexs) {
    
    
			graph.insertVertex(vertex);
		}
		
		//添加边
		//A-B A-C B-C B-D B-E 
//		graph.insertEdge(0, 1, 1); // A-B
//		graph.insertEdge(0, 2, 1); // 
//		graph.insertEdge(1, 2, 1); // 
//		graph.insertEdge(1, 3, 1); // 
//		graph.insertEdge(1, 4, 1); // 
		
		//更新边的关系
		graph.insertEdge(0, 1, 1);
		graph.insertEdge(0, 2, 1);
		graph.insertEdge(1, 3, 1);
		graph.insertEdge(1, 4, 1);
		graph.insertEdge(3, 7, 1);
		graph.insertEdge(4, 7, 1);
		graph.insertEdge(2, 5, 1);
		graph.insertEdge(2, 6, 1);
		graph.insertEdge(5, 6, 1);

		
		
		//显示一把邻结矩阵
		graph.showGraph();
		
		//测试一把,我们的dfs遍历是否ok
		System.out.println("深度遍历");
		graph.dfs(); // A->B->C->D->E [1->2->4->8->5->3->6->7]
//		System.out.println();
		System.out.println("广度优先!");
		graph.bfs(); // A->B->C->D-E [1->2->3->4->5->6->7->8]
		
	}
	
	//构造器
	public Graph(int n) {
    
    
		//初始化矩阵和vertexList
		edges = new int[n][n];
		vertexList = new ArrayList<String>(n);
		numOfEdges = 0;
		
	}
	
	//得到第一个邻接结点的下标 w 
	/**
	 * 
	 * @param index 
	 * @return 如果存在就返回对应的下标,否则返回-1
	 */
	public int getFirstNeighbor(int index) {
    
    
		for(int j = 0; j < vertexList.size(); j++) {
    
    
			if(edges[index][j] > 0) {
    
    
				return j;
			}
		}
		return -1;
	}
	//根据前一个邻接结点的下标来获取下一个邻接结点
	public int getNextNeighbor(int v1, int v2) {
    
    
		for(int j = v2 + 1; j < vertexList.size(); j++) {
    
    
			if(edges[v1][j] > 0) {
    
    
				return j;
			}
		}
		return -1;
	}
	
	//深度优先遍历算法
	//i 第一次就是 0
	private void dfs(boolean[] isVisited, int i) {
    
    
		//首先我们访问该结点,输出
		System.out.print(getValueByIndex(i) + "->");
		//将结点设置为已经访问
		isVisited[i] = true;
		//查找结点i的第一个邻接结点w
		int w = getFirstNeighbor(i);
		while(w != -1) {
    
    //说明有
			if(!isVisited[w]) {
    
    
				dfs(isVisited, w);
			}
			//如果w结点已经被访问过
			w = getNextNeighbor(i, w);
		}
		
	}
	
	//对dfs 进行一个重载, 遍历我们所有的结点,并进行 dfs
	public void dfs() {
    
    
		isVisited = new boolean[vertexList.size()];
		//遍历所有的结点,进行dfs[回溯]
		for(int i = 0; i < getNumOfVertex(); i++) {
    
    
			if(!isVisited[i]) {
    
    
				dfs(isVisited, i);
			}
		}
	}
	
	//对一个结点进行广度优先遍历的方法
	private void bfs(boolean[] isVisited, int i) {
    
    
		int u ; // 表示队列的头结点对应下标
		int w ; // 邻接结点w
		//队列,记录结点访问的顺序
		LinkedList queue = new LinkedList();
		//访问结点,输出结点信息
		System.out.print(getValueByIndex(i) + "=>");
		//标记为已访问
		isVisited[i] = true;
		//将结点加入队列
		queue.addLast(i);
		
		while( !queue.isEmpty()) {
    
    
			//取出队列的头结点下标
			u = (Integer)queue.removeFirst();
			//得到第一个邻接结点的下标 w 
			w = getFirstNeighbor(u);
			while(w != -1) {
    
    //找到
				//是否访问过
				if(!isVisited[w]) {
    
    
					System.out.print(getValueByIndex(w) + "=>");
					//标记已经访问
					isVisited[w] = true;
					//入队
					queue.addLast(w);
				}
				//以u为前驱点,找w后面的下一个邻结点
				w = getNextNeighbor(u, w); //体现出我们的广度优先
			}
		}
		
	} 
	
	//遍历所有的结点,都进行广度优先搜索
	public void bfs() {
    
    
		isVisited = new boolean[vertexList.size()];
		for(int i = 0; i < getNumOfVertex(); i++) {
    
    
			if(!isVisited[i]) {
    
    
				bfs(isVisited, i);
			}
		}
	}
	
	//图中常用的方法
	//返回结点的个数
	public int getNumOfVertex() {
    
    
		return vertexList.size();
	}
	//显示图对应的矩阵
	public void showGraph() {
    
    
		for(int[] link : edges) {
    
    
			System.err.println(Arrays.toString(link));
		}
	}
	//得到边的数目
	public int getNumOfEdges() {
    
    
		return numOfEdges;
	}
	//返回结点i(下标)对应的数据 0->"A" 1->"B" 2->"C"
	public String getValueByIndex(int i) {
    
    
		return vertexList.get(i);
	}
	//返回v1和v2的权值
	public int getWeight(int v1, int v2) {
    
    
		return edges[v1][v2];
	}
	//插入结点
	public void insertVertex(String vertex) {
    
    
		vertexList.add(vertex);
	}
	//添加边
	/**
	 * 
	 * @param v1 表示点的下标即使第几个顶点  "A"-"B" "A"->0 "B"->1
	 * @param v2 第二个顶点对应的下标
	 * @param weight 表示 
	 */
	public void insertEdge(int v1, int v2, int weight) {
    
    
		edges[v1][v2] = weight;
		edges[v2][v1] = weight;
		numOfEdges++;
	}
}

最小スパニング ツリー アルゴリズム

最小全域木はグラフ理論において非常に重要な概念です。グラフ内のすべてのノードを接続するツリーを指し、このツリー上のすべてのエッジの重みの合計が最小になります。

最小スパニング ツリーには、いくつかの重要なプロパティがあります。

  1. グラフ内のすべてのノードが含まれており、孤立した点はありません。
  2. それは年輪のない木です。
  3. 重みの合計が最小になります。

ここに画像の説明を挿入

一般的な最小スパニング ツリー アルゴリズムは次のとおりです。
4. Prim算法: ノードから開始し、すべてのノードが含まれるまでスパニング ツリーに新しいノードとエッジを継続的に追加します。追加された各新しいエッジは、ツリー内のノードへの最短のエッジです。
5. Kruskal算法: エッジの重みに応じて小さいエッジから大きいエッジまで選択し、このエッジがリングを形成しない限り、最小スパニング ツリーに追加されます。
6. Dijkstra算法: ダイクストラの最短パス アルゴリズムを使用して、各ノードから他のすべてのノードへの最短パスを見つけます。これらの最短パスによって最小スパニング ツリーが形成されます。

最小スパニング ツリーには、ネットワーク接続、回路配線など、多くの実用的な用途があります。これらの実際的な問題に対する効率的な解決策を提供します。

つまり、最小スパニング ツリーはグラフ理論において非常に古典的で重要な概念であり、関連するアルゴリズムも重要であり、理解して習得する価値があります。

最小スパニング ツリー (Kruskal (Kruskal) および Prim (Prim)) アルゴリズムのアニメーション デモンストレーション

プリムのアルゴリズム

Prim のアルゴリズムは、最小スパニング ツリーの古典的なアルゴリズムの 1 つです。その基本的な考え方は次のとおりです。

  • グラフ内の任意のノードから開始して、エッジとノードを段階的に追加して、最小スパニング ツリーを形成します。追加される新しいエッジはそれぞれ、ツリー内のノードとツリーにないノードを接続する必要があり、この新しいエッジはすべての候補エッジの中で最小の重みを持つエッジでなければなりません。

Prim のアルゴリズムの手順は次のとおりです。

  1. グラフ内の任意のノードを開始ノードとして選択し、そのノードが訪問済みであることをマークします。
  2. 訪問済みノードに接続されているすべての未訪問ノードの中から最小の重みを持つエッジを見つけます。このエッジによって接続されている未訪問のノードは、訪問済みとしてマークされます。
  3. すべてのノードにアクセスするまでステップ 2 を繰り返します。
  4. 形成されるエッジ セットは最小スパニング ツリーです。

または、非常にわかりやすく説明している次のビデオを参照することもできます。

アルゴリズム図:
ここに画像の説明を挿入

Prim のアルゴリズムの時間計算量は O(n2) ですが、優先キューを使用して実装すると O(nlogn) に削減できます。

Prim のアルゴリズムは、重み付き無向接続グラフにのみ適用できます。グラフが有向または切断されている場合、Prim のアルゴリズムは最小スパニング ツリーを取得できません。

アルゴリズムの練習

コード:

import java.util.*;

public class PrimAlgorithm {
    
    
    private static final int INF = Integer.MAX_VALUE;

    public static int prim(int[][] graph) {
    
    
        int n = graph.length;
        //创建距离表,代表索引对应节点距离最小生成树的距离
        int[] dist = new int[n];
        //记录每个节点是否被添加,未添加记为false
        boolean[] visited = new boolean[n];
        //先将距离表都初始化为最大值
        Arrays.fill(dist, INF);
        //从0节点开始遍历,将距离更新一下
        dist[0] = 0;
        //记录要返回的最小权值
        int res = 0;
		
        for (int i = 0; i < n; i++) {
    
    
        	//u代表要添加的节点(距离最近且未访问)
            int u = -1;
            for (int j = 0; j < n; j++) {
    
    
                if (!visited[j] && (u == -1 || dist[j] < dist[u])) {
    
    
                    u = j;
                }
            }
            //将要添加的节点标为已访问
            visited[u] = true;
            //记录权值
            res += dist[u];
            //更新距离表
            for (int v = 0; v < n; v++) {
    
    
                if (!visited[v] && graph[u][v] != INF && graph[u][v] < dist[v]) {
    
    
                    dist[v] = graph[u][v];
                }
            }
        }

        return res;
    }

    public static void main(String[] args) {
    
    
        int[][] graph = new int[][]{
    
    
                {
    
    0, 2, INF, 6, INF},
                {
    
    2, 0, 3, 8, 5},
                {
    
    INF, 3, 0, INF, 7},
                {
    
    6, 8, INF, 0, 9},
                {
    
    INF, 5, 7, 9, 0}
        };
        int res = prim(graph);
        System.out.println(res);
    }
}

この実装では、まず距離配列 dist と訪問フラグ配列 Visited を初期化し、距離配列のすべての要素を無限大 (訪問されていないノードを表す) に初期化します。

次に、ノード 0 から始まるすべてのノードを走査し、そのたびに距離配列の最小値を次に訪問するノードとして選択します。次に、このノードを訪問済みとしてマークし、距離配列内のまだ訪問していないノードまでの距離を更新します。最後に、すべてのエッジの重みの合計を最小スパニング ツリーの重みとして返します。

この実装の時間計算量は O(n^2) です。ここで、n はノードの数です。

クラスカルアルゴリズム

Kruskal のアルゴリズムは、最小スパニング ツリーのもう 1 つの古典的なアルゴリズムです。その基本的な考え方は、
グラフ内のすべてのエッジを重みに従って小さいものから大きいものまで並べ替えることです。最小の重みを持つエッジを選択し、エッジが循環を形成しない限り、それを最小全域木に追加します。最小スパニング ツリーにグラフ内のすべてのノードが含まれるまで、この手順を繰り返します。

Kruskal アルゴリズムの手順は次のとおりです。

  1. グラフ内のすべてのエッジを重みの昇順に並べ替えます。
  2. リングが形成されているかどうかを判断するには、最小の重みを持つエッジを選択します。循環を形成しない場合は、最小スパニングツリーに追加されます。
  3. 最小スパニング ツリーにグラフ内のすべての頂点が含まれるまで、手順 2 を繰り返します。
  4. 最小スパニングツリーを出力します。

クラスカルのアルゴリズムの時間計算量は O(ElogE) です。ここで、E はグラフ内のエッジの数です。

クラスカル アルゴリズムの実装では、選択されたエッジがリングを形成するかどうかを判断するために和集合検索セットを使用する必要があります共用体チェックは、2 つの要素が同じ集合に属するかどうかを O(1) 時間で判定することができ、これがクラスカルアルゴリズムを実現する鍵となります。

Prim のアルゴリズムと比較すると、Kruskal のアルゴリズムは、時間計算量がノードの数には依存せず、エッジの数のみに依存するため、より多くのエッジを持つスパース グラフに適しています。ただし、Kruskal アルゴリズムでは事前にすべてのエッジをソートする必要があるため、空間の複雑さが増加します。

図:
ここに画像の説明を挿入

そしてルックアップ

ユニオン チェック セットはツリー型のデータ構造であり、一部の素なセットのマージやクエリの問題に対処するために使用されます
次の 3 つの操作をサポートします。

ここに画像の説明を挿入

  1. 初期化する
  2. union(x, y): 要素 x と要素 y が配置されているコレクションをマージします。
  3. find(x): 要素 x が配置されている集合の代表を見つけます。集合の代表は、集合に追加された最も古い要素です。

ユニオン検索は一種の動的接続を実現しており、最初は各要素が単独でセットを形成し、セットはユニオン操作によって継続的にマージされ、最終的にいくつかの素な大きなセットが形成されます。

共用体検索の一般的な用途は、2 つのノードが無向グラフ内の同じ接続されたグラフ内にあるかどうかをクエリするなど、グラフ理論におけるオフライン クエリの問題を解決することです。

共用体チェックを実装するには 2 つの一般的な方法があります。

  1. クイック検索: ツリー構造を使用すると、検索操作はルート ノードまでトラバースする必要があり、時間計算量は O(n) です。
  2. パス圧縮による高速マージ: 検索操作のトラバーサル プロセス中に、ノードをルート ノードに直接ポイントして、パス圧縮を実現し、ツリーの深さを減らします。バランス調整後、時間計算量は O(1) に達する可能性があります。

ここに画像の説明を挿入
ここに画像の説明を挿入

アルゴリズムの練習

import java.util.*;

public class KruskalAlgorithm {
    
    
    private static class Edge implements Comparable<Edge> {
    
    
        int u, v, w;

        public Edge(int u, int v, int w) {
    
    
            this.u = u;
            this.v = v;
            this.w = w;
        }

        @Override
        public int compareTo(Edge o) {
    
    
            return Integer.compare(this.w, o.w);
        }
    }

    public static int kruskal(int[][] graph) {
    
    
        int n = graph.length;
        List<Edge> edges = new ArrayList<>();
        for (int u = 0; u < n; u++) {
    
    
            for (int v = u + 1; v < n; v++) {
    
    
                if (graph[u][v] != 0) {
    
    
                    edges.add(new Edge(u, v, graph[u][v]));
                }
            }
        }
        Collections.sort(edges);
        int[] parent = new int[n];
        for (int i = 0; i < n; i++) {
    
    
            parent[i] = i;
        }
        int res = 0;
        for (Edge edge : edges) {
    
    
            int u = edge.u;
            int v = edge.v;
            int w = edge.w;
            int pu = find(parent, u);
            int pv = find(parent, v);
            if (pu != pv) {
    
    
                parent[pu] = pv;
                res += w;
            }
        }
        return res;
    }

    private static int find(int[] parent, int x) {
    
    
        if (parent[x] != x) {
    
    
            parent[x] = find(parent, parent[x]);
        }
        return parent[x];
    }

    public static void main(String[] args) {
    
    
        int[][] graph = new int[][]{
    
    
                {
    
    0, 2, 0, 6, 0},
                {
    
    2, 0, 3, 8, 5},
                {
    
    0, 3, 0, 0, 7},
                {
    
    6, 8, 0, 0, 9},
                {
    
    0, 5, 7, 9, 0}
        };
        int res = kruskal(graph);
        System.out.println(res);
    }
}

この実装では、まずグラフ内のすべてのエッジをリストに格納し、重みの昇順に並べ替えます。次に、ユニオン検索を初期化し、各ノードの親をそれ自体に初期化します。

次に、ソートされたエッジ リストを走査します。各エッジについて、その 2 つの端点が同じ接続ブロック内にない場合は、それらを同じ接続ブロックにマージし、このエッジの重みを最小スパニング ツリーの重みに追加します。

この実装の時間計算量は O(m log m) です。ここで、m はエッジの数です。

最短経路アルゴリズム

グラフ理論の最短距離 (Shortest Path) アルゴリズムのアニメーションのデモ - Dijkstra (Dijkstra) と Floyd (Floyd)

ダイクストラのアルゴリズム

ダイクストラのアルゴリズムは、ノードから他のノードへの最短パスを計算するために使用される典型的な最短パス アルゴリズムです。その最大の特徴は、開始点から外側の層へ拡張し(幅優先探索の考え方)、最後まで拡張することです。

ダイクストラのアルゴリズムは、単一ソースの最短経路を見つけるためのアルゴリズムです。その基本的な考え方は次のとおりです。

  1. 開始ノードとしてノードを選択し、開始ノードから他のノードまでの最短パスを計算します。
  2. 開始ノードから到達可能なすべてのノードをたどり、最短パスを更新します。次のノードを選択して、すべてのノードを通過するまで通過を続けます。
  3. 開始ノードからすべてのノードまでの最短パスが最終的に得られるまで、上記の手順を繰り返します。

ダイクストラのアルゴリズムの手順は次のとおりです。

  1. 開始ノード ソースを選択し、その距離を 0 に設定し、他のノードの距離を無限大に設定します。
  2. S セットに含まれず、距離が最小のノード u を見つけます。その距離は dist[u] です。
  3. u を S セットに追加し、u が訪問されたことを示します。
  4. u を中間ノードとして、その隣接ノード v までの距離を更新します。dist[v] = min(dist[v], dist[u] + 重み(u, v))。
  5. S にすべてのノードが含まれるまで、手順 2、3、および 4 を繰り返します。
  6. 各ノードの最短経路と距離を出力します。

ダイクストラのアルゴリズムは、配列 dist を使用して各ノードの最短パス長を記録し、小さなルート ヒープ優先キューを使用して最小距離のノードを見つけます。時間と空間の複雑さはそれぞれ O(nlogn) と O(n) です。

アルゴリズムの練習

import java.util.*;

public class DijkstraAlgorithm {
    
    
    private static final int INF = Integer.MAX_VALUE;

    public static int[] dijkstra(int[][] graph, int start) {
    
    
        int n = graph.length;
        int[] dist = new int[n];
        boolean[] visited = new boolean[n];
        Arrays.fill(dist, INF);
        dist[start] = 0;

        for (int i = 0; i < n; i++) {
    
    
            int u = -1;
            for (int j = 0; j < n; j++) {
    
    
                if (!visited[j] && (u == -1 || dist[j] < dist[u])) {
    
    
                    u = j;
                }
            }
            visited[u] = true;
            for (int v = 0; v < n; v++) {
    
    
                if (!visited[v] && graph[u][v] != INF && dist[u] + graph[u][v] < dist[v]) {
    
    
                    dist[v] = dist[u] + graph[u][v];
                }
            }
        }

        return dist;
    }

    public static void main(String[] args) {
    
    
        int[][] graph = new int[][]{
    
    
                {
    
    0, 2, INF, 6, INF},
                {
    
    2, 0, 3, 8, 5},
                {
    
    INF, 3, 0, INF, 7},
                {
    
    6, 8, INF, 0, 9},
                {
    
    INF, 5, 7, 9, 0}
        };
        int start = 0;
        int[] dist = dijkstra(graph, start);
        System.out.println(Arrays.toString(dist));
    }
}

この実装では、まず距離配列 dist と訪問フラグ配列 Visited を初期化し、距離配列のすべての要素を無限大 (訪問されていないノードを表す) に初期化します。次に、開始点からすべてのノードを走査し、そのたびに距離配列の最小値を次に訪問するノードとして選択します。次に、このノードを訪問済みとしてマークし、距離配列内のまだ訪問していないノードまでの距離を更新します。最後に、距離配列を返します。

この実装の時間計算量は O(n^2) です。ここで、n はノードの数です。優先キューを使用して最適化すると、時間計算量は O(m log n) に削減できます (m はエッジの数)。

フロイドのアルゴリズム

フロイドのアルゴリズムは、ノードのすべてのペア間の最短パスを見つけるためのアルゴリズムです。その基本的な考え方は、
再帰によって各ノードから他のすべてのノードへの最短パスを見つけることです。
フロイドのアルゴリズムの手順は次のとおりです。

  1. dist[i][j]ノード i からノード j までの最短パス長を表すdist 配列を初期化します。このときi==jdist[i][j] = 0ノード i とノード j の間に直接パスがある場合はdist[i][j]パス長、そうでない場合はパス長となりますdist[i][j] = ∞
  2. 各中間ノード k をトラバースして、dist 配列を更新します。
    dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
    これは、ノード i からノード j への最短パスがノード k を通過できることを意味します。
  3. すべての中間ノードを通過するまでステップ 2 を繰り返します。
  4. dist 配列の最終結果は、各ノード ペア間の最短パス長です。

フロイドのアルゴリズムの時間計算量と空間計算量は両方とも O(n3) です。ここで、n はノードの数です。

フロイドのアルゴリズムは、任意の 2 つのノード間の最短経路を解くのに適しており、有向グラフと重み付きグラフの最短経路問題を解くことができます。

ダイクストラのアルゴリズムとフロイドのアルゴリズムはどちらも最短経路問題を解くための古典的なアルゴリズムですが、次のような主な違いがあります。

  1. 適用可能な図のタイプ:
    • ダイクストラのアルゴリズムは、有向グラフまたは無向グラフの単一ソースの最短経路を見つけるためにのみ使用でき、2 点間の最短経路を見つけることはできません。
    • フロイドのアルゴリズムを使用すると、有向グラフまたは無向グラフ内の任意の 2 点間の最短経路を見つけることができます。
  2. 最短パスの種類:
    • ダイクストラのアルゴリズムは最短パス ツリーを見つけますが、単一のソース ポイントから他のポイントへの最短パスのみを取得できます。
    • フロイドアルゴリズムは、すべてのノード間の最短経路を一度に計算し、最短経路行列を取得します。
  3. 時間計算量:
    • ダイクストラのアルゴリズムは優先キューを使用して実装され、時間計算量は O(nlogn) です。
    • フロイドのアルゴリズムの時間計算量は O(n3) です。
    • グラフ内のノードの数が多くてもエッジの数が少ない場合は、ダイクストラのアルゴリズムの方が効率的です。グラフのノードとエッジの数が多い場合、フロイドのアルゴリズムの方が効率的です。
  4. 空間の複雑さ:
    • ダイクストラのアルゴリズムは O(n) 空間のみを必要とします。
    • フロイドのアルゴリズムでは、最短パス行列を保存するために O(n2) スペースが必要です。
  5. 中間ノードは必要ですか:
    • ダイクストラのアルゴリズムは、最短パスを更新するときに、中間ノード情報を考慮せず、始点から終点までの最短パスのみを考慮します。
    • フロイドのアルゴリズムでは、最短パスを更新するときに中間ノード情報が必要ですが、最短パスは中間ノード ジャンプを通じてのみ更新できます。

アルゴリズムの練習

import java.util.*;

public class FloydAlgorithm {
    
    
    private static final int INF = Integer.MAX_VALUE;

    public static int[][] floyd(int[][] graph) {
    
    
        int n = graph.length;
        int[][] dist = new int[n][n];
        for (int i = 0; i < n; i++) {
    
    
            for (int j = 0; j < n; j++) {
    
    
                dist[i][j] = graph[i][j];
            }
        }
        for (int k = 0; k < n; k++) {
    
    
            for (int i = 0; i < n; i++) {
    
    
                for (int j = 0; j < n; j++) {
    
    
                    if (dist[i][k] != INF && dist[k][j] != INF && dist[i][k] + dist[k][j] < dist[i][j]) {
    
    
                        dist[i][j] = dist[i][k] + dist[k][j];
                    }
                }
            }
        }
        return dist;
    }

    public static void main(String[] args) {
    
    
        int[][] graph = new int[][]{
    
    
                {
    
    0, 2, INF, 6, INF},
                {
    
    2, 0, 3, INF, INF},
                {
    
    INF, 3, 0, 4, INF},
                {
    
    6, INF, 4, 0, 8},
                {
    
    INF, INF, INF, 8, 0}
        };
        int[][] dist = floyd(graph);
        for (int i = 0; i < dist.length; i++) {
    
    
            System.out.println(Arrays.toString(dist[i]));
        }
    }
}

この実装では、最初にグラフ内の隣接行列を距離行列にコピーします。次に、すべてのノード ペア (i,j) を反復処理して、(i,j) からノード k までの距離を縮めようとします。ノード k を通過することで距離を短縮できる場合は、距離行列の (i, j) 要素を更新します。

この実装の時間計算量は O(n^3) です。ここで、n はノードの数です。

おすすめ

転載: blog.csdn.net/zyb18507175502/article/details/130881189