E - 社内連絡網 / Internal Contact Network Editorial by admin
gemini-3.5-flash-thinking概要
この問題は、木構造(会社の組織図)において指定された \(K\) 人の社員(頂点)の集合に対し、以下の2つの値を効率よく求める問題です。
- 値 1:全員の最小共通祖先(LCA)から、各メンバーへの距離の総和。
- 値 2:メンバーからちょうど1人を除外したときの、新たなLCAから残ったメンバーへの距離の総和の最小値。
木上の距離とLCA(最小共通祖先)の性質を巧みに利用することで、各クエリに対して高速に解答を求めることができます。
考察
1. 距離の総和の式変形
木上の2頂点 \(u, v\) の距離は、根(社員1)からの深さ \(\mathrm{depth}\) と、2頂点の最小共通祖先 \(\mathrm{LCA}(u, v)\) を用いて以下のように表せます。
\[\mathrm{dist}(u, v) = \mathrm{depth}(u) + \mathrm{depth}(v) - 2 \cdot \mathrm{depth}(\mathrm{LCA}(u, v))\]
今、招集されたメンバーの集合を \(S\)、そのLCAを \(r = \mathrm{LCA}(S)\) とします。 \(r\) は \(S\) に含まれるすべての頂点の祖先であるため、任意の \(v \in S\) に対して \(\mathrm{LCA}(r, v) = r\) が成り立ちます。
したがって、値 1 である「\(r\) から各メンバーへの距離の総和」は以下のように変形できます。
\[\sum_{v \in S} \mathrm{dist}(r, v) = \sum_{v \in S} (\mathrm{depth}(v) - \mathrm{depth}(r)) = \left( \sum_{v \in S} \mathrm{depth}(v) \right) - |S| \cdot \mathrm{depth}(r)\]
メンバー全員の深さの総和を \(D = \sum_{v \in S} \mathrm{depth}(v)\)、メンバー数を \(K = |S|\) とおくと、値 1 は \(D - K \cdot \mathrm{depth}(r)\) と非常にシンプルな式で計算できます。
2. 1人除外したときの最小化(値 2)
同様に、あるメンバー \(t \in S\) を1人除外したときの距離の総和を考えます。 残った集合 \(S \setminus \{t\}\) のLCAを \(r'\) とすると、その距離の総和は以下のようになります。
\[\sum_{v \in S \setminus \{t\}} \mathrm{dist}(r', v) = \left( \sum_{v \in S \setminus \{t\}} \mathrm{depth}(v) \right) - (K - 1) \cdot \mathrm{depth}(r')\]
ここで、 \(\sum_{v \in S \setminus \{t\}} \mathrm{depth}(v) = D - \mathrm{depth}(t)\) であるため、上の式は次のように整理できます。
\[D - \left[ \mathrm{depth}(t) + (K - 1) \cdot \mathrm{depth}(r') \right]\]
\(D\) は除外する人によらず一定なので、この値(距離の総和)を最小化するためには、引き算している部分である
\[g(t) = \mathrm{depth}(t) + (K - 1) \cdot \mathrm{depth}(\mathrm{LCA}(S \setminus \{t\}))\]
を最大化すればよいことになります。
3. LCAの重要な性質とDFS順(行きがけ順)
素朴にすべての \(t \in S\) について \(\mathrm{LCA}(S \setminus \{t\})\) を求め直すと、1クエリあたり \(O(K^2 \log N)\) などの時間がかかり、実行時間制限に間に合いません(TLE)。そこで、LCAに関する強力な性質を利用します。
【LCAの性質】 木をDFS(深さ優先探索)したときの行きがけ順(
tinと呼びます)で頂点集合 \(S\) をソートし、 \(V_1, V_2, \ldots, V_K\) と並べたとき、集合全体のLCAは、最もtinが小さい頂点 \(V_1\) と、最もtinが大きい頂点 \(V_K\) のLCAに一致する。\[\mathrm{LCA}(S) = \mathrm{LCA}(V_1, V_K)\]
この性質を用いると、除外する頂点 \(t\) に応じて、残された集合のLCA \(r' = \mathrm{LCA}(S \setminus \{t\})\) は以下の3つのケースに分類できます。
- \(t = V_1\) (
tinが最小の頂点)を除外する場合 残った集合の中でtinが最小になるのは \(V_2\)、最大なのは \(V_K\) です。 したがって、新たなLCAは \(r' = \mathrm{LCA}(V_2, V_K)\) となります。 - \(t = V_K\) (
tinが最大の頂点)を除外する場合 残った集合の中でtinが最小になるのは \(V_1\)、最大なのは \(V_{K-1}\) です。 したがって、新たなLCAは \(r' = \mathrm{LCA}(V_1, V_{K-1})\) となります。 - それ以外の \(t \in \{V_2, \ldots, V_{K-1}\}\) を除外する場合
残った集合の
tinの最小値は \(V_1\)、最大値は \(V_K\) のまま変わりません。 したがって、新たなLCAは元のLCAと変わらず \(r' = r = \mathrm{LCA}(V_1, V_K)\) となります。
4. 高速化のまとめ
ケース3において、LCAは常に元の \(r\) のままで一定です。 したがって、ケース3の中で \(g(t) = \mathrm{depth}(t) + (K - 1) \cdot \mathrm{depth}(r)\) を最大化するためには、\(V_2, \ldots, V_{K-1}\) の中で最も深さ \(\mathrm{depth}(t)\) が大きい頂点を選べばよいことになります。
これにより、調べるべき除外候補 \(t\) は以下の最大3通りに絞られます!
- 候補①: \(t = V_1\)
- 候補②: \(t = V_K\)
- 候補③: \(V_2, \ldots, V_{K-1}\) のうち、最も \(\mathrm{depth}\) が大きい頂点(\(K > 2\) のときのみ)
これら3つの候補に対して \(g(t)\) を計算し、その最大値を \(D\) から引くことで、値 2 を \(O(\log N + K)\) で求めることができます。
アルゴリズム
前処理(木全体の情報取得)
- 与えられた親の関係から隣接リストを構築します。
- DFS(またはBFS)を行い、各頂点 \(i\) の根からの深さ
depth[i]と、DFSでの行きがけ順tin[i]を求めます。 - ダブリング(Doubling)を用いて、2頂点のLCAを \(O(\log N)\) で求めるためのテーブル
parent[i][d](頂点 \(i\) の \(2^d\) 個上の祖先)を構築します。
クエリ処理 各クエリで与えられた頂点集合 \(V\) に対して以下を行います。
- \(K = 1\) の場合は、値 1 =
0、値 2 =-1を出力して次のクエリへ。 - 集合 \(V\) を
tinの値が小さい順にソートします。ソート後の配列を \(V = [V_0, V_1, \ldots, V_{K-1}]\) とします(0-indexed)。 - 全体のLCA \(r = \mathrm{LCA}(V_0, V_{K-1})\) を求めます。
- 全体の深さの総和 \(D = \sum_{v \in V} \mathrm{depth}(v)\) を求めます。
- 値 1 を \(D - K \cdot \mathrm{depth}(r)\) として計算します。
- 値 2 を求めるため、以下の3つの候補から \(g(t)\) の最大値を求めます。
- \(t = V_0\) のとき: \(g_1 = \mathrm{depth}(V_0) + (K-1) \cdot \mathrm{depth}(\mathrm{LCA}(V_1, V_{K-1}))\)
- \(t = V_{K-1}\) のとき: \(g_2 = \mathrm{depth}(V_{K-1}) + (K-1) \cdot \mathrm{depth}(\mathrm{LCA}(V_0, V_{K-2}))\)
- \(K > 2\) のとき、中間メンバー \(V_1, \ldots, V_{K-2}\) の中での最大深さを \(M\) とし、 \(g_3 = M + (K-1) \cdot \mathrm{depth}(r)\)
- \(g_1, g_2, g_3\) の最大値を \(g_{\max}\) としたとき、値 2 は \(D - g_{\max}\) となります。
- \(K = 1\) の場合は、値 1 =
計算量
時間計算量
- 前処理: \(O(N \log N)\)
- DFSによる
depth,tinの計算に \(O(N)\)。 - ダブリングテーブルの構築に \(O(N \log N)\)(深さの最大が \(18\) 程度なので、定数倍も非常に小さいです)。
- DFSによる
- クエリ処理: \(O(\sum K \log K + Q \log N)\)
- 各クエリにおいて、 \(K\) 個の要素のソートに \(O(K \log K)\)。
- LCAの計算(高々3回)に \(O(\log N)\)。
- 中間メンバーの最大深さの探索に \(O(K)\)。
- 全クエリの \(K\) の総和は \(\sum K \le 2 \times 10^5\) であるため、制限時間内に余裕で間に合います。
空間計算量
- \(O(N \log N)\)
- ダブリング用のテーブル
parent(サイズ \(N \times 18\))が支配的です。
- ダブリング用のテーブル
実装のポイント
再帰の回避(Python高速化) Pythonにおいて再帰関数によるDFSを行うと、コールスタックのオーバーヘッドや再帰回数制限(
sys.setrecursionlimit)の問題が発生します。 本コードでは、明示的なスタック(while stack:)を用いた非再帰のDFSを実装することで、高速かつ安全に行きがけ順tinを求めています。高速な入出力 クエリ数が多く入力の行数も多いため、
sys.stdin.read().split()を用いて入力を一括で取得し、処理を高速化しています。ソースコード
import sys
def solve():
# 入力を一括で読み込む
input_data = sys.stdin.read().split()
if not input_data:
return
N = int(input_data[0])
Q = int(input_data[1])
P = [int(x) for x in input_data[2:N+1]]
# 隣接リストの構築
adj = [[] for _ in range(N + 1)]
for i in range(2, N + 1):
adj[P[i - 2]].append(i)
depth = [0] * (N + 1)
parent = [[0] * 18 for _ in range(N + 1)]
parent[1][0] = 1
for i in range(2, N + 1):
p = P[i - 2]
depth[i] = depth[p] + 1
parent[i][0] = p
# ダブリングテーブルの構築
for d in range(17):
for i in range(1, N + 1):
parent[i][d+1] = parent[parent[i][d]][d]
# DFS順 tin の計算
tin = [0] * (N + 1)
timer = 0
stack = [1]
while stack:
u = stack.pop()
timer += 1
tin[u] = timer
for v in reversed(adj[u]):
stack.append(v)
R18 = list(range(18))
REV18 = list(reversed(range(18)))
# LCAを求める関数
def get_lca(u, v):
if depth[u] < depth[v]:
u, v = v, u
diff = depth[u] - depth[v]
pu = parent[u]
for d in R18:
if (diff >> d) & 1:
u = pu[d]
pu = parent[u]
if u == v:
return u
pu = parent[u]
pv = parent[v]
for d in REV18:
if pu[d] != pv[d]:
u = pu[d]
v = pv[d]
pu = parent[u]
pv = parent[v]
return pu[0]
idx = N + 1
out = []
for _ in range(Q):
K = int(input_data[idx])
V = [int(x) for x in input_data[idx+1 : idx+1+K]]
idx += 1 + K
if K == 1:
out.append("0 -1")
continue
# tin の昇順にソート
V.sort(key=lambda x: tin[x])
v1 = V[0]
vK = V[-1]
r = get_lca(v1, vK)
D = sum(depth[x] for x in V)
ans1 = D - K * depth[r]
# 候補1: v1 を除外
v2 = V[1]
lca_ex1 = get_lca(v2, vK)
g1 = depth[v1] + (K - 1) * depth[lca_ex1]
# 候補2: vK を除外
vK_minus_1 = V[-2]
lca_exK = get_lca(v1, vK_minus_1)
g2 = depth[vK] + (K - 1) * depth[lca_exK]
max_g = g1 if g1 > g2 else g2
# 候補3: それ以外の頂点を除外
if K > 2:
max_dep_mid = max(depth[x] for x in V[1:-1])
g3 = max_dep_mid + (K - 1) * depth[r]
if g3 > max_g:
max_g = g3
ans2 = D - max_g
out.append(f"{ans1} {ans2}")
print('\n'.join(out))
if __name__ == '__main__':
solve()
この解説は gemini-3.5-flash-thinking によって生成されました。