D - スタンプラリー / Stamp Rally Editorial by admin
Gemini 3.8 Flash (High)概要
\(1\) から \(7\) までの整数で表されるスタンプが並んだ長さ \(N\) の列から、全 \(7\) 種類すべてが含まれる連続区間 \([l, r]\) の総数を求める問題です。
考察
素朴なアプローチとその問題点
すべての区間の組 \((l, r)\) (\(1 \le l \le r \le N\)) を列挙して、その中に \(1\) から \(7\) がすべて含まれるかを確認する方法が考えられます。 しかし、区間の選び方だけでも \(\frac{N(N+1)}{2}\) 通り(\(O(N^2)\) 通り)存在します。本問では \(N \le 10^6\) であるため、区間の総数は約 \(5 \times 10^{11}\) にも達し、制限時間内に到底終わりません(TLE)。
右端 \(r\) を固定して考える
効率的に数え上げるために、区間の右端 \(r\) を \(1\) から \(N\) まで順に固定して考えます。
右端を \(r\) に固定したとき、左端 \(l\) (\(1 \le l \le r\)) として選べる条件を整理してみましょう。 区間 \([l, r]\) に種類 \(k\) (\(1 \le k \le 7\)) のスタンプが含まれるためには、「\(r\) 以前で種類 \(k\) が最後に現れた位置」が \(l\) 以上である必要があります。
したがって、すべての種類 \(1 \le k \le 7\) が区間 \([l, r]\) に含まれる条件は、各種類 \(k\) の最新の出現位置を \(P_k\) と置くと、 $\(l \le P_1 \text{ かつ } l \le P_2 \text{ かつ } \dots \text{ かつ } l \le P_7\)$ が成り立つことです。
これは、最新出現位置の最小値を \(M = \min(P_1, P_2, \dots, P_7)\) と置くと、 $\(1 \le l \le M\)$ と言い換えることができます。
つまり、右端が \(r\) のときに条件を満たす左端 \(l\) の個数は、ちょうど \(M\) 個になります。 なお、まだ一度も出現していないスタンプ種類がある場合は、その種類の最新位置を \(0\) とみなせば \(M = 0\) となり、条件を満たす \(l\) が \(0\) 個であることが自然に表現できます。
アルゴリズム
- 各スタンプ種類 \(1 \dots 7\) の直近の出現位置を保持する配列
last_posを用意し、初期値をすべて \(0\) とします。 - 答えの合計を保持する変数
ansを \(0\) で初期化します。 - \(r = 1, 2, \dots, N\) について、順番に以下の処理を行います:
- \(C_r\) の最新の出現位置を \(r\) に更新します(
last_pos[C_r] = r)。 - 全種類について直近の出現位置の最小値 \(M = \min_{1 \le i \le 7} \text{last\_pos}[i]\) を求めます。
ansに \(M\) を加算します。
- \(C_r\) の最新の出現位置を \(r\) に更新します(
- 最終的な
ansの値を出力します。
計算量
- 時間計算量: \(O(NK)\) (\(K\) はスタンプの種類数で、今回は \(K = 7\))
- \(r\) を \(1\) から \(N\) まで動かし、各ステップで要素数 \(7\) の配列の最小値を求めるため、ステップごとの計算量は \(O(K)\) です。全体では \(O(7N)\) となり、約 \(7 \times 10^6\) 回の計算で十分に高速に動作します。
- 空間計算量: \(O(K)\)
- 大きさ \(8\) の配列
last_posといくつかの変数のみを保持すればよいため、\(O(1)\) です。
- 大きさ \(8\) の配列
実装のポイント
オーバーフローへの注意: 条件を満たす区間の総数は、最大で約 \(\frac{N(N+1)}{2} \approx 5 \times 10^{11}\) になります。これは一般的な 32 bit 整数型(上限約 \(2 \times 10^9\))を超えてオーバーフローするため、答えを足し合わせる変数
ansは 64 bit 整数型(C++ のlong long型)を使用する必要があります。出現していない種類の扱い:
last_posの初期値を \(0\) にしておくことで、「全種類揃っていないときは最小値が \(0\) になり、答えに \(0\) が足される(=条件を満たす区間が存在しない)」という処理を、条件分岐を書かずにシンプルに扱うことができます。ソースコード
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<int> last_pos(8, 0);
long long ans = 0;
for (int r = 1; r <= n; ++r) {
int c;
cin >> c;
last_pos[c] = r;
int min_pos = r;
for (int i = 1; i <= 7; ++i) {
min_pos = min(min_pos, last_pos[i]);
}
ans += min_pos;
}
cout << ans << "\n";
return 0;
}
この解説は gemini-3.8-flash-high によって生成されました。