公式

E - フェンスの塗装 / Painting the Fence 解説 by admin

gpt-5.5-high

概要

すべての塗装指示を実行した場合に塗られる板の枚数から、「無視したことで塗られなくなる板の枚数」を引いて考えます。
各板について、その板を塗る指示番号の集合が、無視する連続 \(K\) 回の中にすべて含まれるときだけ、その板は失われます。

考察

\(x\) を塗る指示番号の集合を \(S_x\) とします。

無視する区間を \([s, s+K-1]\) としたとき、板 \(x\) が最終的に塗られないのは次の場合です。

  • もともとは少なくとも 1 回塗られる
  • その板を塗るすべての指示が、無視する区間 \([s, s+K-1]\) に含まれる

つまり、

\[ S_x \subseteq [s, s+K-1] \]

であるときだけ、板 \(x\) は「失われる」ことになります。

したがって、答えは

\[ \text{全指示を実行したときに塗られる板の枚数} - \text{失われる板の枚数の最小値} \]

です。


素朴に各 \(s\) について実行する指示を集めて区間の union を計算すると、\(s\) は最大 \(M\) 通りあり、各回に \(O(M \log M)\) かかるため、全体で \(O(M^2 \log M)\) となり間に合いません。

また、\(N\) は最大 \(10^9\) なので、板を 1 枚ずつ見ることもできません。

そこで、塗装区間の端点だけに注目します。
区間の端点の間では、「現在その場所を塗っている指示の集合」は変化しません。

例えば、ある連続した板の範囲で有効な指示集合が

\[ S = \{2, 5, 6\} \]

だったとします。

この範囲が失われるには、無視する区間 \([s, s+K-1]\)\(2,5,6\) をすべて含む必要があります。
つまり、最小の指示番号と最大の指示番号だけを見れば十分です。

\[ \min S = 2,\quad \max S = 6 \]

なので、条件は

\[ s \leq 2 \]

かつ

\[ s+K-1 \geq 6 \]

です。

変形すると、

\[ 6-K+1 \leq s \leq 2 \]

となります。

一般に、現在の有効な指示集合を \(S\)、その最小値を \(\mathrm{mn}\)、最大値を \(\mathrm{mx}\) とすると、その範囲が失われる \(s\)

\[ \mathrm{mx}-K+1 \leq s \leq \mathrm{mn} \]

を満たすものです。

ただし、\(s\)

\[ 1 \leq s \leq M-K+1 \]

でなければならないので、実際には

\[ \max(1, \mathrm{mx}-K+1) \leq s \leq \min(\mathrm{mn}, M-K+1) \]

です。

この範囲のすべての \(s\) に対して、「この板の範囲の長さ」だけ失われる枚数を加算すればよいです。

アルゴリズム

まず、各塗装指示 \([L_i, R_i]\) を半開区間として扱います。

\[ [L_i, R_i+1) \]

とすることで、長さをそのまま

\[ R_i+1-L_i \]

として扱えます。

各指示について、以下の 2 種類のイベントを作ります。

  • 位置 \(L_i\) で指示 \(i\) が有効になる
  • 位置 \(R_i+1\) で指示 \(i\) が無効になる

これらのイベントを座標順に処理します。


掃き出し中、現在有効な指示番号集合 \(S\) について、以下を高速に取得したいです。

  • 最小の指示番号 \(\mathrm{mn}\)
  • 最大の指示番号 \(\mathrm{mx}\)

そのために、2 つのヒープを使います。

  • 最小値取得用の min-heap
  • 最大値取得用の max-heap

Python では max-heap がないので、負の値を入れて管理します。

削除については、ヒープから直接削除するのは難しいため、alive[i] で指示 \(i\) が現在有効かどうかを管理し、ヒープの先頭が無効な指示なら取り除く、という遅延削除を行います。


ある座標 \(x\) のイベントをすべて処理した後、次のイベント座標を \(nx\) とします。

このとき、区間

\[ [x, nx) \]

では有効な指示集合は変化しません。

長さは

\[ nx-x \]

です。

有効な指示が 1 つ以上あるなら、この範囲は全指示を実行した場合には塗られるので、total に長さを加えます。

さらに、この範囲が失われる無視区間の開始位置 \(s\) を求めます。

現在有効な指示番号の最小値を \(\mathrm{mn}\)、最大値を \(\mathrm{mx}\) とすると、この範囲が失われる条件は

\[ [s, s+K-1] \supseteq [\mathrm{mn}, \mathrm{mx}] \]

です。

したがって、

\[ \mathrm{mx}-K+1 \leq s \leq \mathrm{mn} \]

です。

実際に選べる \(s\)

\[ 1 \leq s \leq M-K+1 \]

なので、加算する範囲は

\[ \max(1, \mathrm{mx}-K+1) \]

から

\[ \min(\mathrm{mn}, M-K+1) \]

までです。

この範囲に現在の長さを加算します。

範囲加算を高速に行うため、imos 法を使います。
diff[l] += length, diff[r+1] -= length としておき、最後に累積和を取ることで各 \(s\) に対する失われる枚数が分かります。

最後に、失われる枚数の最小値を min_loss として、

\[ \text{答え} = \text{total} - \text{min_loss} \]

を出力します。

計算量

  • 時間計算量: \(O(M \log M)\)
  • 空間計算量: \(O(M)\)

イベント数は各指示につき 2 個なので \(2M\) 個です。
イベントのソートに \(O(M \log M)\)、掃き出し中のヒープ操作も全体で \(O(M \log M)\) です。

実装のポイント

半開区間 \([L_i, R_i+1)\) として扱うことで、連続した板の枚数を単純に座標差で計算できます。

events[p] = (l << shift) | i
events[p] = ((r + 1) << shift) | (M + i)

コードではイベントをタプルではなく整数に詰めて管理しています。
これは高速化のためで、意味としては次のようなイベントを持っているのと同じです。

  • (座標, 指示番号) : 追加イベント
  • (座標, M + 指示番号) : 削除イベント

また、同じ座標にあるイベントはすべてまとめて処理します。
その後、次のイベント座標までの区間について、有効な指示集合が一定であるとして処理します。

ヒープでは削除済みの指示が残ることがあるので、先頭が無効なら取り除きます。

while min_heap and not alive[min_heap[0]]:
    heappop(min_heap)

while max_heap and not alive[-max_heap[0]]:
    heappop(max_heap)

これにより、現在有効な指示番号の最小値と最大値を正しく取得できます。

ソースコード

import sys
import heapq

def main():
    input = sys.stdin.buffer.readline
    N, M, K = map(int, input().split())

    shift = (2 * M + 1).bit_length()
    mask = (1 << shift) - 1

    events = [0] * (2 * M)
    p = 0
    for i in range(1, M + 1):
        l, r = map(int, input().split())
        events[p] = (l << shift) | i
        p += 1
        events[p] = ((r + 1) << shift) | (M + i)
        p += 1

    events.sort()

    s_count = M - K + 1
    diff = [0] * (s_count + 2)

    alive = bytearray(M + 1)
    min_heap = []
    max_heap = []
    heappush = heapq.heappush
    heappop = heapq.heappop

    total = 0
    i = 0
    e_len = 2 * M

    while i < e_len:
        x = events[i] >> shift

        while i < e_len and (events[i] >> shift) == x:
            code = events[i] & mask
            if code <= M:
                idx = code
                alive[idx] = 1
                heappush(min_heap, idx)
                heappush(max_heap, -idx)
            else:
                idx = code - M
                alive[idx] = 0
            i += 1

        while min_heap and not alive[min_heap[0]]:
            heappop(min_heap)
        while max_heap and not alive[-max_heap[0]]:
            heappop(max_heap)

        if i < e_len and min_heap:
            length = (events[i] >> shift) - x
            total += length

            mn = min_heap[0]
            mx = -max_heap[0]

            if mx - mn < K:
                left = mx - K + 1
                if left < 1:
                    left = 1
                right = mn
                if right > s_count:
                    right = s_count

                if left <= right:
                    diff[left] += length
                    diff[right + 1] -= length

    cur = 0
    min_loss = 10**30
    for s in range(1, s_count + 1):
        cur += diff[s]
        if cur < min_loss:
            min_loss = cur

    print(total - min_loss)

if __name__ == "__main__":
    main()

この解説は gpt-5.5-high によって生成されました。

投稿日時:
最終更新: