公式

E - 花壇の水やり管理 / Flowerbed Watering Management 解説 by admin

Gemini 3.0 Flash (Thinking)

概要

この問題は、配列に対する「区間加算(Range Update)」と「区間和の取得(Range Sum Query)」を効率的に行う問題です。

花壇の数 \(N\) と操作の数 \(Q\) がともに最大 \(10^5\) と大きいため、愚直に配列を更新・走査する方法では間に合いません。データ構造を工夫して高速に処理する必要があります。

考察

1. 愚直な方法の限界

操作 1(水やり)で \(l\) から \(r\) までの要素すべてに \(v\) を加算し、操作 2(確認)で \(l\) から \(r\) までの合計を計算するとします。 最悪の場合、1回の操作に \(O(N)\) の時間がかかります。操作は \(Q\) 回行われるため、全体の計算量は \(O(NQ)\) となります。 \(N, Q = 10^5\) のとき、\(NQ = 10^{10}\) となり、一般的な実行時間制限(2秒程度)を大幅に超えてしまいます(通常 \(10^8\) 回程度の計算が限界です)。

2. 効率的なアプローチ

「区間の更新」と「区間の和」を同時に高速化できるデータ構造が必要です。代表的なものに以下の2つがあります。 - 遅延評価セグメント木 (Lazy Segment Tree): 一般的な手法ですが、実装がやや複雑です。 - 2つの Binary Indexed Tree (BIT): 実装が軽量で、今回の「加算」と「和」の操作であれば BIT 2つで対応可能です。

正解コードでは、実装のシンプルさと実行速度の面から BIT を2つ使う手法 を採用しています。

アルゴリズム

BITによる区間更新と区間和

通常、BITは「点更新」と「累積和(区間和)」を \(O(\log N)\) で行うデータ構造ですが、数式を変形することで「区間更新」と「区間和」にも対応できます。

配列 \(C\) の階差数列を \(D\) とします(\(D_i = C_i - C_{i-1}\))。このとき、ある要素 \(C_i\)\(C_i = \sum_{j=1}^i D_j\) と表せます。 求めたい \(k\) までの累積和 \(S_k = \sum_{i=1}^k C_i\) は、以下のように変形できます。

\[ \begin{aligned} S_k = \sum_{i=1}^k C_i &= \sum_{i=1}^k \sum_{j=1}^i D_j \\ &= \sum_{j=1}^k (k - j + 1) D_j \\ &= (k + 1) \sum_{j=1}^k D_j - \sum_{j=1}^k (j \cdot D_j) \end{aligned} \]

この式を見ると、以下の2つの値を管理できれば累積和が求まることがわかります。 1. \(\sum D_j\) (階差数列の和) \(\rightarrow\) BIT1 で管理 2. \(\sum (j \cdot D_j)\) (インデックスを掛けた階差数列の和) \(\rightarrow\) BIT2 で管理

操作1:区間 \([l, r]\)\(v\) を加算

階差数列 \(D\) においては、インデックス \(l\)\(+v\)、インデックス \(r+1\)\(-v\) するだけで区間更新を表現できます。 - BIT1: \(l\)\(v\) を加算、\(r+1\)\(-v\) を加算 - BIT2: \(l\)\(l \cdot v\) を加算、\(r+1\)\((r+1) \cdot (-v)\) を加算

操作2:区間 \([l, r]\) の和を取得

\((\text{累積和 } S_r) - (\text{累積和 } S_{l-1})\) を計算します。各累積和は上記の変形した式を用いて、2つの BIT から取得します。

計算量

  • 時間計算量: \(O((N + Q) \log N)\)
    • 初期値の構築に \(O(N)\) または \(O(N \log N)\)
    • 各クエリに対し \(O(\log N)\)
  • 空間計算量: \(O(N)\)
    • サイズ \(N\) の BIT を 2 つ使用

実装のポイント

  • 高速な入出力: Pythonでは input() を繰り返すと遅いため、sys.stdin.read().split() で一括読み込みを行うのが一般的です。

  • 1-indexed: BITは構造上、インデックスを 1 から始める必要があるため、配列のサイズやループの範囲に注意します。

  • 初期値の処理: 最初に与えられる \(C_i\) も、「区間 \([i, i]\)\(C_i\) を加算する」という操作 1 とみなして処理するか、階差をとって \(O(N)\) で BIT を構築します。

    ソースコード

import sys

def solve():
    # Fast I/O: read all input at once and split into a list of strings
    try:
        input_all = sys.stdin.read().split()
    except EOFError:
        return
    if not input_all:
        return
    
    # N: number of flowerbeds, Q: number of operations
    N = int(input_all[0])
    Q = int(input_all[1])
    
    # We use two Binary Indexed Trees (BITs) to handle range updates and range sums.
    # Let D[i] be the difference array such that C[i] = sum(D[1...i]).
    # Then the prefix sum of C up to k is:
    # sum(C[1...k]) = (k + 1) * sum(D[1...k]) - sum(j * D[j] for j in 1...k)
    # bit1 stores D[j], and bit2 stores j * D[j].
    
    # size = N + 1 to handle range updates at r + 1 where r can be N
    size = N + 1
    bit1 = [0] * (size + 1)
    bit2 = [0] * (size + 1)
    
    # Process initial values of C_i (flowerbeds water amounts)
    # Initial values are at input_all[2] through input_all[N+1]
    prev_C = 0
    for i in range(1, N + 1):
        curr_C = int(input_all[i + 1])
        diff = curr_C - prev_C
        bit1[i] = diff
        bit2[i] = i * diff
        prev_C = curr_C
        
    # O(N) construction of BITs from the initial difference values
    for i in range(1, size + 1):
        j = i + (i & -i)
        if j <= size:
            bit1[j] += bit1[i]
            bit2[j] += bit2[i]
    
    ptr = N + 2
    results = []
    
    # Process Q operations
    for _ in range(Q):
        q_type = input_all[ptr]
        if q_type == '1':
            # Operation 1: Range Update [l, r] with value v
            l = int(input_all[ptr + 1])
            r = int(input_all[ptr + 2])
            v = int(input_all[ptr + 3])
            
            # Update bit1: add v at l, subtract v at r+1
            idx = l
            while idx <= size:
                bit1[idx] += v
                idx += idx & -idx
            idx = r + 1
            while idx <= size:
                bit1[idx] -= v
                idx += idx & -idx
            
            # Update bit2: add l*v at l, subtract (r+1)*v at r+1
            idx = l
            val_l = l * v
            while idx <= size:
                bit2[idx] += val_l
                idx += idx & -idx
            idx = r + 1
            val_r = (r + 1) * v
            while idx <= size:
                bit2[idx] -= val_r
                idx += idx & -idx
            ptr += 4
        else:
            # Operation 2: Range Sum Query [l, r]
            l = int(input_all[ptr + 1])
            r = int(input_all[ptr + 2])
            
            # Prefix sum up to r
            s1_r, s2_r = 0, 0
            i = r
            while i > 0:
                s1_r += bit1[i]
                s2_r += bit2[i]
                i -= i & -i
            ps_r = (r + 1) * s1_r - s2_r
            
            # Prefix sum up to l-1
            s1_l, s2_l = 0, 0
            i = l - 1
            while i > 0:
                s1_l += bit1[i]
                s2_l += bit2[i]
                i -= i & -i
            ps_l = l * s1_l - s2_l
            
            # The range sum [l, r] is prefix_sum(r) - prefix_sum(l-1)
            results.append(str(ps_r - ps_l))
            ptr += 3
            
    # Output all query results separated by newlines
    if results:
        sys.stdout.write('\n'.join(results) + '\n')

if __name__ == '__main__':
    solve()

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

投稿日時:
最終更新: