E - ストレスメーター / Stress Meter 解説 by admin
Claude 4.6 Opus (Thinking)概要
\(N\) 個のタスクの部分列 \([L, R]\) を処理したときのストレス値と限界回数を \(Q\) 回求める問題です。ストレス値 \(G\) が常に \(0\) 以上 \(K\) 未満に収まることを利用し、平方分割(sqrt decomposition) で高速に処理します。
考察
素朴なアプローチ
各クエリに対してタスクを \(L\) から \(R\) まで1つずつシミュレーションすると、1クエリあたり最悪 \(O(N)\) かかり、全体で \(O(NQ)\) です。\(N, Q \leq 6 \times 10^4\) なので最悪 \(3.6 \times 10^9\) となり TLE です。
重要な気づき:状態空間が小さい
ステップ 3 の処理に注目します。\(G \geq K\) ならリセットされるので、各タスクの処理後のストレス値 \(G\) は必ず \(0\) 以上 \(K-1\) 以下 です。\(K \leq 300\) なので、取りうる状態はたったの \(300\) 通りです。
解決のアイデア
タスク列をサイズ \(\sqrt{N}\) 程度のブロックに分割します。各ブロックについて、「開始時のストレス値が \(g\)(\(0 \leq g < K\))のとき、ブロック全体を処理した後のストレス値と限界回数はいくつか」 を事前に計算しておけば、ブロック単位で \(O(1)\) の遷移が可能になります。
アルゴリズム
前処理
- タスク列を \(\sqrt{N}\) 個ずつのブロックに分割する。
- 各ブロック \(b\) について、開始ストレス値 \(g = 0, 1, \ldots, K-1\) のそれぞれに対し、ブロック内の全タスクを順にシミュレーションし、以下を記録する:
block_next[b][g]:処理後のストレス値block_count[b][g]:限界に達した回数
クエリ処理
クエリ \([L, R]\) に対し、区間を3つに分ける:
[L, ... 左端ブロックの末尾] | [完全なブロック群] | [右端ブロックの先頭, ..., R]
- 左端の半端部分:タスクを1つずつシミュレーション(\(O(\sqrt{N})\))
- 中間の完全なブロック群:前処理の結果を使い、ブロックごとに \(O(1)\) で遷移(\(O(\sqrt{N})\) ブロック)
- 右端の半端部分:タスクを1つずつシミュレーション(\(O(\sqrt{N})\))
具体例
\(K = 5\), ブロックサイズ 3 で、あるブロックが「\(A = [3, 2, 4],\ B = [1, 0, 3]\)」のとき:
- 開始 \(g=0\):\(0 \xrightarrow{+3-1} 2 \xrightarrow{+2-0} 4 \xrightarrow{+4-3} 5 \geq 5\) → リセット → 最終 \(g=0\), \(c=1\)
- 開始 \(g=2\):\(2 \xrightarrow{+3-1} 4 \xrightarrow{+2-0} 6 \geq 5\) → リセット → \(0 \xrightarrow{+4-3} 1\) → 最終 \(g=1\), \(c=1\)
このように全 \(K\) 通りの開始状態を事前計算しておきます。
計算量
- 前処理の時間計算量: \(O(N \times K)\)
- ブロック数 \(\frac{N}{\sqrt{N}} = \sqrt{N}\) 個、各ブロックで \(K\) 通りの初期値 × ブロックサイズ \(\sqrt{N}\)
- \(\sqrt{N} \times K \times \sqrt{N} = NK\)
- クエリ処理の時間計算量: \(O(Q \times \sqrt{N})\)
- 全体の時間計算量: \(O(NK + Q\sqrt{N})\)
- \(N = 6 \times 10^4, K = 300\) のとき \(NK = 1.8 \times 10^7\)、\(Q\sqrt{N} \approx 1.5 \times 10^7\)
- 空間計算量: \(O(\sqrt{N} \times K + N)\)
実装のポイント
オーバーフロー対策:\(A_i\) が最大 \(10^9\) なので、\(G + A_i\) の計算時に
long longを使う必要があります。ただし、ステップ 3 の後は \(G < K \leq 300\) に収まるので、状態としてはintで十分です。ブロックサイズの選択:
sqrt(N)を使い、端数はブロック数の調整で吸収します。同一ブロック内のクエリ:\(L\) と \(R\) が同じブロックに属する場合は、前処理を使わず直接シミュレーションします。
0-indexed への変換:入力は 1-indexed なので、
L--; R--;で変換しています。ソースコード
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int N, K, Q;
cin >> N >> K >> Q;
vector<long long> A(N), B(N);
for (int i = 0; i < N; i++) cin >> A[i] >> B[i];
int block_size = max(1, (int)sqrt(N));
int num_blocks = (N + block_size - 1) / block_size;
vector<vector<int>> block_next(num_blocks, vector<int>(K));
vector<vector<int>> block_count(num_blocks, vector<int>(K));
for (int b = 0; b < num_blocks; b++) {
int start = b * block_size;
int end = min(N, start + block_size);
for (int g = 0; g < K; g++) {
int cur_g = g;
int cur_c = 0;
for (int i = start; i < end; i++) {
long long ng = (long long)cur_g + A[i];
ng = max(0LL, ng - B[i]);
if (ng >= K) {
cur_c++;
cur_g = 0;
} else {
cur_g = (int)ng;
}
}
block_next[b][g] = cur_g;
block_count[b][g] = cur_c;
}
}
for (int q = 0; q < Q; q++) {
int L, R;
cin >> L >> R;
L--; R--;
int cur_g = 0, cur_c = 0;
int lb = L / block_size;
int rb = R / block_size;
if (lb == rb) {
for (int i = L; i <= R; i++) {
long long ng = (long long)cur_g + A[i];
ng = max(0LL, ng - B[i]);
if (ng >= K) {
cur_c++;
cur_g = 0;
} else {
cur_g = (int)ng;
}
}
} else {
int left_end = (lb + 1) * block_size - 1;
for (int i = L; i <= left_end; i++) {
long long ng = (long long)cur_g + A[i];
ng = max(0LL, ng - B[i]);
if (ng >= K) {
cur_c++;
cur_g = 0;
} else {
cur_g = (int)ng;
}
}
for (int b = lb + 1; b < rb; b++) {
cur_c += block_count[b][cur_g];
cur_g = block_next[b][cur_g];
}
int right_start = rb * block_size;
for (int i = right_start; i <= R; i++) {
long long ng = (long long)cur_g + A[i];
ng = max(0LL, ng - B[i]);
if (ng >= K) {
cur_c++;
cur_g = 0;
} else {
cur_g = (int)ng;
}
}
}
cout << cur_c << " " << cur_g << "\n";
}
return 0;
}
この解説は claude4.6opus-thinking によって生成されました。
投稿日時:
最終更新: