問題
$N$ 頂点 $M$ 辺の連結な無向重み付きグラフと、$N-1$ 本の辺からなる「候補となる全域木」が与えられる。この候補の全域木が 最小全域木(MST) かどうかを判定せよ。
サイクル性質(Cycle Property): 全域木に含まれない各辺 $(u,v,w)$ について、木上の $u$–$v$ パス上の最大辺重みが $w$ 以下であれば、その辺で木を改善することはできない。すべての非木辺でこれが成り立つとき、かつそのときに限り、候補はMSTである。パス最大値は LCAダブリングに「経路上の最大辺重み」を同時に持たせることで $O(\log N)$ で求められる(理論上は Komlós のアルゴリズムで $O(N+M)$ も可能)。
入力形式
N M
u_1 v_1 w_1
:
u_M v_M w_M
i_1 i_2 ... i_{N-1}
最後の行は、全域木として選ばれている辺の番号(1-indexed)を $N-1$ 個。
制約
$2 \le N \le 2\times10^5$
$N-1 \le M \le 2\times10^5$
$1 \le w_i \le 10^9$
グラフは連結、与えられる木は正当
入出力例
入力例1
4 5
1 2 1
2 3 2
3 4 1
1 3 3
2 4 4
1 2 3
出力例1
Yes
入力例2
4 5
1 2 1
2 3 2
3 4 1
1 3 3
2 4 4
1 4 5
出力例2
No
概念図
ヒント(段階的開示)
ヒント1: 方向性
候補木がMSTかどうかを判定するのに、必ずしも真のMSTを別途構築して重みを比較する必要はない。木に含まれない辺それぞれについて「その辺を使うと木が改善できるか」だけを個別にチェックする方法を考えよ。
ヒント2: アプローチ
非木辺 $(u,v,w)$ を木に追加すると閉路ができる。閉路中の最大辺を除去すれば重みは非増加になるはずなので、「$w$ が木上の $u$–$v$ パスの最大辺重み以上」であることがすべての非木辺で成り立てば、木は改善不可能=MSTである。パス最大値クエリはLCAダブリングのテーブルに「そこまでの経路の最大辺重み」も一緒に持たせれば同時に求められる。
ヒント3: 誘導(コード骨格)
LOG = 18
up = [[-1] * n for _ in range(LOG)]
maxedge = [[0] * n for _ in range(LOG)]
for k in range(1, LOG):
for v in range(n):
p = up[k - 1][v]
if p == -1:
continue
up[k][v] = up[k - 1][p]
maxedge[k][v] = max(maxedge[k - 1][v], maxedge[k - 1][p])
def path_max(u, v):
# depth[u]>=depth[v]にswapし、深さを揃えながらmaxedgeを取り
# LCAまで登る
...
各非木辺 $(u,v,w)$ について path_max(u,v) <= w を確認し、1つでも破れば No。
模範解答 (Python)
import sys
from collections import deque
def solve():
data = sys.stdin.buffer.read().split()
idx = 0
n = int(data[idx]); idx += 1
m = int(data[idx]); idx += 1
edges = []
for _ in range(m):
u = int(data[idx]) - 1; idx += 1
v = int(data[idx]) - 1; idx += 1
w = int(data[idx]); idx += 1
edges.append((u, v, w))
tree_idx = [int(data[idx + i]) - 1 for i in range(n - 1)]
idx += n - 1
tree_edge_set = set(tree_idx)
adj = [[] for _ in range(n)]
for ti in tree_idx:
u, v, w = edges[ti]
adj[u].append((v, w))
adj[v].append((u, w))
LOG = 1
while (1 << LOG) < n:
LOG += 1
LOG += 1
depth = [-1] * n
up = [[-1] * n for _ in range(LOG)]
maxedge = [[0] * n for _ in range(LOG)]
depth[0] = 0
dq = deque([0])
while dq:
u = dq.popleft()
for v, w in adj[u]:
if depth[v] == -1:
depth[v] = depth[u] + 1
up[0][v] = u
maxedge[0][v] = w
dq.append(v)
for k in range(1, LOG):
upk, upk1 = up[k], up[k - 1]
mek, mek1 = maxedge[k], maxedge[k - 1]
for v in range(n):
p = upk1[v]
if p == -1:
continue
upk[v] = upk1[p]
mek[v] = max(mek1[v], mek1[p])
def path_max(u, v):
best = 0
if depth[u] < depth[v]:
u, v = v, u
diff = depth[u] - depth[v]
k = 0
while diff:
if diff & 1:
best = max(best, maxedge[k][u])
u = up[k][u]
diff >>= 1
k += 1
if u == v:
return best
for k in range(LOG - 1, -1, -1):
if up[k][u] != up[k][v]:
best = max(best, maxedge[k][u], maxedge[k][v])
u = up[k][u]
v = up[k][v]
best = max(best, maxedge[0][u], maxedge[0][v])
return best
ok = True
for i, (u, v, w) in enumerate(edges):
if i in tree_edge_set:
continue
if path_max(u, v) > w:
ok = False
break
print("Yes" if ok else "No")
solve()
計算量: 前処理 $O(N\log N)$、各クエリ $O(\log N)$、全体 $O((N+M)\log N)$。
Step-by-Step 解説
1候補木の隣接リスト構築
入力で指定された$N-1$本の辺だけを使って木を作る。
入力で指定された$N-1$本の辺だけを使って木を作る。
2BFSで深さと直近の親を求める
頂点0を根としてBFSし、
頂点0を根としてBFSし、
depth,up[0],maxedge[0]を求める。3ダブリングテーブルの構築
up[k][v]と同時にmaxedge[k][v]($2^k$個上まで登る経路上の最大辺重み)を求める。4path_maxクエリ
深さを揃えながら登り、LCAの1つ手前まで同時に登って最大値を反映する。$O(\log N)$。
深さを揃えながら登り、LCAの1つ手前まで同時に登って最大値を反映する。$O(\log N)$。
5非木辺のサイクル性質チェック
すべての非木辺で
すべての非木辺で
path_max(u,v) <= wを確認。1本でも破ればNo。よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 総重み比較だけでMST判定 | サイクル性質による個別チェックの本質を学べない | 各非木辺についてパス最大値をダブリングで確認する |
深さを揃える過程でmaxedgeの反映を忘れる | 通過した辺の重みを見落とす | 揃えるループ内でもbest=max(best,maxedge[k][u])を行う |
u==vになった直後の処理を省略 | 深さを揃えた時点でLCAが片方の祖先だった場合を見逃す | if u==v: return bestの早期リターンを入れる |
| LOGの大きさが不足 | $2^{LOG}<N$だと祖先探索が不完全になる | $LOG$は$\lceil\log_2 N\rceil+1$程度余裕を持たせる |
次のステップ
- 発展: Kruskal Reconstruction Treeを使うとパス最大値クエリが$O(1)$(前処理$O((N+M)\alpha(N))$)に高速化できる
- 次回予告: Batcherのビトニックソートネットワーク(Sorting Network・比較器構成)