D - ネットワークの構築 / Network Construction 解説 by admin
Gemini 3.1 Pro (Thinking)概要
特定の必須ケーブル(辺)を必ず含めるという条件のもとで、通信コストが最小となるネットワーク(最小全域木)を構築する問題です。
考察
全サーバーを連結にしつつ閉路を持たないネットワークを構築する問題は、グラフ理論において「最小全域木(Minimum Spanning Tree, MST)」を求める問題として知られています。最小全域木を求める代表的な手法にクラスカル法(Kruskal’s algorithm)があります。
通常のクラスカル法では、すべての辺をコストの小さい順(昇順)にソートし、「閉路ができないように辺を \(1\) 本ずつ追加していく」という貪欲法を行います。
しかし今回は「必ず含めなければならない \(K\) 本のケーブル」が指定されています。これをどう扱うかがポイントです。 直感的に考えると、「必須の \(K\) 本のケーブルを最初にすべて繋いでしまい、足りない部分をコストの低いケーブルで補う」というアプローチが思い浮かびます。実際、この方法で正しく最小コストを求めることができます。
注意点として、以下のケースでは全域木を作ることができないため -1 を出力する必要があります。
1. 必須の \(K\) 本のケーブルだけで閉路ができてしまう場合
全域木は閉路を持たないという条件に反するため、構築不可能です。
2. 利用可能なケーブルをすべて使っても、全サーバーを繋ぐことができない場合
最終的に選んだケーブルの本数が \(N-1\) 本に満たない場合、サーバー全体が連結になっていません。
アルゴリズム
閉路の判定や頂点の連結判定には、Union-Find(素集合データ構造)を使用します。
- \(N\) 頂点の Union-Find を初期化します。
- まず、必須の \(K\) 本のケーブルを順番に確認し、Union-Find を使ってサーバー同士を繋ぎます(
unite操作)。- このとき、繋ごうとした2つのサーバーがすでに同じグループに属している(=閉路ができる)場合は、その時点で構築不可能と判断し
-1を出力して終了します。 - 閉路ができなければ、そのケーブルのコストを合計に足し、使ったケーブルの本数を \(+1\) します。
- このとき、繋ごうとした2つのサーバーがすでに同じグループに属している(=閉路ができる)場合は、その時点で構築不可能と判断し
- 次に、必須ではない残りのケーブルを、敷設コスト \(W_i\) の小さい順(昇順)にソートします。
- ソートしたケーブルを順番に見ていき、Union-Find で閉路ができないか確認しながら追加していきます。
- 閉路ができない(繋ぐサーバーが異なるグループに属している)場合のみ、ケーブルを採用してコストを合計に足し、使った本数を \(+1\) します。
- 使ったケーブルの本数が \(N-1\) 本に達したら、全域木が完成したことになるので探索を打ち切ります。
- 最後に、使ったケーブルの本数がちょうど \(N-1\) 本であれば合計コストを出力し、足りなければ(全体が連結にならなかったので)
-1を出力します。
計算量
- 時間計算量: \(O(M \log M)\)
- 必須ではない辺のソートに \(O(M \log M)\) の時間がかかります。
- Union-Find の各操作は非常に高速(アッカーマン関数の逆関数 \(\alpha(N)\))であり、辺の追加処理全体で \(O(M \alpha(N))\) となります。
- したがって、全体の時間計算量はソート部分が支配的となり \(O(M \log M)\) です。
- 空間計算量: \(O(N + M)\)
- Union-Find の配列に \(O(N)\)、辺の情報を保持するリストや集合(Set)に \(O(M)\) のメモリを使用します。
実装のポイント
1-indexed と 0-indexed の変換: 入力されるサーバーの番号やケーブルの番号は \(1\) から始まりますが、プログラム内部の配列や Union-Find では \(0\) から始まるインデックスとして扱うと実装がシンプルになります。
必須ケーブルの高速な判定: あるケーブルが必須かどうかを判定するために、必須ケーブルの番号を
set(ハッシュテーブル)に入れておくと、\(O(1)\) で高速に判定できます。再帰上限の引き上げ(Python特有): Union-Find の
find関数で再帰呼び出しを使用するため、Python ではsys.setrecursionlimitを用いて再帰の深さの上限を増やしておく必要があります(制約の \(N = 2 \times 10^5\) に対応するため)。高速な入出力: 入力量が多いため、
sys.stdin.read().split()を用いて入力を一括で読み込むと実行時間を大幅に短縮できます。ソースコード
import sys
sys.setrecursionlimit(300000)
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.size = [1] * n
def find(self, x):
if self.parent[x] == x:
return x
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def unite(self, x, y):
rx = self.find(x)
ry = self.find(y)
if rx == ry:
return False
if self.size[rx] < self.size[ry]:
rx, ry = ry, rx
self.parent[ry] = rx
self.size[rx] += self.size[ry]
return True
def solve():
input_data = sys.stdin.read().split()
if not input_data:
return
N = int(input_data[0])
M = int(input_data[1])
K = int(input_data[2])
U = [0] * M
V = [0] * M
W = [0] * M
idx = 3
for i in range(M):
U[i] = int(input_data[idx]) - 1
V[i] = int(input_data[idx+1]) - 1
W[i] = int(input_data[idx+2])
idx += 3
E = set()
if K > 0:
for _ in range(K):
E.add(int(input_data[idx]) - 1)
idx += 1
uf = UnionFind(N)
total_cost = 0
edge_count = 0
for i in E:
u, v, w = U[i], V[i], W[i]
if not uf.unite(u, v):
print(-1)
return
total_cost += w
edge_count += 1
other_edges = []
for i in range(M):
if i not in E:
other_edges.append((W[i], U[i], V[i]))
other_edges.sort()
for w, u, v in other_edges:
if edge_count == N - 1:
break
if uf.unite(u, v):
total_cost += w
edge_count += 1
if edge_count == N - 1:
print(total_cost)
else:
print(-1)
if __name__ == '__main__':
solve()
この解説は gemini-3.1-pro-thinking によって生成されました。
投稿日時:
最終更新: