본문 바로가기

알고리즘/기타

Mo's

모스 알고리즘(Mo's Algorithm)은 업데이트가 없는 복잡한 구간 쿼리를 처리하는 알고리즘입니다.

 

시작하기 전, 선행지식으로 sqrt Decomposition과 오프라인 쿼리에 대해 알고 있어야 합니다.

sqrt Decomposition은 이에 대해 설명한 제 글이 있습니다.

https://anz1217.tistory.com/127

 

MITM, sqrt Decomposition

Meet in the middle (MITM)에 대해 알아봅시다. https://www.acmicpc.net/problem/1208 1208번: 부분수열의 합 2 첫째 줄에 정수의 개수를 나타내는 N과 정수 S가 주어진다. (1 ≤ N ≤ 40, |S| ≤ 1,000,000) 둘..

anz1217.tistory.com

 

오프라인 쿼리는 생각보다 간단하니 이 글에 바로 설명합니다.

쿼리 문제를 풀 때, 대부분의 경우에 우리는 쿼리가 들어오자마자 처리하고 결과를 출력합니다.

하지만 잘 생각해보면, 쿼리를 굳이 입력이 들어오는 즉시 처리할 필요는 없습니다. 입출력 스트림은 별개로 작동하므로 모든 쿼리를 한 번에 다 받은 다음, 원하는 순서로 쿼리들을 처리하여 답을 한꺼번에 구하고, 원래 쿼리가 들어온 순서대로 답을 출력하는 방식으로도 문제를 풀 수 있기 때문입니다.

전자가 온라인 쿼리, 후자가 오프라인 쿼리입니다.

 

예를 들어 다음과 같은 문제를 푼다고 하면,

https://www.acmicpc.net/problem/16978

 

16978번: 수열과 쿼리 22

길이가 N인 수열 A1, A2, ..., AN이 주어진다. 이때, 다음 쿼리를 수행하는 프로그램을 작성하시오. 1 i v: Ai = v로 변경한다. 2 k i j: k번째 1번 쿼리까지 적용되었을 때, Ai, Ai+1, ..., Aj의 합을 출력한다.

www.acmicpc.net

 

쿼리를 한꺼번에 받은 다음 2번 쿼리를 \(k\)가 작은 순으로 정렬하여 풀어나가면 더 쉽게 문제를 풀 수 있습니다.

각 2번 쿼리가 들어온 순서를 저장한 다음, 마지막에 그 순서대로 답을 출력하면 됩니다.

 

풀이 코드는 생략합니다.


https://www.acmicpc.net/problem/11659

 

11659번: 구간 합 구하기 4

첫째 줄에 수의 개수 N과 합을 구해야 하는 횟수 M이 주어진다. 둘째 줄에는 N개의 수가 주어진다. 수는 1,000보다 작거나 같은 자연수이다. 셋째 줄부터 M개의 줄에는 합을 구해야 하는 구간 i와 j

www.acmicpc.net

 

 

물론 우리는 누적 합을 구해둔 다음 구간 합을 상수 시간에 계산하면 된다는 사실을 잘 알고 있지만, 연습을 위해 모스 알고리즘으로 문제를 풀어봅시다.

 

기본적으로 모스 알고리즘은 투 포인터와 비슷하게 작동합니다.

쿼리의 구간을 나타내는 두 개의 포인터를 유지해 나간다면, 다음 쿼리를 계산할 때 이전 쿼리의 결과를 어느정도 재활용하며 계산할 수 있습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <bits/stdc++.h>
using namespace std;
 
typedef long long ll;
typedef pair<intint> pii;
typedef pair<ll, ll> pll;
 
ll gcd(ll a, ll b) { for (; b; a %= b, swap(a, b)); return a; }
 
int n, m;
int a[100001];
 
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
 
    cin >> n >> m;
    for (int i = 1; i <= n; i++cin >> a[i];
    
    int lp = 1, rp = 0// 구간 [l, r] 을 나타내는 두 포인터
    int sum = 0;
 
    while (m--)
    {
        int l, r; cin >> l >> r;
        while (l < lp) sum += a[--lp];
        while (l > lp) sum -= a[lp++];
        while (rp < r) sum += a[++rp];
        while (rp > r) sum -= a[rp--];
 
        cout << sum << '\n';
    }
}
cs

 

하지만 이 방식으로 문제를 풀더라도 온라인으로 문제를 푼다면 최악의 경우 두 포인터가 각 쿼리마다 \(O(N)\)번씩 이동하기 때문에, 총 시간 복잡도 \(O(NM)\)로 너무 느립니다.

그런데 오프라인으로 문제를 푼다면 어떨까요? 이 문제 풀이 방식은 그대로 두고, 각 쿼리를 계산하는 순서만 적절히 바꿈으로써 시간복잡도를 개선할 수 있지 않을까요?

 

놀랍게도 가능합니다!

\(N\)개의 원소를 sqrt Decomposition에서 했던 것과 같이, \(\sqrt N\)개의 버킷으로 분할합시다.

이제 각 쿼리 (\([l, r]\))를 다음과 같이 정렬하여 풀면 됩니다.

1. \(l\)의 버킷 번호가 작은 순

2. \(l\)의 버킷 번호가 같다면, \(r\)값 자체가 작은 순

 

시간복잡도를 계산해 봅시다.

\(l\)의 버킷 번호가 작은 순으로 정렬했으므로, 왼쪽 포인터는 현재 쿼리에서 다음 쿼리를 계산하는데에 최대 \(O(\sqrt N)\)번만 움직이게 됩니다. 총 쿼리가 \(Q\)개라면, 왼쪽 포인터가 움직이는 횟수는 총 \(O(Q\sqrt N)\)회 입니다.

\(r\)은 버킷 번호가 같다면 비내림차순으로 정렬했습니다. 따라서 한 버킷에 대해 오른쪽 포인터는 \(O(N)\)번 움직이게 됩니다. 버킷이 총 \(\sqrt N\)개 이므로, 오른쪽 포인터가 움직이는 횟수는 총 \(O(N\sqrt N)\)회입니다.

 

따라서 총 \(O((N+Q)\sqrt N)\)의 시간복잡도로 문제를 풀어낼 수 있게 됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <bits/stdc++.h>
using namespace std;
 
typedef long long ll;
typedef pair<intint> pii;
typedef pair<ll, ll> pll;
 
ll gcd(ll a, ll b) { for (; b; a %= b, swap(a, b)); return a; }
 
const int BK_SIZE = 320;
 
struct Query
{
    int l, r;
    int idx;
};
 
bool comp(Query a, Query b)
{
    int al = a.l / BK_SIZE;
    int bl = b.l / BK_SIZE;
 
    if (al != bl) return al < bl;
    return a.r < b.r;
}
 
int n, m;
int a[100001];
vector <Query> q;
int ans[100001];
 
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
 
    cin >> n >> m;
    for (int i = 1; i <= n; i++cin >> a[i];
    for (int i = 0; i < m; i++)
    {
        int l, r; cin >> l >> r;
        q.push_back({ l,r,i });
    }
 
    sort(q.begin(), q.end(), comp);
 
    int lp = 1, rp = 0// 구간 [l, r] 을 나타내는 두 포인터
    int sum = 0;
 
    for (int i = 0; i < m; i++)
    {
        int l = q[i].l, r = q[i].r;
        int idx = q[i].idx;
 
        while (l < lp) sum += a[--lp];
        while (l > lp) sum -= a[lp++];
        while (rp < r) sum += a[++rp];
        while (rp > r) sum -= a[rp--];
 
        ans[idx] = sum;
    }
 
    for (int i = 0; i < m; i++cout << ans[i] << '\n';
}
cs

제 그룹의 문제집에서 연습 문제들을 관리하고 있습니다.
문제집의 문제들을 보고 싶으시다면, 가입 신청을 해 주세요.

 

+ 요즘 많이 가입 신청이 들어오고 있습니다만, 꼭 그룹에 가입하셔서 제 문제들을 푸실 필요는 없습니다.

solved.ac 에서 태그를 찾아 푸시거나 백준 단계별로 풀기 에서 문제를 찾아 푸셔도 충분합니다!

그룹은 단순히 제 개인용 알고리즘 문제집 정리 용도이니 참고해주세요!


https://www.acmicpc.net/group/7712

 

ANZ1217

무슨 내용을 넣어야 좋을까요?

www.acmicpc.net

 

'알고리즘 > 기타' 카테고리의 다른 글

스위핑  (0) 2021.07.01
MITM, sqrt Decomposition  (1) 2021.06.30
투 포인터  (0) 2021.06.29