이 문제는 P1198 [JSOI2008] 최댓값 문제로, 주어진 연산에 따라 배열의 끝에 값을 추가하거나 특정 구간의 최댓값을 출력하는 문제입니다. 다양한 접근법이 존재하며, 대표적으로 ST 테이블, 모노토닉 스택, 그리고 유니온-파인드(Union-Find)를 활용한 최적화 방법이 있습니다. 각 방법의 시간 복잡도와 구현 방식을 살펴보겠습니다.
방법 1: 역방향 ST 테이블
ST 테이블은 일반적으로 정적 배열에서 구간 최댓값을 O(1)에 처리하지만, 여기서는 배열 끝에 새로운 요소가 추가되는 동적 상황을 다룹니다. 역방향 ST 테이블을 사용하면 삽입 시 O(log n)의 시간으로 테이블을 갱신할 수 있습니다. 아래 코드에서는 새로운 원소가 추가될 때마다 그 위치를 기준으로 로그 구간을 갱신합니다.
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
int st[N][21], lg[N], arr[N];
int lastAns;
void update(int pos) {
st[pos][0] = arr[pos];
for (int j = 1; pos - (1 << j) >= 0; j++) {
st[pos][j] = max(st[pos][j-1], st[pos - (1 << (j-1))][j-1]);
}
}
long long query(int l, int r) {
int k = lg[r - l + 1];
return max(st[r][k], st[l + (1 << k) - 1][k]);
}
int main() {
ios::sync_with_stdio(false); cin.tie(0);
int n, mod;
cin >> n >> mod;
for (int i = 2; i <= n; i++) lg[i] = lg[i >> 1] + 1;
int len = 0;
for (int i = 0; i < n; i++) {
char op; cin >> op;
if (op == 'A') {
int x; cin >> x;
x = (x + lastAns) % mod;
arr[++len] = x;
update(len);
} else {
int k; cin >> k;
if (k == 1) {
lastAns = arr[len];
cout << arr[len] << '\n';
} else {
lastAns = query(len - k + 1, len);
cout << lastAns << '\n';
}
}
}
return 0;
}
방법 2: 모노토닉 스택 + 이진 탐색
여기서 중요한 관찰은 문제의 쿼리가 항상 배열의 끝에서부터의 구간을 대상으로 한다는 점입니다. 따라서 새로 추가된 값이 기존의 어떤 값보다 크다면, 그 기존 값은 이후 쿼리에서 절대 최댓값이 될 수 없습니다. 이 성질을 이용해 감소하는 모노토닉 스택을 유지하면, 각 원소는 스택 내에서 내림차순으로 정렬된 위치 인덱스를 저장합니다. 쿼리 시에는 lower_bound로 스택에서 구간 시작 위치보다 큰 첫 번째 인덱스를 찾아 그 값을 사용합니다.
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
int stk[N], top, arr[N], lastAns;
int main() {
ios::sync_with_stdio(false); cin.tie(0);
int n, mod;
cin >> n >> mod;
int len = 0;
top = 0;
for (int i = 0; i < n; i++) {
char op; cin >> op;
if (op == 'A') {
int x; cin >> x;
x = (x + lastAns) % mod;
arr[++len] = x;
while (top && arr[len] > arr[stk[top]]) top--;
stk[++top] = len;
} else {
int l; cin >> l;
int pos = lower_bound(stk + 1, stk + top + 1, len - l + 1) - stk;
lastAns = arr[stk[pos]];
cout << lastAns << '\n';
}
}
return 0;
}
방법 3: 모노토닉 스택 + 유니온-파인드 최적화
이 방법은 쿼리 시 이진 탐색 대신 유니온-파인드를 사용하여 O(α(n))에 가깝게 만듭니다. 핵심 아이디어는 다음과 같습니다. 모노토닉 스택에서 어떤 원소가 pop될 때, 그 원소는 자신을 pop시킨 새 원소보다 작으므로, 이후 쿼리에서 pop된 원소의 인덱스 위치는 새 원소의 값을 대표로 삼아야 합니다. 따라서 pop되는 시점에 두 인덱스를 유니온하고, 부모를 새 원소로 설정합니다. 쿼리 시에는 찾고자 하는 위치의 대표 원소(find)의 값을 바로 반환합니다.
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
int stk[N], top, parent[N], arr[N], lastAns;
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
int main() {
ios::sync_with_stdio(false); cin.tie(0);
int n, mod;
cin >> n >> mod;
int len = 0;
top = 0;
for (int i = 0; i < n; i++) {
char op; cin >> op;
if (op == 'A') {
int x; cin >> x;
x = (x + lastAns) % mod;
arr[++len] = x;
parent[len] = len;
while (top && arr[len] > arr[stk[top]]) {
parent[find(stk[top])] = len;
top--;
}
stk[++top] = len;
} else {
int l; cin >> l;
int pos = find(len - l + 1);
lastAns = arr[pos];
cout << lastAns << '\n';
}
}
return 0;
}
이 유니온-파인드 기법은 각 원소가 스택에서 한 번씩만 push/pop되므로 거의 선형 시간에 동작합니다. 실제로 기존 이진 탐색 방식보다 약 100ms 가량의 속도 향상을 관찰할 수 있습니다.