Official

D - 通路の扉と体力 / Corridor Doors and Hit Points Editorial by admin

gpt-5.5-xhigh

概要

各質問の時刻 \(T_j\) における扉の重さを固定し、部屋 \(S_j\) から体力 \(P_j\) 以内で到達できる部屋の個数を求めます。

扉を直接列挙すると \(L\) が最大 \(10^9\) なので不可能です。代わりに「左端から各部屋までにある扉の重さの累積和」を使い、到達可能な部屋の範囲を二分探索で求めます。

考察

時刻 \(T\) を固定して考えます。

警備パターン \(i\) が錠をかける扉は、

\[ x \equiv R_i + V_i T \pmod{A_i} \]

を満たす扉です。

ここで、

\[ r_i = (R_i + V_i T) \bmod A_i \]

とします。

扉番号は \(1\) 以上なので、最初に錠がかかる扉は次のようになります。

  • \(r_i \neq 0\) のとき:扉 \(r_i\)
  • \(r_i = 0\) のとき:扉 \(A_i\)

つまり、最初の扉を \(\mathrm{first}_i\) とすると、その後は

\[ \mathrm{first}_i,\ \mathrm{first}_i + A_i,\ \mathrm{first}_i + 2A_i,\ \dots \]

という等差数列になります。

例えば \(A_i = 3,\ r_i = 2\) なら、錠がかかる扉は

\[ 2, 5, 8, 11, \dots \]

です。


次に、関数 \(F(p)\) を次のように定義します。

\[ F(p) = \text{扉 }1\text{ から扉 }p\text{ までの重さの合計} \]

つまり、部屋 \(0\) から部屋 \(p\) まで移動するときに通る扉の重さの合計です。

パターン \(i\) が扉 \(1\) から \(p\) までに錠をかける個数は、

\[ \begin{cases} 0 & (p < \mathrm{first}_i) \\ \left\lfloor \dfrac{p - \mathrm{first}_i}{A_i} \right\rfloor + 1 & (p \geq \mathrm{first}_i) \end{cases} \]

です。

これを全パターンについて足し合わせれば \(F(p)\) が求められます。


部屋 \(S\) から部屋 \(c\) に移動するときの消費体力を考えます。

  • \(c > S\) のとき、通る扉は \(S+1, S+2, \dots, c\) なので、消費体力は

\[ F(c) - F(S) \]

  • \(c < S\) のとき、通る扉は \(c+1, c+2, \dots, S\) なので、消費体力は

\[ F(S) - F(c) \]

したがって、どちらの場合も消費体力は

\[ |F(c) - F(S)| \]

です。

よって、部屋 \(c\) に到達できる条件は

\[ |F(c) - F(S)| \leq P \]

です。

これは次の条件と同じです。

\[ F(S) - P \leq F(c) \leq F(S) + P \]

ここで \(F(c)\) は \(c\) が大きくなるほど減らない、つまり単調非減少です。

そのため、条件を満たす部屋 \(c\) は連続した区間になります。
この区間の左端と右端を二分探索で求めればよいです。

素朴に各部屋や各扉を調べると \(L\) が最大 \(10^9\) なので間に合いませんが、この方法なら \(L\) に対しては \(\log L\) だけで済みます。

アルゴリズム

各質問ごとに以下を行います。

  1. 時刻 \(T\) における各警備パターンの最初の扉 \(\mathrm{first}_i\) を求める。

$\( r_i = (R_i + V_i T) \bmod A_i \)$

として、

$\( \mathrm{first}_i = \begin{cases} A_i & (r_i = 0) \\ r_i & (r_i \neq 0) \end{cases} \)$

  1. 関数 \(F(p)\) を計算できるようにする。

コードでは prefix_sum(pos) が \(F(pos)\) を計算しています。

  1. 現在地 \(S\) に対して、

$\( center = F(S) \)$

とする。

  1. 到達可能な部屋 \(c\) は

$\( center - P \leq F(c) \leq center + P \)$

を満たす。

そこで、

  • \(F(c) \geq center - P\) を初めて満たす部屋を left
  • \(F(c) \leq center + P\) を最後に満たす部屋を right

として、それぞれ二分探索で求める。

  1. 答えは

$\( right - left + 1 \)$

である。

計算量

  • 時間計算量: \(O(NQ \log L)\)
  • 空間計算量: \(O(N)\)

各質問で、\(F(p)\) の計算に \(O(N)\) かかります。
二分探索は \(O(\log L)\) 回 \(F(p)\) を呼び出すため、1 質問あたり \(O(N \log L)\) です。

制約で \(N \times Q \leq 3 \times 10^5\) なので、十分高速に動作します。

実装のポイント

  • 扉番号は \(1\) から始まるため、剰余が \(0\) のときの最初の扉は \(0\) ではなく \(A_i\) です。
  first[i] = (r == 0 ? A[i] : r);
  • \(F(p)\) は単調非減少なので、二分探索が使えます。

  • \(V_i \times T\) は大きくなる可能性があるため、コードでは __int128 を使って安全に計算しています。

  long long r = (long long)(((__int128)V[i] * T + R[i]) % A[i]);
  • 累積和や体力は \(10^{14}\) 程度になるため、long long を使います。

    ソースコード

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int N, L, Q;
    cin >> N >> L >> Q;

    vector<long long> A(N), R(N), V(N);
    for (int i = 0; i < N; i++) {
        cin >> A[i] >> R[i] >> V[i];
    }

    vector<long long> first(N);

    auto prefix_sum = [&](int pos) -> long long {
        long long res = 0;
        long long p = pos;
        for (int i = 0; i < N; i++) {
            if (first[i] <= p) {
                res += (p - first[i]) / A[i] + 1;
            }
        }
        return res;
    };

    while (Q--) {
        long long T, P;
        int S;
        cin >> T >> S >> P;

        for (int i = 0; i < N; i++) {
            long long r = (long long)(((__int128)V[i] * T + R[i]) % A[i]);
            first[i] = (r == 0 ? A[i] : r);
        }

        long long center = prefix_sum(S);
        long long low = center - P;
        long long high = center + P;

        int l = 0, r = L - 1;
        while (l < r) {
            int m = (l + r) / 2;
            if (prefix_sum(m) >= low) r = m;
            else l = m + 1;
        }
        int left = l;

        l = 0;
        r = L - 1;
        while (l < r) {
            int m = (l + r + 1) / 2;
            if (prefix_sum(m) <= high) l = m;
            else r = m - 1;
        }
        int right = l;

        cout << right - left + 1 << '\n';
    }

    return 0;
}

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