Algoritma Kruskal untuk Memanjangkan Pokok dengan Pelaksanaan Java

1. Gambaran keseluruhan

Dalam artikel sebelumnya, kami memperkenalkan algoritma Prim untuk mencari pokok minimum. Dalam artikel ini, kita akan menggunakan pendekatan lain, algoritma Kruskal, untuk menyelesaikan masalah pokok minimum dan maksimum.

2. Pokok Rentang

Pohon yang merangkumi graf yang tidak diarahkan adalah subgraf yang bersambung yang merangkumi semua nod graf dengan bilangan tepi minimum yang mungkin. Secara amnya, grafik mungkin mempunyai lebih daripada satu pokok rentang. Gambar berikut menunjukkan graf dengan pokok rentang (tepi pokok rentang berwarna merah):

Sekiranya graf mempunyai berat tepi, kita dapat menentukan berat pokok rentang sebagai jumlah berat semua tepinya. Pokok rentang minimum adalah pokok rentang yang beratnya paling kecil di antara semua pokok rentang yang mungkin. Gambar berikut menunjukkan pokok rentang minimum pada graf berwajaran tepi:

Begitu juga, pokok rentang maksimum mempunyai berat terbesar di antara semua pokok rentang. Gambar berikut menunjukkan pokok rentang maksimum pada graf berwajaran tepi:

3. Algoritma Kruskal

Dengan memberikan graf, kita dapat menggunakan algoritma Kruskal untuk mencari pokok rentang minimumnya. Sekiranya bilangan simpul dalam grafik adalah V , maka setiap pokok rentangnya harus mempunyai tepi (V-1) dan tidak mengandungi kitaran. Kita dapat menerangkan algoritma Kruskal dalam kod pseudo berikut:

Initialize an empty edge set T. Sort all graph edges by the ascending order of their weight values. foreach edge in the sorted edge list Check whether it will create a cycle with the edges inside T. If the edge doesn't introduce any cycles, add it into T. If T has (V-1) edges, exit the loop. return T

Mari jalankan algoritma Kruskal untuk pokok rentang minimum pada grafik contoh kami langkah demi langkah:

Pertama, kita memilih pinggir (0, 2) kerana mempunyai berat terkecil. Kemudian, kita boleh menambah tepi (3, 4) dan (0, 1) kerana mereka tidak membuat kitaran. Sekarang calon seterusnya adalah kelebihan (1, 2) dengan berat 9. Namun, jika kita memasukkan kelebihan ini, kita akan menghasilkan satu kitaran (0, 1, 2). Oleh itu, kami membuang kelebihan ini dan terus memilih yang terkecil seterusnya. Akhirnya, algoritma selesai dengan menambahkan kelebihan (2, 4) berat 10.

Untuk mengira pokok rentang maksimum, kita dapat mengubah urutan penyortiran ke urutan menurun. Langkah-langkah lain tetap sama. Gambar berikut menunjukkan pembinaan langkah demi langkah pokok rentang maksimum pada grafik sampel kami.

4. Pengesanan Kitaran dengan Set Disjoint

Dalam algoritma Kruskal, bahagian pentingnya adalah untuk memeriksa sama ada kelebihan akan membuat kitaran jika kita menambahkannya ke set tepi yang ada. Terdapat beberapa algoritma pengesanan kitaran grafik yang boleh kita gunakan. Sebagai contoh, kita boleh menggunakan algoritma carian pertama-mendalam (DFS) untuk melintasi grafik dan mengesan sama ada terdapat kitaran.

Walau bagaimanapun, kita perlu melakukan pengesanan kitaran pada tepi yang ada setiap kali kita menguji kelebihan baru. Penyelesaian yang lebih pantas adalah dengan menggunakan algoritma Union-Find dengan struktur data yang terpisah kerana ia juga menggunakan pendekatan penambahan kelebihan tambahan untuk mengesan kitaran. Kami dapat memasukkannya ke dalam proses pembinaan pokok rentang kami.

4.1. Set Disjoint dan Pembinaan Pokok Rentang

Pertama, kami memperlakukan setiap nod grafik sebagai set individu yang mengandungi hanya satu nod. Kemudian, setiap kali kami memperkenalkan kelebihan, kami memeriksa sama ada dua nodnya berada dalam satu set yang sama. Sekiranya jawapannya adalah ya, maka ia akan mewujudkan satu kitaran. Jika tidak, kami menggabungkan dua set terputus menjadi satu set dan menyertakan tepi untuk pokok rentang.

Kita boleh mengulangi langkah di atas sehingga kita membina keseluruhan pokok rentang.

Sebagai contoh, dalam pembinaan pokok rentang minimum di atas, pertama kami mempunyai 5 set nod: {0}, {1}, {2}, {3}, {4}. Semasa kita memeriksa pinggir pertama (0, 2), dua nodnya berada dalam set nod yang berbeza. Oleh itu, kita boleh memasukkan kelebihan ini dan menggabungkan {0} dan {2} menjadi satu set {0, 2}.

Kita boleh melakukan operasi serupa untuk bahagian tepi (3, 4) dan (0, 1). Set nod kemudian menjadi {0, 1, 2} dan {3, 4}. Semasa kita memeriksa tepi seterusnya (1, 2), kita dapat melihat bahawa kedua-dua nod tepi ini berada dalam satu set yang sama. Oleh itu, kami membuang kelebihan ini dan terus memeriksa yang berikutnya. Akhirnya, tepi (2, 4) memenuhi keadaan kita, dan kita boleh memasukkannya untuk pokok rentang minimum.

4.2. Pelaksanaan Set Disjoint

Kita boleh menggunakan struktur pokok untuk mewakili set terasing. Setiap nod mempunyai penunjuk induk untuk merujuk nod induknya. Di setiap set, terdapat simpul akar unik yang mewakili set ini. Node akar mempunyai penunjuk induk yang dirujuk sendiri .

Mari gunakan kelas Java untuk menentukan maklumat set disjoint:

public class DisjointSetInfo { private Integer parentNode; DisjointSetInfo(Integer parent) { setParentNode(parent); } //standard setters and getters }

Mari label setiap nod graf dengan nombor bulat, bermula dari 0. Kita boleh menggunakan struktur data senarai, Node senarai , untuk menyimpan maklumat set takrif bagi graf. Pada mulanya, setiap simpul adalah anggota wakil dari setnya sendiri:

void initDisjointSets(int totalNodes) { nodes = new ArrayList(totalNodes); for (int i = 0; i < totalNodes; i++) { nodes.add(new DisjointSetInfo(i)); } } 

4.3. Cari Operasi

Untuk mencari set yang menjadi node, kita dapat mengikuti rantai induk node ke atas sehingga kita sampai ke simpul akar:

Integer find(Integer node) { Integer parent = nodes.get(node).getParentNode(); if (parent.equals(node)) { return node; } else { return find(parent); } }

Adalah mungkin untuk mempunyai struktur pokok yang sangat tidak seimbang untuk set disjoint. Kita dapat meningkatkan operasi cari dengan menggunakan teknik pemampatan p ath .

Oleh kerana setiap simpul yang kita lawati dalam perjalanan ke simpul akar adalah bahagian dari set yang sama, kita dapat melampirkan simpul akar ke rujukan induknya secara langsung. Kali seterusnya apabila kita mengunjungi nod ini, kita memerlukan satu jalan pencarian untuk mendapatkan simpul akar:

Integer pathCompressionFind(Integer node) { DisjointSetInfo setInfo = nodes.get(node); Integer parent = setInfo.getParentNode(); if (parent.equals(node)) { return node; } else { Integer parentNode = find(parent); setInfo.setParentNode(parentNode); return parentNode; } }

4.4. Operasi Kesatuan

Sekiranya kedua-dua nod tepi berada dalam set yang berbeza, kami akan menggabungkan kedua-dua set ini menjadi satu. Kita boleh mencapai operasi penyatuan ini dengan menetapkan akar satu nod wakil ke nod wakil yang lain:

void union(Integer rootU, Integer rootV) { DisjointSetInfo setInfoU = nodes.get(rootU); setInfoU.setParentNode(rootV); }

Operasi penyatuan sederhana ini dapat menghasilkan pokok yang sangat tidak seimbang kerana kami memilih simpul akar rawak untuk kumpulan bergabung. Kita dapat meningkatkan prestasi menggunakan teknik penyatuan dengan peringkat .

Since it is tree depth that affects the running time of the find operation, we attach the set with the shorter tree to the set with the longer tree. This technique only increases the depth of the merged tree if the original two trees have the same depth.

To achieve this, we first add a rank property to the DisjointSetInfo class:

public class DisjointSetInfo { private Integer parentNode; private int rank; DisjointSetInfo(Integer parent) { setParentNode(parent); setRank(0); } //standard setters and getters }

In the beginning, a single node disjoint has a rank of 0. During the union of two sets, the root node with a higher rank becomes the root node of the merged set. We increase the new root node's rank by one only if the original two ranks are the same:

void unionByRank(int rootU, int rootV) { DisjointSetInfo setInfoU = nodes.get(rootU); DisjointSetInfo setInfoV = nodes.get(rootV); int rankU = setInfoU.getRank(); int rankV = setInfoV.getRank(); if (rankU < rankV) { setInfoU.setParentNode(rootV); } else { setInfoV.setParentNode(rootU); if (rankU == rankV) { setInfoU.setRank(rankU + 1); } } }

4.5. Cycle Detection

We can determine whether two nodes are in the same disjoint set by comparing the results of two find operations. If they have the same representive root node, then we've detected a cycle. Otherwise, we merge the two disjoint sets by using a union operation:

boolean detectCycle(Integer u, Integer v) { Integer rootU = pathCompressionFind(u); Integer rootV = pathCompressionFind(v); if (rootU.equals(rootV)) { return true; } unionByRank(rootU, rootV); return false; } 

The cycle detection, with the union by rank technique alone, has a running time of O(logV). We can achieve better performance with both path compression and union by rank techniques. The running time is O(α(V)), where α(V) is the inverse Ackermann function of the total number of nodes. It is a small constant that is less than 5 in our real-world computations.

5. Java Implementation of Kruskal’s Algorithm

We can use the ValueGraph data structure in Google Guava to represent an edge-weighted graph.

To use ValueGraph, we first need to add the Guava dependency to our project's pom.xml file:

     com.google.guava     guava     28.1-jre 

We can wrap the above cycle detection methods into a CycleDetector class and use it in Kruskal's algorithm. Since the minimum and maximum spanning tree construction algorithms only have a slight difference, we can use one general function to achieve both constructions:

ValueGraph spanningTree(ValueGraph graph, boolean minSpanningTree) { Set edges = graph.edges(); List edgeList = new ArrayList(edges); if (minSpanningTree) { edgeList.sort(Comparator.comparing(e -> graph.edgeValue(e).get())); } else { edgeList.sort(Collections.reverseOrder(Comparator.comparing(e -> graph.edgeValue(e).get()))); } int totalNodes = graph.nodes().size(); CycleDetector cycleDetector = new CycleDetector(totalNodes); int edgeCount = 0; MutableValueGraph spanningTree = ValueGraphBuilder.undirected().build(); for (EndpointPair edge : edgeList) { if (cycleDetector.detectCycle(edge.nodeU(), edge.nodeV())) { continue; } spanningTree.putEdgeValue(edge.nodeU(), edge.nodeV(), graph.edgeValue(edge).get()); edgeCount++; if (edgeCount == totalNodes - 1) { break; } } return spanningTree; }

In Kruskal's algorithm, we first sort all graph edges by their weights. This operation takes O(ElogE) time, where E is the total number of edges.

Kemudian kami menggunakan gelung untuk melihat senarai tepi yang disusun. Dalam setiap lelaran, kami memeriksa sama ada satu kitaran akan dibentuk dengan menambahkan pinggir ke dalam set tepi pokok rentang semasa. Gelung ini dengan pengesanan kitaran memerlukan masa paling banyak O (ElogV) .

Oleh itu, keseluruhan masa berjalan adalah O (ELogE + ELogV) . Oleh kerana nilai E berada dalam skala O (V2) , kerumitan masa algoritma Kruskal adalah O (ElogE) atau O (ElogV) .

6. Kesimpulannya

Dalam artikel ini, kami belajar bagaimana menggunakan algoritma Kruskal untuk mencari pokok graf minimum atau maksimum. Seperti biasa, kod sumber untuk artikel tersebut terdapat di GitHub.