素集合 (Union-Find) で ケーブルで つながった パソコンの 組を数える
パソコンの
海外で定番の問題リスト 4 本には入っていません
問題
事務所に n 台のcables をcables[i] = [a, b] は、
- 1 本も
ケーブルが つながっていない パソコンも、 それだけで 1 つの ネットワークです - n は 1 以上 1000 以下で、
cablesの長さは 0 以上 3000 以下です - 自分
自身に つなぐ ケーブルは ありません。 同じ 2 台を 結ぶ ケーブルが 2 本以上 あってもかまいません
入力と出力の例
例 1
0、
入力
n = 6
cables = [[0, 3], [3, 5],
[1, 4]]出力
3例 2
0、
入力
n = 5
cables = [[2, 0], [0, 4],
[4, 2], [1, 3], [3, 1]]出力
2例 3
はじめは 2
入力
n = 8
cables = [[0, 1], [2, 3],
[4, 5], [6, 7], [1, 2],
[5, 6], [3, 4]]出力
1図で見る
例 1 で、
面接での聞かれ方
面接では、
- 「台数と
つなぎ方の 一覧が 渡されます。 つながっている 台のかたまりが いくつあるか 数えてください」 - 書き終えた
あとに 「深さ優先でも 解けますが、 素集合に すると 何が うれしいですか?」と 聞かれます - 「ケーブルが 1 本増える
たびに、 いまの ネットワークの 数を 答えてください」と、 つなぎ足していく 形に 広げられる こともあります - 「全部を 1 つの
ネットワークに するには、 あと 何本ケーブルが 要りますか?」と 聞かれる こともあります (答えは、 ネットワークの 数 - 1 本です)
台の
ヒント
まだ開いていません
付箋は 4 枚。下の段ほど答えに近づきます
ヒント 2
ヒント 3
ヒント 4答えに近い
アルゴリズムのチートシートの「グラフ探索と幅優先探索」に、
関わる学び
グラフ探索は、
まず 考える手順
段 1
まずは、
段 2
ここでは、
段 3
ケーブルを 1 本見る
段 4
代表をfind は、find 1 回のfind は
解答
Python
Python
def find(parent, x):
top = x
while parent[top] != top:
top = parent[top]
while parent[x] != top:
up = parent[x]
parent[x] = top
x = up
return top
def count_networks(n, cables):
parent = list(range(n))
size = [1] * n
groups = n
for a, b in cables:
ra = find(parent, a)
rb = find(parent, b)
if ra == rb:
continue
if size[ra] < size[rb]:
ra, rb = rb, ra
parent[rb] = ra
size[ra] += size[rb]
groups -= 1
return groupsJavaScript
JavaScript
function find(parent, x) {
let top = x;
while (parent[top] !== top) {
top = parent[top];
}
while (parent[x] !== top) {
const up = parent[x];
parent[x] = top;
x = up;
}
return top;
}
function countNetworks(n, cables) {
const parent = [];
const size = [];
for (let i = 0; i < n; i++) {
parent.push(i);
size.push(1);
}
let groups = n;
for (const [a, b] of cables) {
let ra = find(parent, a);
let rb = find(parent, b);
if (ra === rb) {
continue;
}
if (size[ra] < size[rb]) {
[ra, rb] = [rb, ra];
}
parent[rb] = ra;
size[ra] += size[rb];
groups--;
}
return groups;
}Go
Go
func find(
parent []int, x int,
) int {
top := x
for parent[top] != top {
top = parent[top]
}
for parent[x] != top {
up := parent[x]
parent[x] = top
x = up
}
return top
}
func countNetworks(
n int, cables [][]int,
) int {
parent := make([]int, n)
size := make([]int, n)
for i := range parent {
parent[i] = i
size[i] = 1
}
groups := n
for _, c := range cables {
ra := find(parent, c[0])
rb := find(parent, c[1])
if ra == rb {
continue
}
if size[ra] < size[rb] {
ra, rb = rb, ra
}
parent[rb] = ra
size[ra] += size[rb]
groups--
}
return groups
}parent はsize はgroups は n からparent の
計算量
- 時間は O((n + m) α(n)) です。
m は cablesの長さで、 台の 初期化が n、 ケーブルごとに findを 2 回呼びます。α(n) は、 経路の 圧縮と 大きさで つなぐ 工夫を 両方使った ときの find1 回の手間で、 n が 現実の どんな 大きさでも 4 を 超えない、 ほぼ 一定の値です - 使う
記憶は O(n) です。 親の 並びと 大きさの 並びだけです - 段 1 の
深さ優先と 時間の 大きさは 同じで、 隣の 台の 一覧を 作る 分の 記憶 O(n + m) が 要ります。 素集合の 強みは、 ケーブルが 1 本ずつ 増えていく 場面でも、 そのつど数え直さずに、 足した 1 本ぶんだけで 数を 保てる ところです
よく ある間違い
- 代表を
探さずに、 台その ものをつなぐ。 parent[b] = aと台を 直接つなぐと、 b が すでに 別の ネットワークの 途中に いた とき、 その ネットワークの 残りが 切り離されます。 つなぐのは、 必ず findで見つけた 根どうしです - 同じ
ネットワークなのに、 数を減らす。 根が 同じかを 確かめずに 数を 1 減らすと、 例 2 のように 輪に なった ケーブルや 2 本目の ケーブルで、 減らしすぎます。 根が 違う ときだけ、 つないで 減らします - ケーブルに
出てくる 台だけを数える。 cablesを見て 数えると、 どの ケーブルにも 出て こない 台 (例 1 の 2 番) を 数え落とします。 n 台から 始めて、 つないだ分を引きます - 大きさを
根以外から読む。 sizeは根の 台の 物だけが 正しい 値です。 つないだあとに、 下へ 入れた 側の 大きさを、 上に なった 根へ足します
解答を動かした版 Python 3.14.7Node.js 24.22.1Go 1.24.2確認日 2026年11月8日
面接での 話し方の例
書き始める
「最初は
全部の 台が ばらばらなので、 ネットワークは n 個です。 ケーブルを 1 本ずつ 見て、 両端の 根が 違う ときだけ、 2 つの 根を つないで 数を 1 減らします。 根が 同じなら、 もう つながっているので 何もしません。 経路の 圧縮と 大きさで つなぐ 工夫を 入れて、 時間は ほぼ O(n + m) です。」
書き終えたら、
「例 2 では、
まず 5 つで 始めるので 数は 5 です。 2 と 0、 0 と 4 を つなぐと 3 に なります。 次の 4 と 2 は、 根が 同じなので 数えません。 1 と 3 を つないで 2 に なり、 最後の 3 と 1 も 同じなので、 答えは 2 です。」