Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions docs/graph/boruvka.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Boruvka法

無向重み付きグラフの最小全域木(MST)の総コストを求めるアルゴリズム。各連結成分が同時に「自分から出る最小コストの辺」を選び、それらを一斉に併合する操作を繰り返す。1ラウンドで連結成分数が少なくとも半分になるため、$O(\log V)$ ラウンド・各ラウンド $O(E)$ で全体 $O(E \log V)$ を実現する。3つのMSTアルゴリズムの中で最古(1926年)で、各成分の最小辺探索が独立に行えるため並列化・分散処理と相性が良い。

## アルゴリズム

1. 各頂点を1つの連結成分とする(Union-Find で管理、[unionfind.md](../datastructure/unionfind.md)参照)。
2. 各ラウンドで、すべての辺を走査し、連結成分ごとに「その成分から別の成分へ出る最小コストの辺」を求める。
3. 求めた各辺を Union-Find で併合する(既に同じ成分同士なら閉路になるのでスキップ)。
4. 連結成分が1つになるまで繰り返す。あるラウンドで1本も併合できなければ、グラフは連結ではなく全域木は存在しない。

### 同コスト辺の扱い

複数の辺が同じコストを持つ場合、素朴に各成分の最小辺を選ぶと、同一ラウンドで選ばれた辺どうしが閉路を作り、正しくない結果になることがある。本実装では比較を「コスト → 辺のインデックス」の順に行い、全順序を与える(実質的にすべての辺のコストを相異なるものとして扱う)。これにより、同一ラウンドで選ばれる辺が互いに閉路を作らないことが保証される。

## 計算量

| | 時間 | 空間 |
|---|---|---|
| | $O(E \log V)$ | $O(V + E)$ |

## インターフェース

```cpp
#include "toolbox/graph/mst/boruvka.hpp"

// long long 特殊化
std::optional<long long> toolbox::graph::boruvka(
int n,
const std::vector<std::tuple<long long, long long, long long>> &edges
);

// 汎用版
template <typename Vertex, typename Cost>
std::optional<Cost> toolbox::graph::boruvka(
int n,
const std::vector<std::tuple<Cost, Vertex, Vertex>> &edges
);
```

- `n`: 頂点数
- `edges[i]`: `{コスト, 端点u, 端点v}` の辺
- 戻り値: 最小全域木の総コスト。グラフが連結でない場合は `std::nullopt`

### 制約

- `n > 0`

## 使用例

```cpp
#include "toolbox/graph/mst/boruvka.hpp"

int n = 4;
std::vector<std::tuple<long long, long long, long long>> edges = {
{1, 0, 1}, {2, 1, 2}, {1, 2, 3}, {4, 0, 3}, {3, 0, 2},
};
auto total = toolbox::graph::boruvka(n, edges);
// total == 4 (辺 0-1(1) + 2-3(1) + 1-2(2))
```

## 実装上の注意

- 辺リスト形式は Kruskal法([kruskal.md](kruskal.md))と共通。隣接リスト形式から MST を求めたい場合は Prim法([prim.md](prim.md))が使える。
- Kruskal/Prim と同様、採用された辺そのもの(全域木の構成)は返さず、総コストのみを返す。
- 多重辺・自己ループがあっても正しく動作する(自己ループは常にスキップされる)。

## 参考文献
62 changes: 62 additions & 0 deletions docs/graph/kruskal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Kruskal法

無向重み付きグラフの最小全域木(MST)の総コストを求めるアルゴリズム。辺をコストの昇順にソートし、閉路を作らない辺だけを貪欲に採用する。Union-Find([unionfind.md](../datastructure/unionfind.md)参照)を使って閉路判定を $O(\alpha(n))$ で行うことで、全体を $O(E \log E)$ で実現する。

## アルゴリズム

1. 全ての辺をコストの昇順にソートする。
2. コストの小さい辺から順に見て、その辺の両端点が既に同じ集合に属していなければ(閉路を作らなければ)採用し、Union-Findで両端点を併合する。属していれば(閉路を作るなら)採用しない。
3. 採用した辺が $n-1$ 本になれば全域木が完成。最後まで $n-1$ 本に届かなければ、グラフは連結ではなく全域木は存在しない。

## 計算量

| | 時間 | 空間 |
|---|---|---|
| | $O(E \log E)$ | $O(V + E)$ |

## インターフェース

```cpp
#include "toolbox/graph/mst/kruskal.hpp"

// long long 特殊化
std::optional<long long> toolbox::graph::kruskal(
int n,
std::vector<std::tuple<long long, long long, long long>> edges
);

// 汎用版
template <typename Vertex, typename Cost>
std::optional<Cost> toolbox::graph::kruskal(
int n,
std::vector<std::tuple<Cost, Vertex, Vertex>> edges
);
```

- `n`: 頂点数
- `edges[i]`: `{コスト, 端点u, 端点v}` の辺
- 戻り値: 最小全域木の総コスト。グラフが連結でない場合は `std::nullopt`

### 制約

- `n > 0`

## 使用例

```cpp
#include "toolbox/graph/mst/kruskal.hpp"

int n = 4;
std::vector<std::tuple<long long, long long, long long>> edges = {
{1, 0, 1}, {2, 1, 2}, {1, 2, 3}, {4, 0, 3}, {3, 0, 2},
};
auto total = toolbox::graph::kruskal(n, edges);
// total == 4 (辺 0-1(1) + 2-3(1) + 1-2(2))
```

## 実装上の注意

- 採用された辺そのもの(全域木の構成)は返さず、総コストのみを返す。これは本ライブラリの `dijkstra`/`bellman_ford` が最短経路そのものを復元しない(距離のみ返す)のと同じ方針。全域木の辺集合が必要な場合は、`unionfind::unite` が成功した辺を呼び出し側で記録すればよい。
- 同じ2頂点を結ぶ多重辺があっても正しく動作する(コストの小さい方が採用される)。

## 参考文献
71 changes: 71 additions & 0 deletions docs/graph/prim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Prim法

無向重み付きグラフの最小全域木(MST)の総コストを求めるアルゴリズム。1つの頂点から始め、既に木に含まれる頂点集合と、まだ含まれない頂点集合の間を結ぶ辺のうち最小コストのものを繰り返し追加して木を成長させる。優先度付きキューを使うことで $O(E \log V)$ を実現する。

## アルゴリズム

1. 頂点 $0$ を始点として木に加える。
2. 木に含まれる頂点から出る辺を優先度付きキュー(コスト昇順)で管理し、最小コストの辺を取り出す。
3. その辺の行き先がまだ木に含まれていなければ、その頂点を木に加え、コストを加算し、その頂点から出る辺をキューに追加する。既に含まれていればスキップする(遅延削除)。
4. 全頂点が木に含まれれば完成。キューが空になっても全頂点に到達できなければ、グラフは連結ではなく全域木は存在しない。

木の成長を優先度付きキューで進める点はDijkstra法([dijkstra.md](dijkstra.md))とほぼ同じ構造で、キューに入れる値が「始点からの累積距離」ではなく「木に加えるための辺1本のコスト」である点だけが異なる。

## 計算量

| | 時間 | 空間 |
|---|---|---|
| | $O(E \log V)$ | $O(V + E)$ |

## インターフェース

```cpp
#include "toolbox/graph/mst/prim.hpp"

// long long 特殊化
std::optional<long long> toolbox::graph::prim(
const std::vector<std::vector<std::pair<long long, long long>>> &cost
);

// 汎用版
template <typename Vertex, typename Cost>
std::optional<Cost> toolbox::graph::prim(
const std::vector<std::vector<std::pair<Vertex, Cost>>> &cost
);
```

- `cost[u]`: 頂点 `u` から出る `{隣接頂点, 辺コスト}` のリスト(無向グラフなので両方向に辺を張る)
- 戻り値: 最小全域木の総コスト。グラフが連結でない場合は `std::nullopt`

### 制約

- `cost.size() > 0`(頂点数が1以上)

## 使用例

```cpp
#include "toolbox/graph/mst/prim.hpp"

int n = 4;
std::vector<std::vector<std::pair<long long, long long>>> g(n);
auto add_edge = [&](int u, int v, long long w) {
g[u].push_back({v, w});
g[v].push_back({u, w});
};
add_edge(0, 1, 1);
add_edge(1, 2, 2);
add_edge(2, 3, 1);
add_edge(0, 3, 4);
add_edge(0, 2, 3);

auto total = toolbox::graph::prim(g);
// total == 4 (辺 0-1(1) + 2-3(1) + 1-2(2))
```

## 実装上の注意

- 隣接リスト表現(Dijkstraと同じ形式)を入力とする。辺リスト形式で最小全域木を求めたい場合は Kruskal法([kruskal.md](kruskal.md))が使える。
- Kruskalと同様、採用された辺そのもの(全域木の構成)は返さず、総コストのみを返す。
- 多重辺・自己ループがあっても正しく動作する。

## 参考文献
3 changes: 3 additions & 0 deletions includes/toolbox/graph/graph.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#pragma once
#include "toolbox/graph/directed/topological.hpp"
#include "toolbox/graph/mst/boruvka.hpp"
#include "toolbox/graph/mst/kruskal.hpp"
#include "toolbox/graph/mst/prim.hpp"
#include "toolbox/graph/other/diameter_tree.hpp"
#include "toolbox/graph/shortest_path/bellman_ford.hpp"
#include "toolbox/graph/shortest_path/dijkstra.hpp"
103 changes: 103 additions & 0 deletions includes/toolbox/graph/mst/boruvka.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#pragma once

#include <optional>
#include <tuple>
#include <vector>

#include "toolbox/datastructure/unionfind/unionfind.hpp"

namespace toolbox {

namespace graph {

/**
* @brief Boruvka's algorithm for finding a minimum spanning tree.
* @tparam Vertex Type of vertex
* @tparam Cost Type of cost
* @param n Number of vertices
* @param edges Edge list, each edge given as {cost, u, v}
* @return The total cost of the minimum spanning tree, or std::nullopt if the graph is not
* connected (no spanning tree exists).
* @note [constraint]: n > 0
* @note [complexity]: O(E log V). Each round every component picks its cheapest outgoing edge
* and those edges are merged in; the number of components at least halves per round, so there
* are O(log V) rounds and each round scans all edges in O(E).
* @note Uses toolbox::datastructure::unionfind for components; like kruskal/prim (see
* kruskal.hpp, prim.hpp), it returns only the total cost, not the selected edges.
*/
template <typename Vertex, typename Cost>
std::optional<Cost> boruvka(int n, const std::vector<std::tuple<Cost, Vertex, Vertex>> &edges) {
const int m = static_cast<int>(edges.size());
datastructure::unionfind uf(n);
Cost total = Cost();
int num_components = n;

// A consistent tie-break (cost first, then edge index) makes every edge effectively
// unique. This guarantees the edges chosen by different components within a single round
// can never form a cycle among themselves, so Boruvka stays correct even when several
// edges share the same weight.
const auto cheaper = [&edges](const int lhs, const int rhs) {
if (std::get<0>(edges[lhs]) != std::get<0>(edges[rhs])) {
return std::get<0>(edges[lhs]) < std::get<0>(edges[rhs]);
}
return lhs < rhs;
};

while (num_components > 1) {
// For each component (identified by its union-find representative), the index of the
// cheapest edge leaving it, or -1 if the component has no outgoing edge this round.
std::vector<int> cheapest(n, -1);
for (int i = 0; i < m; ++i) {
const int cu = uf.find(static_cast<int>(std::get<1>(edges[i])));
const int cv = uf.find(static_cast<int>(std::get<2>(edges[i])));
if (cu == cv) {
continue; // both endpoints already merged (also skips self-loops)
}
if (cheapest[cu] == -1 || cheaper(i, cheapest[cu])) {
cheapest[cu] = i;
}
if (cheapest[cv] == -1 || cheaper(i, cheapest[cv])) {
cheapest[cv] = i;
}
}

bool progress = false;
for (int c = 0; c < n; ++c) {
if (cheapest[c] == -1) {
continue;
}
const auto &[cost, u, v] = edges[cheapest[c]];
if (uf.unite(static_cast<int>(u), static_cast<int>(v))) {
total += cost;
--num_components;
progress = true;
}
}
if (!progress) {
break; // no component could grow: the graph is disconnected
}
}

if (num_components != 1) {
return std::nullopt;
}
return total;
}

/**
* @brief Boruvka's algorithm for finding a minimum spanning tree.
* @param n Number of vertices
* @param edges Edge list, each edge given as {cost, u, v}
* @return The total cost of the minimum spanning tree, or std::nullopt if the graph is not
* connected (no spanning tree exists).
* @note [constraint]: n > 0
* @note [complexity]: O(E log V)
*/
std::optional<long long> boruvka(
int n, const std::vector<std::tuple<long long, long long, long long>> &edges) {
return boruvka<long long, long long>(n, edges);
}

} // namespace graph

} // namespace toolbox
63 changes: 63 additions & 0 deletions includes/toolbox/graph/mst/kruskal.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#pragma once

#include <algorithm>
#include <optional>
#include <tuple>
#include <vector>

#include "toolbox/datastructure/unionfind/unionfind.hpp"

namespace toolbox {

namespace graph {

/**
* @brief Kruskal's algorithm for finding a minimum spanning tree.
* @tparam Vertex Type of vertex
* @tparam Cost Type of cost
* @param n Number of vertices
* @param edges Edge list, each edge given as {cost, u, v}
* @return The total cost of the minimum spanning tree, or std::nullopt if the graph is not
* connected (no spanning tree exists).
* @note [constraint]: n > 0
* @note [complexity]: O(E log E), dominated by sorting the edges.
* @note Uses toolbox::datastructure::unionfind to detect cycles; does not reconstruct the
* selected edges themselves (only their total cost), matching the level of detail returned by
* dijkstra/bellman_ford in this library.
*/
template <typename Vertex, typename Cost>
std::optional<Cost> kruskal(int n, std::vector<std::tuple<Cost, Vertex, Vertex>> edges) {
std::sort(edges.begin(), edges.end(),
[](const auto &a, const auto &b) { return std::get<0>(a) < std::get<0>(b); });
datastructure::unionfind uf(n);
Cost total = Cost();
int edges_used = 0;
for (const auto &[cost, u, v] : edges) {
if (uf.unite(static_cast<int>(u), static_cast<int>(v))) {
total += cost;
++edges_used;
}
}
if (edges_used != n - 1) {
return std::nullopt;
}
return total;
}

/**
* @brief Kruskal's algorithm for finding a minimum spanning tree.
* @param n Number of vertices
* @param edges Edge list, each edge given as {cost, u, v}
* @return The total cost of the minimum spanning tree, or std::nullopt if the graph is not
* connected (no spanning tree exists).
* @note [constraint]: n > 0
* @note [complexity]: O(E log E)
*/
std::optional<long long> kruskal(int n,
std::vector<std::tuple<long long, long long, long long>> edges) {
return kruskal<long long, long long>(n, edges);
}

} // namespace graph

} // namespace toolbox
Loading
Loading