Official

E - 本棚への配架 / Shelving Books on a Bookshelf Editorial by admin

Gemini 3.0 Flash (Thinking)

概要

\(N\) 冊の本を、重さの条件を満たす本棚の \(M\) 個の空きスペースのうち「最も左側にあるもの」に順番に置いていき、最終的に何冊の本を置くことができるかを求める問題です。

考察

各本について、条件を満たすスペースを左から順番に探すという素朴な方法(線形探索)を考えてみます。 この場合、1冊の本に対して最大 \(M\) 個のスペースを確認する必要があるため、最悪のケースでの計算量は \(O(N \times M)\) となります。本問題の制約では \(N, M \leq 2 \times 10^5\) であるため、最大で \(4 \times 10^{10}\) 回程度の計算が必要になり、実行時間制限(TLE)を超えてしまいます。

効率的に解くためには、以下の2点を高速に行う必要があります。 1. 耐荷重が \(R_i\) 以上である空きスペースのうち、最もインデックスが小さいものを探す。 2. 本を置いたスペースを「使用済み」として更新する。

これを実現するために、セグメント木(Segment Tree)というデータ構造を利用します。

アルゴリズム

セグメント木の各ノードに、その区間内にある空きスペースの「耐荷重の最大値」を保持させます。

1. セグメント木の構築

本棚の各スペースの耐荷重 \(C_1, C_2, \ldots, C_M\) を葉(最下層)に並べ、親ノードには左右の子の最大値を持たせます。これにより、任意の区間内における最大耐荷重を \(O(1)\) で参照できるようになります。

2. 条件を満たすスペースの探索

ある重さ \(R_i\) の本を置く場所を探す際、木の根(全体範囲)から以下のように探索します(\(O(\log M)\)): - 現在のノードの最大値が \(R_i\) 未満であれば、その区間に置ける場所はありません。 - 左側の子ノードの最大値が \(R_i\) 以上であれば、左側の区間に条件を満たすスペースが存在するため、左へ進みます。 - 左側の子ノードの最大値が \(R_i\) 未満で、右側の子ノードの最大値が \(R_i\) 以上であれば、右へ進みます。

これを葉に到達するまで繰り返すことで、「条件を満たす最も左側のスペース」を高速に見つけることができます。

3. スペースの更新

本を置いたスペースの耐荷重を \(0\) に書き換えます。その後、その葉から根に向かって最大値を再計算し、木の状態を更新します(\(O(\log M)\))。

4. 全体の流れ

すべての本 \(R_1, R_2, \ldots, R_N\) について上記の探索と更新を繰り返します。本を置くことができた回数をカウントし、最終的な答えとします。

計算量

  • 時間計算量: \(O((N + M) \log M)\)
    • セグメント木の構築に \(O(M)\)
    • 各本に対する探索と更新に \(O(\log M)\)。これが \(N\) 冊分あるため \(O(N \log M)\) となります。
  • 空間計算量: \(O(M)\)
    • セグメント木を管理するための配列に \(O(M)\) のメモリを使用します。

実装のポイント

  • セグメント木のサイズ: \(M\) 以上の最小の2のべき乗(\(2^k\))を葉の数とすることで、実装がシンプルになります。

  • 高速な入出力: Pythonの場合、データ量が多い(\(4 \times 10^5\) 個以上の整数)ため、sys.stdin.read().split() などを用いて一括で入力を読み込むと実行時間を短縮できます。

  • 更新の最適化: 親ノードの値を更新する際、値が変化しなくなった時点で更新を打ち切ることで、定数倍の高速化が期待できます。

    ソースコード

import sys

# 本棚への配架問題を解決するために、セグメント木(Segment Tree)を使用します。
# セグメント木は、各区間における耐荷重の最大値を保持します。
# これにより、ある重さ以上の耐荷重を持つ「最も左側(番号が小さい)の空きスペース」を効率的に探索できます。

def solve():
    # 標準入力からすべてのデータを一括で読み込み、高速化を図ります。
    try:
        input_data = sys.stdin.read().split()
    except EOFError:
        return
    
    if not input_data:
        return
    
    # N: 本の冊数, M: 本棚のスペース数
    N = int(input_data[0])
    M = int(input_data[1])
    
    # R: 各本の重さ, C: 各スペースの耐荷重
    R = list(map(int, input_data[2:2+N]))
    C = list(map(int, input_data[2+N:2+N+M]))
    
    # セグメント木のサイズを決定します(M以上の最小の2のべき乗)。
    size = 1
    while size < M:
        size <<= 1
    
    # tree[1]をルートとし、tree[size]以降を葉とする配列を初期化します。
    # 各要素は、その範囲内にある空きスペースの耐荷重の最大値を表します。
    tree = [0] * (2 * size)
    for i in range(M):
        tree[size + i] = C[i]
    
    # ボトムアップでセグメント木を構築します。
    for i in range(size - 1, 0, -1):
        left_val = tree[i << 1]
        right_val = tree[(i << 1) | 1]
        if left_val > right_val:
            tree[i] = left_val
        else:
            tree[i] = right_val
            
    placed_books_count = 0
    
    # 各本について順番に配架場所を検討します。
    for r in R:
        # 本棚全体の最大耐荷重が本の重さ未満であれば、その本は置けません。
        if tree[1] < r:
            continue
        
        # 条件を満たす最も左側のスペースを木の上から探索します。
        idx = 1
        while idx < size:
            left_child = idx << 1
            # 左側の子(より小さい番号のスペースを含む範囲)に条件を満たすスペースがあるか確認
            if tree[left_child] >= r:
                idx = left_child
            else:
                # 左側にない場合は右側の子へ
                idx = left_child | 1
        
        # 本を置くことができたのでカウントを増やします。
        placed_books_count += 1
        
        # 本を置いたスペースの耐荷重を0に更新(既に使用済みであることを示す)。
        tree[idx] = 0
        
        # 木を葉からルートに向かって更新します。
        idx >>= 1
        while idx >= 1:
            left_val = tree[idx << 1]
            right_val = tree[(idx << 1) | 1]
            
            if left_val > right_val:
                new_max = left_val
            else:
                new_max = right_val
            
            # 最大値が変化しなくなったら、それ以上の親の更新は不要です。
            if tree[idx] == new_max:
                break
            tree[idx] = new_max
            idx >>= 1
                
    # 最終的に本棚に置くことができた本の冊数を出力します。
    sys.stdout.write(str(placed_books_count) + '\n')

if __name__ == '__main__':
    solve()

この解説は gemini-3-flash-thinking によって生成されました。

posted:
last update: