E - 妨害と通信ネットワーク / Jamming and Communication Network Editorial by admin
Gemini 3.0 Flash (Thinking)概要
この問題は、グラフから辺を1本取り除いた後の最小全域木(MST)の重みの最大値を求める問題です。
青木君は高橋君が作るMSTの総コストを最大化しようとしますが、MSTに含まれない辺を削除してもMSTの重みは変わりません。そのため、青木君は「もともとのMSTに含まれる辺」のうち、それを削除した際に代わりとなる辺(代替辺)のコストが最も大きくなるような辺を1本選んで切断することになります。
考察
1. どの辺を切断すべきか?
まず、グラフ全体の最小全域木 \(T\) を求めます。 - 青木君が \(T\) に含まれない辺を切断した場合:高橋君は依然として \(T\) を構築できるため、MSTの重みは変わりません。 - 青木君が \(T\) に含まれる辺 \(e\) を切断した場合:高橋君は \(e\) の代わりに、\(T \setminus \{e\}\) を再び連結にするような辺 \(e'\) のうち、最もコストが小さいものを選んで新しいMSTを構築します。
したがって、青木君の戦略は 「MSTに含まれる各辺 \(e\) について、それを切断した後の最小コスト増加量を計算し、その増加量が最大になる \(e\) を選ぶ」 ことになります。
2. 代替辺の条件
MST \(T\) に含まれる辺 \(e\) を切断すると、木 \(T\) は2つのコンポーネントに分かれます。これらを再び繋ぐことができる辺 \(e' = (u, v)\) は、「もともとの木 \(T\) において、頂点 \(u, v\) を結ぶパス上に辺 \(e\) が含まれている」 ような辺です。
高橋君は、このような \(e'\) の中で重み \(W_{e'}\) が最小のものを選びます。
3. 効率的な計算方法
愚直にすべての辺 \(e \in T\) に対して代替辺を探すと、最悪 \(O(NM)\) かかり間に合いません。そこで、以下の性質を利用します。
- すべての非MST辺 \((u, v, w)\) について考える。この辺は、木 \(T\) 上の \(u\) から \(v\) へのパスに含まれるすべての辺 \(e\) に対して、重み \(w\) の「代替辺候補」となる。
- 各辺 \(e \in T\) について、自分をパスに含む非MST辺の中で最小の重み \(w\) を知りたい。
これは、「非MST辺を重みの昇順にソートし、まだ代替辺が決まっていない木上のパスを塗りつぶしていく」 という操作で解くことができます。
アルゴリズム
- 最小全域木 (MST) の構築: クラスカル法を用いてMSTを求め、その総重みを \(S\) とします。MSTに含まれる辺の集合を \(E_{MST}\)、含まれない辺の集合を \(E_{other}\) とします。
- 木構造の整理: MSTを根付き木として扱い、各頂点の深さと親への辺を記録します。また、ダブリング(Binary Lifting)を用いて、最小共通祖先 (LCA) を高速に求められるようにします。
- 代替辺の決定 (パス被覆):
\(E_{other}\) の辺を重み \(W\) の昇順にソートします。各辺 \((u, v, w) \in E_{other}\) について、\(u\) から \(v\) へのパス上にある辺のうち、まだ代替辺が割り当てられていない辺すべてに重み \(w\) を割り当てます。
- この「未割り当ての辺を飛ばす」操作を高速化するため、Union-Find (DSU) を活用します。各頂点について「その頂点より上方向で、まだ未処理の辺がある最も近い頂点」を DSU で管理することで、各辺を一度だけ走査する \(O(M \alpha(N))\) の計算量で処理できます。
- 答えの計算: 各辺 \(e \in E_{MST}\) について、その代替辺の重みを \(W_{alt}(e)\) とすると、その辺を切断した後のMSTの重みは \(S - W_e + W_{alt}(e)\) です。 これらと元の \(S\) の中での最大値が答えとなります。
計算量
- MSTの構築: \(O(M \log M)\) または \(O(M \log N)\)
- LCAの構築: \(O(N \log N)\)
- 代替辺の計算 (ソート + DSUパス被覆): \(O(M \log M + M \alpha(N))\)
- 時間計算量: \(O(M \log M)\)
- 空間計算量: \(O(N \log N + M)\)
実装のポイント
再帰制限: Pythonの場合、深い木の走査で再帰回数制限に達しやすいため、DFSや木の構築はスタックを用いた反復形式(Iterative)で実装するのが安全です。
DSUによるパス短縮: パスを遡る際、一度処理した頂点は
dsu.union(current, parent)のようにして飛ばすことで、計算量を劇的に落とすことができます。2-辺連結性の保証: 問題文でグラフが 2-辺連結であることが保証されているため、すべての \(e \in E_{MST}\) に対して必ず代替辺が存在します。
ソースコード
import sys
# Increase recursion depth for safety, though iterative methods are used
sys.setrecursionlimit(300000)
def solve():
# Efficiently read input data
input_data = sys.stdin.read().split()
if not input_data:
return
it = iter(input_data)
N = int(next(it))
M = int(next(it))
edges = []
for i in range(M):
u = int(next(it))
v = int(next(it))
w = int(next(it))
edges.append((u, v, w, i))
# Kruskal's algorithm to find the Minimum Spanning Tree (MST)
# Sort edges by weight to build MST
edges.sort(key=lambda x: x[2])
parent_mst = list(range(N + 1))
# Iterative DSU find function
def find(p, i):
root = i
while p[root] != root:
root = p[root]
while p[i] != root:
nxt = p[i]
p[i] = root
i = nxt
return root
mst_weight = 0
mst_edge_indices = []
non_mst_edges = []
adj = [[] for _ in range(N + 1)]
edge_weights = [0] * M
for u, v, w, i in edges:
edge_weights[i] = w
root_u = find(parent_mst, u)
root_v = find(parent_mst, v)
if root_u != root_v:
parent_mst[root_u] = root_v
mst_weight += w
mst_edge_indices.append(i)
adj[u].append((v, i))
adj[v].append((u, i))
else:
# Non-MST edges are candidates for replacing removed MST edges
non_mst_edges.append((u, v, w, i))
# Root the MST at node 1 and precompute depths and parents using iterative DFS
depth = [0] * (N + 1)
parent_node = [0] * (N + 1)
edge_to_parent_idx = [-1] * (N + 1)
stack = [1]
visited = [False] * (N + 1)
visited[1] = True
while stack:
u = stack.pop()
for v, i in adj[u]:
if not visited[v]:
visited[v] = True
parent_node[v] = u
edge_to_parent_idx[v] = i
depth[v] = depth[u] + 1
stack.append(v)
# Precompute binary lifting table for Lowest Common Ancestor (LCA)
LOGN = (N).bit_length()
up = [[0] * (N + 1) for _ in range(LOGN)]
up[0] = parent_node
for i in range(1, LOGN):
up_prev = up[i-1]
up_curr = up[i]
for u in range(1, N + 1):
up_curr[u] = up_prev[up_prev[u]]
def get_lca(u, v):
if depth[u] < depth[v]:
u, v = v, u
diff = depth[u] - depth[v]
for i in range(LOGN):
if (diff >> i) & 1:
u = up[i][u]
if u == v:
return u
for i in range(LOGN - 1, -1, -1):
up_i = up[i]
if up_i[u] != up_i[v]:
u = up_i[u]
v = up_i[v]
return up[0][u]
# DSU for path covering to efficiently find the minimum replacement weight for each MST edge
# dsu_path[i] points to the highest ancestor of node i whose parent-edge hasn't been processed
dsu_path = list(range(N + 1))
replacement_weight = [-1] * M
# Process non-MST edges (already sorted by weight) to cover edges on the tree paths
for u, v, w, i in non_mst_edges:
lca = get_lca(u, v)
# Traverse path from u to lca
curr = find(dsu_path, u)
while depth[curr] > depth[lca]:
idx = edge_to_parent_idx[curr]
if replacement_weight[idx] == -1:
replacement_weight[idx] = w
# Use DSU to jump over this edge in future traversals
dsu_path[curr] = find(dsu_path, parent_node[curr])
curr = dsu_path[curr]
# Traverse path from v to lca
curr = find(dsu_path, v)
while depth[curr] > depth[lca]:
idx = edge_to_parent_idx[curr]
if replacement_weight[idx] == -1:
replacement_weight[idx] = w
dsu_path[curr] = find(dsu_path, parent_node[curr])
curr = dsu_path[curr]
# Aoki wants to maximize the weight of the resulting MST after removing one edge
# If he removes an edge not in the MST, the MST weight remains mst_weight
# If he removes an MST edge e, the weight becomes mst_weight - W(e) + W(replacement_e)
ans = mst_weight
for i in mst_edge_indices:
if replacement_weight[i] != -1:
ans = max(ans, mst_weight - edge_weights[i] + replacement_weight[i])
# Output the maximized MST weight
sys.stdout.write(str(ans) + '\n')
if __name__ == '__main__':
solve()
この解説は gemini-3-flash-thinking によって生成されました。
posted:
last update: