D - 通路の扉と体力 / Corridor Doors and Hit Points 解説 by admin
gpt-5.3-codex概要
各時刻で「どの扉に何個の錠があるか」を数え、部屋間移動コストを扉重さの累積和で表す問題です。
累積和関数 \(F(x)\) を作れると、到達可能範囲は左右それぞれ二分探索で求められます。
考察
まず、質問 \((T,S,P)\) を固定して考えます。
時刻 \(T\) において、パターン \(i\) が錠をかける扉は
\[x \equiv B_i \pmod{A_i},\quad B_i=(R_i+V_iT)\bmod A_i\]
です。
扉 \(x\) の重さは「この合同式を満たすパターン数」です。
部屋 \(S\) から部屋 \(c\) への移動コストは、間にある扉重さの総和です。
ここで
\[F(x)=\sum_{d=1}^{x} w(d)\]
(\(w(d)\) は扉 \(d\) の重さ)とすると、
- \(c\ge S\) のときコスト \(=F(c)-F(S)\)
- \(c\le S\) のときコスト \(=F(S)-F(c)\)
となります。
つまり「到達可能な \(c\)」は、\(F\) が単調非減少であることから連続区間になります。
素朴解が厳しい理由
- 各扉の重さを全扉(最大 \(10^9\) 本近い)について作るのは不可能。
- 各質問ごとに全扉を走査するのも不可能。
重要な工夫
\(F(x)\) を直接 \(O(N)\) で計算します。
各パターン \(i\) について、\(1..x\) の中で
\[d \equiv B_i \pmod{A_i}\]
を満たす個数を数えればよいです。
- \(B_i=0\) のとき: 該当扉は \(A_i,2A_i,\dots\)
個数は \(\left\lfloor \dfrac{x}{A_i}\right\rfloor\) - \(B_i>0\) のとき: 最初は \(B_i\)、以降 \(A_i\) 間隔
\(x\ge B_i\) なら \(1+\left\lfloor \dfrac{x-B_i}{A_i}\right\rfloor\)、そうでなければ \(0\)
これを全パターンで足せば \(F(x)\)。
さらに、到達可能範囲は以下で求まります。
- 右端: 最大の \(c\in[S,L-1]\) で \(F(c)\le F(S)+P\)
- 左端: 最小の \(c\in[0,S]\) で \(F(c)\ge F(S)-P\)
どちらも二分探索可能です(\(F\) が単調なので)。
アルゴリズム
各質問ごとに:
- 各パターンの \(B_i=(R_i+V_iT)\bmod A_i\) を計算
- 関数
pref(x)=F(x)を \(O(N)\) で計算できるようにする - \(FS=F(S)\) を計算
- 二分探索で右端
rightMostを求める
条件:pref(mid) <= FS + P - 二分探索で左端
leftMostを求める
条件:pref(mid) >= FS - P - 答えは
rightMost - leftMost + 1
この方法は、コード中コメントの通り \(N\times Q\le 3\times 10^5\) を活かして、
「1質問あたり \(O(N\log L)\)」で十分間に合います。
計算量
- 時間計算量: \(O(Q \cdot N \cdot \log L)\)
- 空間計算量: \(O(N)\)(1質問中に使う配列)
実装のポイント
\(R_i+V_iT\) は最大で大きくなるため、
__int128で計算してから mod を取る。pref(x)は \(x=0\) のとき \(0\) を返すようにする(境界の安全性)。左右で二分探索の条件が異なる点に注意。
- 右は「条件を満たす最大値」
- 左は「条件を満たす最小値」
累積和の合計も大きくなりうるため、内部和を
__int128にしておくと安全です。ソースコード
#include <bits/stdc++.h>
using namespace std;
using int64 = long long;
using i128 = __int128_t;
struct Pattern {
int64 A, R, V;
};
struct Query {
int64 T, S, P;
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, Q;
int64 L;
cin >> N >> L >> Q;
vector<Pattern> pat(N);
for (int i = 0; i < N; i++) {
cin >> pat[i].A >> pat[i].R >> pat[i].V;
}
vector<Query> qs(Q);
for (int i = 0; i < Q; i++) {
cin >> qs[i].T >> qs[i].S >> qs[i].P;
}
// Since N*Q <= 3e5, we can process each query with all patterns.
// For each query, door weights are:
// w(x) = number of i such that x ≡ Bi (mod Ai), where Bi = (Ri + Vi*T) mod Ai.
// Need count of rooms c where sum of w on doors between S and c <= P.
// Define F(x)=sum_{d=1..x} w(d), x in [0..L-1], F(0)=0 over doors index.
// For c>=S: cost = F(c)-F(S)
// For c<=S: cost = F(S)-F(c)
//
// We need:
// rightmost c>=S with F(c) <= F(S)+P
// leftmost c<=S with F(c) >= F(S)-P (since F nondecreasing)
//
// F(x)=sum_i cnt_i(x), cnt_i(x)=# {d in [1..x] : d ≡ Bi mod Ai}
// Each cnt_i(x) can be computed O(1).
//
// Then binary search on c with O(N) predicate each -> O(N log L) per query.
// With N*Q<=3e5, and logL~30, this is acceptable in C++ with tight code.
auto floor_div = [](int64 a, int64 b)->int64{
// b>0, a>=0 in our use
return a / b;
};
for (int qi = 0; qi < Q; qi++) {
const auto &qu = qs[qi];
int64 T = qu.T, S = qu.S, P = qu.P;
vector<int64> A(N), B(N);
A.reserve(N); B.reserve(N);
for (int i = 0; i < N; i++) {
A[i] = pat[i].A;
i128 val = (i128)pat[i].V * (i128)T + (i128)pat[i].R;
int64 b = (int64)(val % pat[i].A);
if (b < 0) b += pat[i].A;
B[i] = b;
}
auto pref = [&](int64 x)->int64{
// sum over doors 1..x, x can be 0..L-1
if (x <= 0) return 0;
i128 sum = 0;
for (int i = 0; i < N; i++) {
int64 a = A[i], b = B[i];
int64 cnt = 0;
if (b == 0) {
// doors: a,2a,...
cnt = floor_div(x, a);
} else {
if (x >= b) cnt = 1 + floor_div(x - b, a);
else cnt = 0;
}
sum += cnt;
}
return (int64)sum;
};
int64 FS = pref(S);
// right bound
int64 low = S, high = L - 1;
int64 targetR = FS + P;
while (low < high) {
int64 mid = low + (high - low + 1) / 2;
if (pref(mid) <= targetR) low = mid;
else high = mid - 1;
}
int64 rightMost = low;
// left bound
low = 0; high = S;
int64 targetL = FS - P;
while (low < high) {
int64 mid = low + (high - low) / 2;
if (pref(mid) >= targetL) high = mid;
else low = mid + 1;
}
int64 leftMost = low;
cout << (rightMost - leftMost + 1) << '\n';
}
return 0;
}
この解説は gpt-5.3-codex によって生成されました。