Square Root Decomposition
제곱근 분할법이라 부르며 이름 그대로 어떤 작업을 $O(\sqrt{N})$ 에 하는 일이 잦은 알고리즘이다.
아이디어는 굉장히 단순한데, 활용법이 다양한 알고리즘 중 하나이다.
길이 $N$ 의 배열이 있다고 하자. 여기서 제곱근 분할법을 써서 구간합을 구하고 싶다.
다음과 같은 시간복잡도가 수반된다.
- 업데이트 $O(1)$
- 구간합 쿼리 $O(\sqrt{N})$
업데이트가 일어나는 상황이라면 세그먼트 트리로 $O(\log N)$ 에 구현이 가능하고 업데이트가 일어나지 않는 상황이라면 $O(1)$ 에 Sparse Table 로도 가능하지만, 이 알고리즘은 이것 말고도 다양한 쓰임새가 있다.
$M=\left\lfloor \sqrt{N} \right\rfloor$ 이라 하고, $N$ 개의 배열을 대략 $M$ 개의 그룹으로 나눈다.
그룹이라고도 하고 버킷(bucket)이라고도 흔히 부른다.
그리고 각 그룹의 원소들의 합을 구해둔다. $G_i$ 의 원소의 합을 $S_i$ 라 하자.
특정 구간 $[l, r]$ 의 구간합을 구해주고 싶다고 하자.
$[l, r]$ 에 속하는 $G_i$ 의 개수(노란 부분)는 $O(\sqrt{N})$ 이다. 왜냐면 총 $G_i$ 개수가 $O(\sqrt{N})$ 이하기 때문이다.
$l$ 이나 $r$이 속한 그룹을 $G_l,\,G_r$ 이라 할 때, $G_l,\,G_r$ 각 그룹에서 하나하나 세줘야 하는 원소의 개수(파란 부분)는 $O(\sqrt{N})$ 이다.
따라서 구간합 쿼리의 총 시간복잡도는 ${\color{salmon} O(\sqrt{N}) }$ 이다.
처음부터 $G_l=G_r$ 일 수도 있다.
인덱스 $i$의 값을 업데이트 한다고 하자. $i$ 가 속한 그룹 $G_i$ 의 원소들의 합만 업데이트 해주면 되므로 $O(1)$ 이다.
구현
대표적인 구간합 구하기 문제를 이걸로 풀어보자.
int n, m, k;
cin >> n >> m >> k;
vi a(n);
fv(a);
int sq = sqrt(n);
int B = (n + sq - 1) / sq;
vi bucket(B);
for (int i = 0; i < n; i++) {
bucket[i / sq] += a[i];
}
auto query = [&](int l, int r) {
int ret = 0;
int s = l / sq, e = r / sq;
for (int i = s + 1; i < e; i++) ret += bucket[i];
if (s != e) {
for (int i = l; i < (s + 1) * sq; i++) ret += a[i];
for (int i = r; i >= e * sq; i--) ret += a[i];
} else {
for (int i = l; i <= r; i++) ret += a[i];
}
return ret;
};
for (int i = 0; i < m + k; i++) {
int cmd, u, v;
cin >> cmd >> u >> v;
if (cmd == 1) {
u--;
bucket[u / sq] -= a[u];
a[u] = v;
bucket[u / sq] += a[u];
} else {
cout << query(u - 1, v - 1) << endl;
}
}
쿼리를 수행할 때, 우선 완전히 포함되는 bucket
들의 합은 그대로 쓰고, l
과 r
이 같은 경우와 다를 경우를 구분해서 세준다.
구현법은 가지각색이라 정답은 없다.
연습 문제
위에서 본 문제이다.
BOJ 2912
BOJ 2912 - 백설공주와 난쟁이
$\sqrt{N}$ 개의 bucket
으로 나누자.
$[l, r]$ 에 대한 쿼리가 들어오면, 그 안엔 최대 $\sqrt{N}$ 개의 버킷이 있다.
각 버킷에 대해서 해당 버킷에서 가장 색이 많은 숫자를 골라놓는다.
이 전처리는 $O(\sqrt{N} C)$ 정도에 가능하다.
만약 $[l, r]$ 구간에 정답인 색이 존재한다면
- 구간이 모두 포함된 버킷들의 각각 가장 많은 색 $O(\sqrt{N})$ 개, 혹은
- 구간에 포함되지 않은 부분 원소들 $O(\sqrt{N})$ 개
안에 정답이 존재한다.
어떤 색이 특정 구간안에 세는것은 이분탐색을 이용해 $O(\log N)$에 가능하다.
void solve() {
int N, M, C;
cin >> N >> C;
vi a(N);
fv(a);
vvi colors(10001);
for (int i = 0; i < N; i++) colors[a[i]].pb(i);
auto cnt = [&](int c, int l, int r) { return ubi(colors[c], r) - lbi(colors[c], l); };
int sq = sqrt(N);
int B = (N + sq - 1) / sq;
vi bucket(B);
auto get_most_color = [&](int l, int r) {
int ret = 0;
vi cnt(C + 1);
for (int i = l; i <= r; i++) {
cnt[a[i]]++;
if (cnt[a[i]] > cnt[ret]) ret = a[i];
}
return ret;
};
for (int i = 0; i < B; i++) {
bucket[i] = get_most_color(i * sq, min((i + 1) * sq - 1, N - 1));
}
cin >> M;
while (M--) {
int l, r;
cin >> l >> r;
l--, r--;
int len = r - l + 1;
vi cand;
int s = l / sq, e = r / sq;
if (s ^ e) {
for (int i = s + 1; i < e; i++) cand.pb(bucket[i]);
cand.pb(get_most_color(l, (s + 1) * sq - 1));
cand.pb(get_most_color(e * sq, r));
} else {
cand.pb(get_most_color(l, r));
}
uniq(cand);
for (int c: cand) {
if (cnt(c, l, r) * 2 > len) {
cout << "yes " << c << endl;
goto out;
}
}
cout << "no\n";
out:;
}
}
쿼리 캐싱, BOJ 12857
BOJ 12857 - 홍준이는 문자열을 좋아해
위 글을 보면, $O(\sqrt{N})$ 과 연관지어 시간복잡도의 제한을 분석하고 증명하여 푸는 문제의 예시를 볼 수 있다.
Comments