公式

E - 会社経営シミュレーション / Company Management Simulation 解説 by admin

gpt-5.5-high

概要

各日の資金増減 \(A_i-B_i-C_i\) が周期 \(N\) で無限に繰り返されます。
各質問について、指定された日から始めたときに、累積資金が初めて負になる日を高速に求めます。

考察

まず、各日の資金増減を

\[ D_i = A_i - B_i - C_i \]

とします。

質問ごとに実際に 1 日ずつシミュレーションすると、倒産まで非常に長い日数がかかる可能性があります。
また、\(Q\) 個の質問それぞれで最大 \(N\) 日ぶん調べるだけでも、最悪で \(O(NQ)\) となり、\(N+Q \leq 2 \times 10^5\) では間に合いません。

重要なのは、経営計画は周期 \(N\) で繰り返されることです。

開始日を \(L\)、0-indexed で \(s=L-1\) とします。
開始から \(k\) 日後までの資金増加量は、循環列上で

\[ D_s + D_{s+1} + \cdots + D_{s+k-1} \]

です。

これを高速に求めるために、配列 \(D\) を 2 回並べたものの累積和を作ります。

累積和を \(P\) とすると、開始日 \(s\) から \(k\) 日後までの増加量は

\[ P_{s+k} - P_s \]

で表せます。


まず、開始から最初の \(N\) 日間、つまり 1 周分だけを考えます。

その 1 周の中で最も資金が少なくなる増加量を

\[ \min_{1 \leq k \leq N} (P_{s+k} - P_s) \]

とします。

この値を \(\mathrm{mn}\) とします。

初期資金が \(S\) なら、1 周目の途中での最低資金は

\[ S + \mathrm{mn} \]

です。

  • もし \(S+\mathrm{mn}<0\) なら、倒産は 1 周目の中で起こります。
  • そうでない場合、1 周目では倒産しません。

次に、1 周全体の資金増加量を

\[ T = D_1 + D_2 + \cdots + D_N \]

とします。

1 周目で倒産しない場合、

  • \(T \geq 0\) なら、周回を重ねても開始時の資金は減らないので、今後も倒産しません。
  • \(T < 0\) なら、1 周するたびに資金が \(-T\) 円ずつ減るので、いつか倒産します。

倒産する具体的な日を求めるには、「ある範囲の累積和の中で、初めてある値未満になる位置」を探す必要があります。

累積和列は単調とは限らないため、二分探索は使えません。
そこで、累積和列に対して区間最小値を持つセグメント木を作り、

  • 区間最小値を求める
  • 区間内で初めて指定値未満になる位置を探す

ことを高速に行います。

アルゴリズム

1. 前処理

各日の増減を

\[ D_i = A_i - B_i - C_i \]

として配列に保存します。

次に、\(D\) を 2 回繰り返した長さ \(2N\) の列について累積和 \(P\) を作ります。

\[ P_0 = 0 \]

\[ P_{i+1} = P_i + D_{i \bmod N} \]

とします。

これにより、任意の開始位置 \(s\) から最大 \(N\) 日分の累積増加量を、連続区間として扱えます。

また、1 周全体の増加量を

\[ T = P_N \]

とします。

累積和 \(P\) に対して、区間最小値を管理するセグメント木を構築します。


2. 各質問の処理

質問で開始日 \(L\)、初期資金 \(S\) が与えられたとします。
0-indexed の開始位置を

\[ s = L - 1 \]

とします。

1 周分に対応する累積和の範囲は

\[ P_{s+1}, P_{s+2}, \ldots, P_{s+N} \]

です。

コード上では半開区間で

\[ [s+1, s+N+1) \]

を見ます。

この範囲の最小値をセグメント木で求めます。

\[ \mathrm{mn} = \min(P_{s+1}, \ldots, P_{s+N}) - P_s \]

これは「開始から 1 周以内での資金増加量の最小値」です。


3. 1 周目で倒産する場合

もし

\[ S + \mathrm{mn} < 0 \]

なら、1 周目のどこかで倒産します。

倒産条件は、ある \(k\) について

\[ S + (P_{s+k} - P_s) < 0 \]

です。

変形すると、

\[ P_{s+k} < P_s - S \]

です。

したがって、区間 \([s+1, s+N+1)\) の中で初めて

\[ P_i < P_s - S \]

となる位置 \(i\) を探します。

答えは

\[ i - s \]

日目です。


4. 1 周目で倒産しない場合

1 周目で倒産しないなら、次に \(T\) を見ます。

\(T \geq 0\) の場合

1 周するたびに資金は減らないため、以後も倒産しません。

答えは 0 です。

\(T < 0\) の場合

1 周するたびに資金が \(d=-T\) だけ減ります。

\(c\) 周終わった後の資金は

\[ S - cd \]

です。

その周の中での最低資金は

\[ S - cd + \mathrm{mn} \]

です。

これが初めて負になる最小の \(c\) を求めます。

\[ S - cd + \mathrm{mn} < 0 \]

\[ cd > S + \mathrm{mn} \]

なので、

\[ c = \left\lfloor \frac{S+\mathrm{mn}}{d} \right\rfloor + 1 \]

です。

つまり、\(c\) 周分は丸ごと進めたあと、その次の周の中で倒産します。

その時点での資金は

\[ S + cT \]

です。

この資金を \(\mathrm{capital}\) とすると、倒産条件は

\[ \mathrm{capital} + (P_i - P_s) < 0 \]

すなわち

\[ P_i < P_s - \mathrm{capital} \]

です。

再びセグメント木を使って、区間 \([s+1, s+N+1)\) の中で初めてこの条件を満たす位置 \(i\) を探します。

答えは

\[ cN + (i-s) \]

です。

計算量

  • 時間計算量: \(O((N+Q)\log N)\)
  • 空間計算量: \(O(N)\)

セグメント木の構築に \(O(N)\)、各質問で区間最小値取得と「初めて閾値未満になる位置」の探索を行うため \(O(\log N)\) です。

実装のポイント

倒産条件は「資金が \(0\) 円未満」なので、判定は <= ではなく < です。

例えば、資金がちょうど \(0\) 円になっても倒産ではありません。

そのため、位置を探す条件も

\[ P_i < P_s - S \]

のように、厳密な不等号になります。

また、累積和や答えの日数は大きくなる可能性があります。
Python では整数が任意精度なので問題ありませんが、C++ などで実装する場合は long long を使う必要があります。

ソースコード

import sys

def main():
    data = list(map(int, sys.stdin.buffer.read().split()))
    it = iter(data)

    N = next(it)
    Q = next(it)

    diff = [0] * N
    for i in range(N):
        a = next(it)
        b = next(it)
        c = next(it)
        diff[i] = a - b - c

    M = 2 * N + 1
    pref = [0] * M
    for i in range(2 * N):
        pref[i + 1] = pref[i] + diff[i % N]

    total = pref[N]

    INF = 10**30
    size = 1
    while size < M:
        size <<= 1

    seg = [INF] * (2 * size)
    seg[size:size + M] = pref

    for i in range(size - 1, 0, -1):
        v1 = seg[i << 1]
        v2 = seg[i << 1 | 1]
        seg[i] = v1 if v1 < v2 else v2

    def range_min(l, r):
        l += size
        r += size
        res = INF
        sg = seg
        while l < r:
            if l & 1:
                if sg[l] < res:
                    res = sg[l]
                l += 1
            if r & 1:
                r -= 1
                if sg[r] < res:
                    res = sg[r]
            l >>= 1
            r >>= 1
        return res

    def first_less(l, r, x):
        l += size
        r += size
        left = []
        right = []
        sg = seg

        while l < r:
            if l & 1:
                left.append(l)
                l += 1
            if r & 1:
                r -= 1
                right.append(r)
            l >>= 1
            r >>= 1

        for node in left:
            if sg[node] < x:
                while node < size:
                    lc = node << 1
                    if sg[lc] < x:
                        node = lc
                    else:
                        node = lc | 1
                return node - size

        for node in reversed(right):
            if sg[node] < x:
                while node < size:
                    lc = node << 1
                    if sg[lc] < x:
                        node = lc
                    else:
                        node = lc | 1
                return node - size

        return -1

    ans = []
    for _ in range(Q):
        L = next(it)
        S = next(it)

        s = L - 1
        base = pref[s]
        l = s + 1
        r = s + N + 1

        mn = range_min(l, r) - base

        if S + mn < 0:
            threshold = base - S
            idx = first_less(l, r, threshold)
            ans.append(str(idx - s))
        else:
            if total >= 0:
                ans.append("0")
            else:
                d = -total
                cycles = (S + mn) // d + 1
                capital = S + cycles * total
                threshold = base - capital
                idx = first_less(l, r, threshold)
                ans.append(str(cycles * N + (idx - s)))

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

if __name__ == "__main__":
    main()

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

投稿日時:
最終更新: