D - フェンスの塗り替え / Repainting the Fence 解説 by admin
Claude 4.6 Opus (Thinking)概要
\(N\) 枚の板に対して \(M\) 回の区間塗り替え操作を順に行い、最終的な各板の色を求める問題です。後から塗った色が上書きされるため、各板の色は「最後に塗られた操作」で決まります。
考察
素朴なアプローチの問題点
最も単純な方法は、各操作ごとに板 \(L_i\) から \(R_i\) まで1枚ずつ色を塗ることです。しかし、1回の操作で最大 \(N = 5 \times 10^5\) 枚を塗る可能性があり、操作が \(M = 2 \times 10^5\) 回あるため、最悪 \(O(NM) = 10^{11}\) 程度の計算量になり、TLE(時間超過)になります。
重要な気づき:「最後に塗った色」だけが意味を持つ
各板の最終的な色は、その板を最後に塗った操作の色で決まります。つまり、同じ板を何度も塗っても、最後の操作以外は無駄です。
この性質を利用すると、操作を逆順(後ろから)に処理できます。逆順に見て、ある操作 \([L_i, R_i, C_i]\) を処理するとき、まだ色が確定していない(=まだどの操作にも割り当てられていない)板だけに色 \(C_i\) を割り当てれば良いのです。一度色が確定した板は、それ以降(逆順でいうそれ以前の操作)では無視できます。
問題:未塗装の板を高速に見つけるには?
逆順処理で区間 \([L_i, R_i]\) 内の未塗装の板を見つけたいですが、単純に1枚ずつ調べると結局 \(O(NM)\) になりかねません。ここで Union-Find(素集合データ構造)の経路圧縮テクニック を使います。
アルゴリズム
「次の未塗装位置」を管理する Union-Find
配列 parent[i] を用意し、「位置 \(i\) 以上で最も近い未塗装の板の番号」を管理します。
- 初期状態:
parent[i] = i(すべて未塗装なので、自分自身が未塗装) - 板 \(i\) を塗ったとき:
parent[i] = i + 1とする(位置 \(i\) はもう塗装済みなので、次の未塗装位置は \(i+1\) 以降) find(x): 経路圧縮付きでparentを辿り、位置 \(x\) 以上の最初の未塗装位置を返す
処理の流れ
- 操作を逆順に処理する(\(i = M, M-1, \ldots, 1\))
- 操作 \([L_i, R_i, C_i]\) に対して、
pos = find(L_i)で区間内の最初の未塗装位置を取得 pos <= R_iの間、板posに色 \(C_i\) を割り当て、parent[pos] = pos + 1として次へ進む- すべての操作を処理した後、色が割り当てられなかった板は初期値 \(0\) のまま
具体例
\(N = 5, M = 2\)、操作1: \([1, 4, 3]\)、操作2: \([2, 5, 7]\) の場合:
- 逆順で操作2を処理: 板2,3,4,5 に色7を割り当て →
boards = [0, 7, 7, 7, 7] - 逆順で操作1を処理: \([1,4]\) の未塗装板を探す → 板1のみ未塗装 → 板1に色3を割り当て
- 最終結果:
3 7 7 7 7
計算量
- 時間計算量: \(O(N + M \cdot \alpha(N))\)(\(\alpha\) はアッカーマン関数の逆関数で、実質定数。各板は高々1回だけ塗られるため、全操作を通じた塗装回数の合計は \(O(N)\))
- 空間計算量: \(O(N + M)\)
実装のポイント
parent配列のサイズは \(N+2\) にする。parent[N+1]が番兵として機能し、findが \(N+1\) を返した時点で区間外と判定できるPython では再帰による
findはスタックオーバーフローの危険があるため、ループ+経路圧縮(path halving) で実装している(parent[x] = parent[parent[x]])入力は
sys.stdin.buffer.read()で一括読み込みし、高速化しているソースコード
import sys
def main():
input_data = sys.stdin.buffer.read().split()
idx = 0
N = int(input_data[idx]); idx += 1
M = int(input_data[idx]); idx += 1
boards = [0] * (N + 1)
# Process operations in reverse order; skip already painted positions
# Use a set or interval structure to track unpainted positions
# We'll use a sorted set approach with an interval list
# We maintain a list of "free" intervals (not yet assigned)
# Initially all [1, N] is free
# Process from last operation to first; for each operation [L, R, C],
# find all free positions in [L, R], assign them C, and remove them from free set.
# Use a simple approach with a "next unpainted" array (like DSU/path compression)
# parent[i] points to the next unpainted position >= i
parent = list(range(N + 2)) # parent[i] for i in 0..N+1
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
operations = []
for i in range(M):
L = int(input_data[idx]); idx += 1
R = int(input_data[idx]); idx += 1
C = int(input_data[idx]); idx += 1
operations.append((L, R, C))
# Process in reverse
for i in range(M - 1, -1, -1):
L, R, C = operations[i]
pos = find(L)
while pos <= R:
boards[pos] = C
parent[pos] = pos + 1 # mark as painted, point to next
pos = find(pos + 1)
sys.stdout.write(' '.join(map(str, boards[1:N+1])) + '\n')
main()
この解説は claude4.6opus-thinking によって生成されました。