D - 点検スケジュール / Inspection Schedule Editorial by admin
gemini-3.5-flash-high概要
この問題は、全長 \(T\) km の高速道路に点検区間をいくつか設置し、間隔制約を満たしつつ、点検区間と重なるイベント会場の数を最小化する問題です。
イベント会場の数 \(N\) が最大で \(12\) と非常に小さいことに着目し、すでに重なったイベント会場の集合をビットで表すビットDP(動的計画法)を用いて解くことができます。
考察
1. イベント会場の数が小さいことに注目する
イベント会場の数 \(N\) は \(0 \le N \le 12\) と非常に小さいです。このように「選択した要素の集合」の全探索が \(2^N\) 通り(\(2^{12} = 4096\) 通り)に収まる場合、ビットDP(状態圧縮DP)が強力なアプローチになります。
2. 間隔制約の整理と「仮想的な開始地点」の導入
点検区間の開始地点を \(s_1 \le s_2 \le \cdots \le s_p\) としたとき、間隔制約は以下の通りです。 1. \(s_1 \le M\) 2. \(s_{j+1} - s_j \le M + K\) (\(1 \le j \le p-1\)) 3. \(T - s_p \le M + K\)
ここで、仮想的な \(0\) 番目の点検区間の開始地点を \(s_0 = -K\) と定義してみます。 すると、1番目の制約は以下のように変形できます。 $\(s_1 - s_0 = s_1 - (-K) \le M + K \iff s_1 \le M\)$
これにより、1番目の制約(起点からの距離)を、2番目の制約(点検区間同士の距離)と同じ形式 \(s_{j+1} - s_j \le M + K\) に統一して扱うことができます。
同様に、終端条件(最後の点検区間から終点まで)は以下のように表せます。 $\(s_p \ge T - K - M\)$
3. DP(動的計画法)の設計
点検区間の開始地点としてあり得る位置 \(s\) を \(0\) から \(T-K\) まで順に走査し、各 \(s\) を「採用するか・しないか」を決定していくDPを考えます。
状態として「重なったイベント会場の集合(ビットマスク)」を持たせますが、遷移(間隔制約の判定)を行うためには、最後に配置した点検区間の開始地点の情報が必要です。 次に配置できる点検区間の開始地点 \(s_{\text{next}}\) は、最後に配置した位置 \(s_{\text{last}}\) に対して \(s_{\text{next}} - s_{\text{last}} \le M + K \iff s_{\text{last}} \ge s_{\text{next}} - M - K\) を満たす必要があります。
したがって、同じイベントの被り状況(ビットマスク)であれば、最後に配置した位置 \(s_{\text{last}}\) は大きければ大きいほど、今後の選択肢が広がり有利になります。
これより、以下のDPテーブルを定義します。 - \(\text{dp}[\text{bit}]\) : 重なったイベント会場の集合が \(\text{bit}\) であるとき、最後に配置した点検区間の開始地点の最大値(不可能な場合は \(-\infty\))
アルゴリズム
ステップ 1: 特例処理
\(T \le M\) の場合、点検区間を \(0\) 個(\(p=0\))にすることが許されます。このときイベント会場との重複は \(0\) にできるため、即座に 0 を出力して終了します。
ステップ 2: イベント重複マスクの前計算
各 \(s \in [0, T-K]\) について、点検区間 \((s, s+K)\) を設置したときに重なるイベント会場の集合をビットマスク mask[s] として前計算しておきます。
点検区間 \((s, s+K)\) とイベント \((L_i, R_i)\) が重なる条件は、\(s < R_i\) かつ \(L_i < s+K\) です。
ステップ 3: DPテーブルの初期化
DPテーブルのサイズは \(2^N\) です。 - 仮想的な初期位置 \(s_0 = -K\) にのみ到達可能であるため、\(\text{dp}[0] = -K\) とします。 - それ以外の状態は \(-\infty\)(到達不可能)で初期化します。
ステップ 4: DPの遷移
\(s\) を \(0\) から \(T-K\) まで順にループします。
各 \(s\) について、現在のDPテーブルをコピーした配列 nxt を用意し、以下のように遷移を行います。
各状態 \(\text{bit}\) について、\(\text{dp}[\text{bit}] \ge s - M - K\) を満たす(=直前の点検区間から \(s\) まで間隔制約を満たして到達できる)場合、点検区間 \(s\) を新たに配置できます。
$\(\text{nxt}[\text{bit} \mid \text{mask}[s]] = \max(\text{nxt}[\text{bit} \mid \text{mask}[s]], s)\)$
※ \(s\) を選ばない場合は状態は変化しないため、あらかじめ nxt を dp の内容で初期化しておくことで対応します。
ステップ 5: 答えの集計
すべての \(s\) の走査が終了した後、終端条件を満たす状態から答えを探します。 \(\text{dp}[\text{bit}] \ge T - K - M\) を満たすすべての \(\text{bit}\) のうち、立っているビット数(popcount)が最小のものが答えとなります。
計算量
時間計算量: \(O(T \cdot 2^N)\)
- マスクの前計算: \(O(T \cdot N)\)
- DPの遷移: 外側のループが \(T - K + 1 \le T\) 回、内側のループが \(2^N\) 回です。よって全体で \(O(T \cdot 2^N)\) となります。
- \(T \le 1000, N \le 12\) のとき、最悪でも \(1000 \times 4096 \approx 4 \times 10^6\) 回程度の演算であり、実行時間制限に余裕で間に合います。
空間計算量: \(O(T + 2^N)\)
mask配列のサイズが \(T - K + 1\)、DPテーブルのサイズが \(2^N\) です。- メモリ使用量は数メガバイト程度に収まり、非常に軽量です。
実装のポイント
インプレース更新の回避: 同じ \(s\) を 1 回の遷移で複数回選んでしまう(同じ場所に何重にも重ねて置いてしまう)のを防ぐため、遷移元となる
curr配列と、遷移先となるnxt配列を分けて管理しています。popcountの高速化: 最後の集計で各ビットマスクの立っているビット数を数える際、あらかじめ \(0\) から \(2^N-1\) までの popcount をテーブル化しておくことで、定数倍の高速化を図っています。
ソースコード
import sys
def solve():
input = sys.stdin.read
data = input().split()
if not data:
return
T = int(data[0])
N = int(data[1])
K = int(data[2])
M = int(data[3])
L = []
R = []
idx = 4
for _ in range(N):
L.append(int(data[idx]))
R.append(int(data[idx+1]))
idx += 2
if T <= M:
print(0)
return
# 各 s における点検区間 (s, s+K) と重なるイベント会場のビットマスクを前計算
mask = [0] * (T - K + 1)
for s in range(T - K + 1):
m = 0
for i in range(N):
if s < R[i] and L[i] < s + K:
m |= (1 << i)
mask[s] = m
num_states = 1 << N
INF = 10**9
dp0 = [-INF] * num_states
dp1 = [-INF] * num_states
# 初期状態: 起点 (位置 -K) に bit 0 が到達可能
dp0[0] = -K
curr = dp0
nxt = dp1
for s in range(T - K + 1):
mask_s = mask[s]
limit = s - M - K
nxt[:] = curr
for bit in range(num_states):
val = curr[bit]
if val >= limit:
nbit = bit | mask_s
if s > nxt[nbit]:
nxt[nbit] = s
curr, nxt = nxt, curr
popcount = [bin(i).count('1') for i in range(num_states)]
ans = N
limit_end = T - K - M
for bit in range(num_states):
if curr[bit] >= limit_end:
if popcount[bit] < ans:
ans = popcount[bit]
print(ans)
if __name__ == '__main__':
solve()
この解説は gemini-3.5-flash-high によって生成されました。
posted:
last update: