E - ストレスメーター / Stress Meter Editorial by admin
gpt-5.5-xhigh概要
各質問について、区間 \([L,R]\) のタスクだけを \(G=0, C=0\) から処理したときの、限界到達回数 \(C\) と最終ストレス値 \(G\) を求めます。
ポイントは、\(K \leq 300\) と小さいため、「ある区間を処理すると、入力されたストレス値 \(G\) がどう変化するか」を全状態について前計算できることです。
考察
まず、タスク \(i\) の処理を見直します。
タスク処理では、
- \(G \leftarrow G + A_i\)
- \(G \leftarrow \max(0, G - B_i)\)
- \(G \geq K\) なら \(C\) を \(1\) 増やし、\(G \leftarrow 0\)
を行います。
ステップ 1, 2 をまとめると、
\[ G \leftarrow \max(0, G + A_i - B_i) \]
です。
したがって、タスク \(i\) の影響は \(A_i\) と \(B_i\) を個別に見る必要はなく、
\[ D_i = A_i - B_i \]
だけで決まります。
現在のストレス値を \(g\) とすると、タスク \(i\) 後は次のようになります。
- \(g + D_i \geq K\) なら、限界に達して \(C\) が \(1\) 増え、\(g=0\)
- \(g + D_i \leq 0\) なら、\(g=0\)
- それ以外なら、\(g \leftarrow g + D_i\)
ここで重要なのは、各タスクの処理後、必ず
\[ 0 \leq G < K \]
が成り立つことです。
なぜなら、\(G \geq K\) になった場合はすぐに \(0\) に戻されるからです。
つまり、各区間の処理は、入力 \(G \in \{0,1,\dots,K-1\}\) に対して、
- 限界到達回数がいくつ増えるか
- 最終的な \(G\) がいくつになるか
という「関数」として表せます。
素朴に各質問ごとに \(L\) から \(R\) までシミュレーションすると、最悪で
\[ O(NQ) \]
となります。
制約は \(N,Q \leq 6 \times 10^4\) なので、これは間に合いません。
そこで、区間ごとの「変化の関数」を前計算し、質問ではそれらを合成して高速に答えます。
アルゴリズム
1. 各タスクの差分を持つ
各タスクについて、
\[ D_i = A_i - B_i \]
を計算しておきます。
1 個のタスクを適用する処理は次のようになります。
x = g + D_i;
if (x >= K) {
c++;
g = 0;
} else if (x <= 0) {
g = 0;
} else {
g = x;
}
2. ブロックに分ける
タスク列を長さ \(16\) 程度の小さなブロックに分けます。
各ブロックについて、すべての初期値
\[ g = 0,1,\dots,K-1 \]
に対して、そのブロック全体を処理した結果を前計算します。
具体的には、ブロック \(b\) について、
nxt[b][g]: 初期ストレス値が \(g\) のとき、ブロック処理後のストレス値add[b][g]: 初期ストレス値が \(g\) のとき、ブロック内で増える限界到達回数
を持ちます。
3. ブロック列の上にセグメント木を作る
ブロックを葉とするセグメント木を作ります。
各ノードは、対応する連続したブロック列全体の変換を持ちます。
あるノードが左部分 \(L\) と右部分 \(R\) からなるとします。
初期値を \(g\) とすると、
- 左部分を処理する
- 限界到達回数が
add[L][g]増える - ストレス値が
nxt[L][g]になる
- 限界到達回数が
- その結果を入力として右部分を処理する
したがって、合成は次のようになります。
\[ mid = nxt_L[g] \]
\[ add[g] = add_L[g] + add_R[mid] \]
\[ nxt[g] = nxt_R[mid] \]
このようにして、セグメント木の各ノードに対して、全 \(g\) について値を計算します。
4. 質問に答える
質問区間 \([L,R]\) に対して、まず \(L,R\) が属するブロックを見ます。
- 左端ブロックの途中部分
- 右端ブロックの途中部分
は、要素数が高々ブロックサイズなので、直接シミュレーションします。
その間に完全に含まれるブロック列については、セグメント木を使ってまとめて適用します。
注意点として、区間の処理は順番が重要です。
関数の合成は一般に交換できないため、セグメント木で得られたノードを必ず左から右の順に適用する必要があります。
コードでは、右側から出てきたノードを一旦配列に保存し、最後に逆順で適用することで、全体として左から右の順序を保っています。
計算量
ブロックサイズを \(B\) とします。コードでは \(B=16\) です。
前計算:
- 各ブロックについて \(K\) 通りの初期値をシミュレーションするので \(O(NK)\)
- セグメント木の構築に \(O\left(\frac{N}{B}K\right)\)
各質問:
- 両端の端数部分を直接処理して \(O(B)\)
- セグメント木のノードをたどって \(O\left(\log \frac{N}{B}\right)\)
したがって全体では、
- 時間計算量: \(O\left(NK + Q\left(B + \log \frac{N}{B}\right)\right)\)
- 空間計算量: \(O\left(\frac{N}{B}K\right)\)
\(B=16\) は定数なので、おおよそ
- 時間計算量: \(O(NK + Q\log N)\)
- 空間計算量: \(O(NK/B)\)
と見なせます。
実装のポイント
各タスクは \(A_i-B_i\) だけを使えば十分です。
各タスク処理後は必ず \(0 \leq G < K\) になるため、状態数は \(K\) 個だけです。
nxtには最終的な \(G\)、addには増加する限界到達回数を保存します。セグメント木のノードを合成するときは、左を処理した後のストレス値を右の入力にします。
区間クエリでは、処理順序を必ず左から右に保つ必要があります。
\(K \leq 300\)、限界到達回数は最大でも \(N \leq 60000\) なので、コードでは
uint16_tに収まります。ソースコード
#include <bits/stdc++.h>
using namespace std;
static inline void apply_delta(int d, int K, int &g, int &c) {
int x = g + d;
if (x >= K) {
++c;
g = 0;
} else if (x <= 0) {
g = 0;
} else {
g = x;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, K, Q;
cin >> N >> K >> Q;
vector<int> D(N);
for (int i = 0; i < N; ++i) {
int A, B;
cin >> A >> B;
D[i] = A - B;
}
constexpr int BS = 16;
int nb = (N + BS - 1) / BS;
int SZ = 1;
while (SZ < nb) SZ <<= 1;
int nodes = 2 * SZ;
vector<uint16_t> nxt((size_t)nodes * K);
vector<uint16_t> add((size_t)nodes * K, 0);
for (int b = 0; b < SZ; ++b) {
size_t base = (size_t)(SZ + b) * K;
for (int g = 0; g < K; ++g) {
nxt[base + g] = (uint16_t)g;
}
}
for (int b = 0; b < nb; ++b) {
int s = b * BS;
int e = min(N, s + BS);
size_t base = (size_t)(SZ + b) * K;
for (int g0 = 0; g0 < K; ++g0) {
int g = g0, c = 0;
for (int i = s; i < e; ++i) {
apply_delta(D[i], K, g, c);
}
nxt[base + g0] = (uint16_t)g;
add[base + g0] = (uint16_t)c;
}
}
for (int v = SZ - 1; v >= 1; --v) {
size_t base = (size_t)v * K;
size_t lb = (size_t)(v << 1) * K;
size_t rb = (size_t)((v << 1) | 1) * K;
for (int g0 = 0; g0 < K; ++g0) {
int mid = nxt[lb + g0];
add[base + g0] = (uint16_t)(add[lb + g0] + add[rb + mid]);
nxt[base + g0] = nxt[rb + mid];
}
}
auto apply_node = [&](int v, int &g, int &c) {
size_t idx = (size_t)v * K + g;
c += add[idx];
g = nxt[idx];
};
while (Q--) {
int L, R;
cin >> L >> R;
--L;
--R;
int g = 0, c = 0;
int bl = L / BS;
int br = R / BS;
if (bl == br) {
for (int i = L; i <= R; ++i) {
apply_delta(D[i], K, g, c);
}
} else {
int left_end = min(N, (bl + 1) * BS);
for (int i = L; i < left_end; ++i) {
apply_delta(D[i], K, g, c);
}
int ql = bl + 1;
int qr = br;
if (ql < qr) {
int l = ql + SZ;
int r = qr + SZ;
int rights[64];
int rc = 0;
while (l < r) {
if (l & 1) apply_node(l++, g, c);
if (r & 1) rights[rc++] = --r;
l >>= 1;
r >>= 1;
}
while (rc > 0) {
apply_node(rights[--rc], g, c);
}
}
int right_start = br * BS;
for (int i = right_start; i <= R; ++i) {
apply_delta(D[i], K, g, c);
}
}
cout << c << ' ' << g << '\n';
}
return 0;
}
この解説は gpt-5.5-xhigh によって生成されました。
posted:
last update: