D - 山岳縦走路の最長下り列 / Longest Descent Sequence on a Mountain Traverse Route 解説 by admin
Claude 4.6 Opus (Thinking)概要
木上の2点間パスを求め、そのパスに沿った標高列の最長狭義単調減少部分列(Longest Strictly Decreasing Subsequence, LDS)の長さを各クエリに対して求める問題です。
考察
問題の分解
この問題は大きく2つのステップに分かれます:
- 木上の2点間パスの特定: 木構造なので、任意の2点 \(u, v\) 間のパスは一意に決まります。このパスは \(u\) から LCA(最小共通祖先)を経由して \(v\) に至る経路です。
- 標高列の最長狭義単調減少部分列の計算: パス上の地点の標高を順に並べた列に対して、LDS を求めます。
LDS を LIS に帰着する
最長狭義単調減少部分列(LDS)は、列の各要素を 符号反転(\(-1\) 倍) することで、最長狭義単調増加部分列(LIS)の問題に帰着できます。
- 元の列: \(h_1 > h_2 > \cdots > h_m\)(狭義減少)
- 符号反転: \(-h_1 < -h_2 < \cdots < -h_m\)(狭義増加)
LIS は有名な \(O(k \log k)\) のアルゴリズム(bisect_left を用いる方法)で効率的に求められます。
計算量の見積もり
- \(N, Q \leq 5000\) と比較的小さいです。
- パスの長さは最大 \(O(N)\) です。
- 各クエリごとにパスを構築し LIS を求めても、1クエリあたり \(O(N \log N)\)、全体で \(O(QN \log N)\) 程度で十分間に合います。
アルゴリズム
1. LCA(最小共通祖先)の前処理
ダブリング(Binary Lifting)を用いて LCA を \(O(\log N)\) で求められるように前処理します。
up[k][v]: 頂点 \(v\) から \(2^k\) 回親をたどった先の頂点depth[v]: 根(地点 \(1\))からの深さ
LCA の求め方: 1. \(u, v\) の深さを揃える(深い方を引き上げる) 2. 同時に引き上げて一致する点を見つける
2. パスの構築
\(u\) から LCA \(l\) までの経路と、\(v\) から LCA \(l\) までの経路を求め、結合します。
- \(u \to l\): \(u\) から親を辿って \(l\) まで順に並べる → \([u, \ldots, l]\)
- \(l \to v\): \(v\) から親を辿って \(l\) まで並べたものを反転 → \([l\) の次の子, \(\ldots, v]\)
結合すると \([u, \ldots, l, \ldots, v]\) というパスが得られます。
3. LDS の計算
パス上の標高列 \(h_1, h_2, \ldots, h_k\) を得たら:
- 各要素を \(-1\) 倍して \(-h_1, -h_2, \ldots, -h_k\) とする
- この列に対して狭義単調増加の LIS を求める
bisect_leftを使い、tails配列を管理する標準的な \(O(k \log k)\) の LIS アルゴリズムを適用
具体例: パス上の標高が \([10, 5, 8, 3, 7, 2]\) の場合 - 狭義減少部分列の例: \([10, 8, 3, 2]\)(長さ4)、\([10, 5, 3, 2]\)(長さ4) - 最長は長さ 4
計算量
- 時間計算量: \(O(N \log N + Q \cdot N \log N)\)
- LCA の前処理: \(O(N \log N)\)
- 各クエリ: パス構築 \(O(N)\) + LIS 計算 \(O(N \log N)\)
- 全体: \(O(QN \log N)\)(\(Q, N \leq 5000\) なので約 \(3 \times 10^8\) 未満で収まる)
- 空間計算量: \(O(N \log N)\)(ダブリングテーブル)
実装のポイント
bisect_leftで狭義増加 LIS を求める:bisect_leftは「その値以上の最初の位置」を返すため、同じ値は上書きされ、狭義単調増加が正しく処理されます。もし広義(\(\leq\))にしたい場合はbisect_rightを使いますが、本問では狭義なのでbisect_leftが適切です。パスの結合時の重複に注意: \(u \to \text{LCA}\) のパスには LCA 自身を含め、\(v \to \text{LCA}\) のパスには LCA を含めずに反転して結合することで、LCA が二重に含まれることを防ぎます。
\(u = v\) のケース: パスは1点のみとなり、LDS の長さは \(1\) です。特別な処理は不要で、一般のアルゴリズムで自然に処理されます。
ソースコード
import sys
from bisect import bisect_left
def main():
input_data = sys.stdin.buffer.read().split()
idx = 0
N = int(input_data[idx]); idx += 1
Q = int(input_data[idx]); idx += 1
H = [0] * (N + 1)
for i in range(1, N + 1):
H[i] = int(input_data[idx]); idx += 1
parent = [0] * (N + 1)
children = [[] for _ in range(N + 1)]
if N >= 2:
for i in range(2, N + 1):
p = int(input_data[idx]); idx += 1
parent[i] = p
children[p].append(i)
# Precompute LCA using binary lifting
LOG = 13
up = [[0] * (N + 1) for _ in range(LOG)]
depth = [0] * (N + 1)
# BFS to compute depth and up[0]
from collections import deque
queue = deque([1])
visited = [False] * (N + 1)
visited[1] = True
order = []
while queue:
v = queue.popleft()
order.append(v)
for c in children[v]:
if not visited[c]:
visited[c] = True
depth[c] = depth[v] + 1
up[0][c] = v
queue.append(c)
for k in range(1, LOG):
for v in range(1, N + 1):
up[k][v] = up[k-1][up[k-1][v]]
def lca(u, v):
if depth[u] < depth[v]:
u, v = v, u
diff = depth[u] - depth[v]
for k in range(LOG):
if (diff >> k) & 1:
u = up[k][u]
if u == v:
return u
for k in range(LOG - 1, -1, -1):
if up[k][u] != up[k][v]:
u = up[k][u]
v = up[k][v]
return up[0][u]
def get_path(u, v):
l = lca(u, v)
# Path from u to l
path_u = []
node = u
while node != l:
path_u.append(node)
node = up[0][node]
path_u.append(l)
# Path from l to v (reversed)
path_v = []
node = v
while node != l:
path_v.append(node)
node = up[0][node]
# path is path_u + reversed(path_v)
path_v.reverse()
return path_u + path_v
def lds_length(seq):
# Longest strictly decreasing subsequence
# = LIS of negated sequence (strictly increasing)
# For strictly increasing LIS, use bisect_left on tails
if not seq:
return 0
# We want longest strictly decreasing subsequence
# Transform: negate values, find longest strictly increasing subsequence
neg_seq = [-x for x in seq]
# LIS strictly increasing
tails = []
for x in neg_seq:
pos = bisect_left(tails, x)
if pos == len(tails):
tails.append(x)
else:
tails[pos] = x
return len(tails)
out = []
for _ in range(Q):
u = int(input_data[idx]); idx += 1
v = int(input_data[idx]); idx += 1
path = get_path(u, v)
heights = [H[node] for node in path]
out.append(str(lds_length(heights)))
sys.stdout.write('\n'.join(out) + '\n')
main()
この解説は claude4.6opus-thinking によって生成されました。
投稿日時:
最終更新: