Official

E - 倉庫の在庫管理 / Warehouse Inventory Management Editorial by admin

gpt-5.5-high

概要

\(D_i = B_i - A_i\) とおくと、求めたい値は \(\sum_i \max(0, D_i)\) です。
報告は区間に対して \(D_i\) を加算または減算する操作なので、「区間加算」と「正の部分の総和」を高速に管理します。

考察

まず、各倉庫について重要なのは \(A_i, B_i\) そのものではなく、差分

\(D_i = B_i - A_i\)

です。

不足分は \(\max(0, D_i)\) なので、答えは

\(\sum_{i=1}^{N} \max(0, D_i)\)

です。

報告による変化は次のように言い換えられます。

  • \(T_j = 1\)\(B_i\)\(X_j\) 増える
    \(D_i\)\(X_j\) 増える
  • \(T_j = 2\)\(A_i\)\(X_j\) 増える
    \(D_i\)\(X_j\) 減る

つまり、問題は次の形になります。

数列 \(D\) に対して区間加算を行い、毎回 \(\sum \max(0, D_i)\) を求める。

素朴に各クエリで区間内の全要素を更新し、その後全体を集計すると \(O(NQ)\) かかります。
\(N, Q \leq 5 \times 10^4\) なので、最大で \(2.5 \times 10^9\) 回程度の処理になり、間に合いません。

そこで、平方分割を使います。

数列をいくつかのブロックに分け、ブロック全体に対する更新はまとめて処理します。
各ブロックでは、要素をソートした配列と累積和を持っておくことで、

\(\sum \max(0, D_i)\)

を高速に再計算できます。

アルゴリズム

\(D_i = B_i - A_i\) を配列として持ちます。

配列を長さ \(K\) 程度のブロックに分割します。
実装では \(K = 256\) としています。

各ブロックについて、次の情報を管理します。

  • arr[i]:各要素の値
  • lazy[b]:ブロック全体に加えられている遅延加算値
  • sblocks[b]:ブロック内の arr をソートしたもの
  • prefs[b]sblocks[b] の累積和
  • bsum[b]:そのブロックにおける \(\sum \max(0, D_i)\)
  • total:全ブロックの bsum の合計、つまり現在の答え

ここで、実際の値は

\(arr[i] + lazy[b]\)

です。

ブロック全体を更新する場合

ブロック全体に \(\delta\) を加算する場合を考えます。

  • \(T = 1\) なら \(\delta = X\)
  • \(T = 2\) なら \(\delta = -X\)

ブロック全体への加算なので、arr[i] は直接変更せず、lazy[b]\(\delta\) を足します。

その後、そのブロックの

\(\sum \max(0, arr[i] + lazy[b])\)

を計算します。

ブロック内の値をソートした配列を \(v\) とします。
正の値になる条件は

\(v + lazy[b] > 0\)

つまり

\(v > -lazy[b]\)

です。

ソート済み配列に対して二分探索を行い、\(v \leq -lazy[b]\) となる最後の位置を求めます。
それより後ろの要素だけが正の値になります。

正になる要素の個数を \(cnt\)、それらの arr の合計を \(sum\) とすると、ブロックの不足分合計は

\(sum + lazy[b] \times cnt\)

です。

累積和を持っているので、\(sum\)\(O(1)\)、境界位置は二分探索で \(O(\log K)\) で求められます。

ブロックの一部だけを更新する場合

区間の端など、ブロックの一部だけが更新対象になる場合は、その部分の要素を直接更新します。

各要素について、更新前の実際の値を

\(old = arr[i] + lazy[b]\)

更新後の値を

\(new = old + \delta\)

とすると、答えへの変化量は

\(\max(0, new) - \max(0, old)\)

です。

これを bsum[b]total に反映します。

また、arr[i]\(\delta\) 増やします。

ただし、この操作をすると、ブロック内のソート済み配列 sblocks[b] が古くなります。
そのため、そのブロックに dirty フラグを立てておきます。

次にそのブロックを「ブロック全体更新」で使う前に、必要ならソート済み配列と累積和を作り直します。

クエリ処理

各クエリの区間 \([L, R]\) に対して、以下のように処理します。

  1. \(T=1\) なら \(\delta = X\)\(T=2\) なら \(\delta = -X\) とする
  2. 区間が含むブロックを調べる
  3. 左端・右端の中途半端な部分は、要素ごとに直接更新する
  4. 完全に含まれるブロックは、遅延加算と二分探索でまとめて更新する
  5. total を出力する

計算量

ブロックサイズを \(K\) とします。

  • 部分的なブロック更新は高々 2 ブロックなので \(O(K)\)
  • 完全に含まれるブロックは高々 \(O(N/K)\) 個で、それぞれ二分探索に \(O(\log K)\)
  • dirty なブロックの再構築は \(O(K \log K)\) ですが、部分更新があったブロックに対して必要なときだけ行うため、全体でならして扱えます

したがって、1 クエリあたりならしで

\(O\left(K \log K + \frac{N}{K}\log K\right)\)

程度です。

\(K \approx \sqrt{N}\) とすると、

  • 時間計算量: \(O(Q \sqrt{N} \log N)\) 程度
  • 空間計算量: \(O(N)\)

です。

実装では \(K = 256\) としており、\(N, Q \leq 5 \times 10^4\) に十分高速です。

実装のポイント

arr[i] は実際の値そのものではなく、ブロックの lazy を除いた値として扱います。
実際の値は常に

\(arr[i] + lazy[b]\)

です。

部分更新では lazy を反映してから更新するのではなく、arr[i] に直接 \(\delta\) を加えます。
これにより、実際の値も正しく \(\delta\) だけ変化します。

また、正の部分だけを足すため、二分探索では

\(v + lazy > 0\)

すなわち

\(v > -lazy\)

となる最初の位置を求めます。
値がちょうど \(0\) のものは不足分に含まれないため、bisect_right を使っています。

答えは最大で \(10^{18}\) になるため、他言語では 64 bit 整数型を使う必要があります。Python では整数が任意精度なのでそのままで問題ありません。

ソースコード

import sys
from bisect import bisect_right

def main():
    data = list(map(int, sys.stdin.buffer.read().split()))
    if not data:
        return

    N = data[0]
    Q = data[1]
    pos = 2

    arr = [0] * N
    for i in range(N):
        A = data[pos]
        B = data[pos + 1]
        pos += 2
        arr[i] = B - A

    SHIFT = 8
    BS = 1 << SHIFT
    nb = (N + BS - 1) >> SHIFT

    ends = [0] * nb
    lens = [0] * nb
    sblocks = [None] * nb
    prefs = [None] * nb
    base_sum = [0] * nb
    lazy = [0] * nb
    bsum = [0] * nb
    dirty = [0] * nb

    brgt = bisect_right
    total = 0

    for b in range(nb):
        s = b << SHIFT
        e = s + BS
        if e > N:
            e = N
        ends[b] = e
        m = e - s
        lens[b] = m

        lst = sorted(arr[s:e])
        sblocks[b] = lst

        pref = [0] * (m + 1)
        acc = 0
        j = 1
        for v in lst:
            acc += v
            pref[j] = acc
            j += 1

        prefs[b] = pref
        base_sum[b] = acc

        idx = brgt(lst, 0)
        sm = acc - pref[idx]
        bsum[b] = sm
        total += sm

    def rebuild(b, a=arr, ends=ends, lens=lens, sblocks=sblocks,
                prefs=prefs, base_sum=base_sum, dirty=dirty, shift=SHIFT):
        s = b << shift
        e = ends[b]
        m = lens[b]

        lst = sorted(a[s:e])
        sblocks[b] = lst

        pref = [0] * (m + 1)
        acc = 0
        j = 1
        for v in lst:
            acc += v
            pref[j] = acc
            j += 1

        prefs[b] = pref
        base_sum[b] = acc
        dirty[b] = 0

    def partial_update(b, left, right, delta, total,
                       a=arr, lazy=lazy, bsum=bsum, dirty=dirty):
        lz = lazy[b]
        tb = bsum[b]

        if delta > 0:
            for i in range(left, right + 1):
                base = a[i]
                old = base + lz
                a[i] = base + delta
                if old > 0:
                    tb += delta
                    total += delta
                else:
                    nv = old + delta
                    if nv > 0:
                        tb += nv
                        total += nv
        else:
            for i in range(left, right + 1):
                base = a[i]
                old = base + lz
                a[i] = base + delta
                if old > 0:
                    nv = old + delta
                    if nv > 0:
                        tb += delta
                        total += delta
                    else:
                        tb -= old
                        total -= old

        bsum[b] = tb
        dirty[b] = 1
        return total

    end_l = ends
    lazy_l = lazy
    bsum_l = bsum
    sblocks_l = sblocks
    prefs_l = prefs
    lens_l = lens
    dirty_l = dirty
    base_sum_l = base_sum
    rebuild_func = rebuild
    partial_func = partial_update

    out = []
    append = out.append

    for _ in range(Q):
        T = data[pos]
        l = data[pos + 1] - 1
        r = data[pos + 2] - 1
        X = data[pos + 3]
        pos += 4

        delta = X if T == 1 else -X

        bl = l >> SHIFT
        br = r >> SHIFT

        if bl == br:
            if l == (bl << SHIFT) and r == end_l[bl] - 1:
                fb = bl
                lb = bl
            else:
                total = partial_func(bl, l, r, delta, total)
                append(str(total))
                continue
        else:
            fb = bl
            lb = br

            if l != (bl << SHIFT):
                total = partial_func(bl, l, end_l[bl] - 1, delta, total)
                fb = bl + 1

            if r != end_l[br] - 1:
                total = partial_func(br, br << SHIFT, r, delta, total)
                lb = br - 1

        for b in range(fb, lb + 1):
            if dirty_l[b]:
                rebuild_func(b)

            old_sum = bsum_l[b]
            nlz = lazy_l[b] + delta
            lazy_l[b] = nlz

            sv = sblocks_l[b]
            m = lens_l[b]

            if sv[0] + nlz >= 0:
                new_sum = base_sum_l[b] + nlz * m
            elif sv[-1] + nlz <= 0:
                new_sum = 0
            else:
                idx = brgt(sv, -nlz)
                new_sum = base_sum_l[b] - prefs_l[b][idx] + nlz * (m - idx)

            bsum_l[b] = new_sum
            total += new_sum - old_sum

        append(str(total))

    sys.stdout.write("\n".join(out))

if __name__ == "__main__":
    main()

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

posted:
last update: