본문 바로가기

알고리즘/백준 문제 풀이

[C++] 28774번 Шестизначные документы

728x90

 

 

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


 

25/01/02

 

 

백준 땅따먹기에서 나온 문제 중 하나인데, 내가 풀었을 당시에는 골드 4였으나 골드 4치고는 굉장히 어렵다고 느꼈던 문제였다.

 

이러한 아이디어를 복습해보고자 글을 적어본다.


 

문제 접근 방식:

 

 

일단 문제가 러시아어로 적혀있기 때문에 번역을 하면 다음과 같다.

 

회계사 발레리(Valeiry)는 회계 보고서에서 불일치를 해결하고 있습니다.
그는 총 $n$개의 문서를 확인해야 하며, $i$번째 문서는 여섯 자리 정수 식별자 $a_i$를 통해 회사 네트워크에서 접근할 수 있습니다.
역전(inversion)이란, 두 번호 $i$와 $j$($i < j$)에 대해 $a_i$와 $a_j$의 $k$번째 자리 숫자에서 $a_i$의 $k$번째 숫자가 $a_j$의 $k$번째 숫자보다 클 때 발생하는 경우를 말합니다.
배열 ${a_i}$의 복잡성(complexity)은 여섯 자리 각각에서 발생하는 모든 역전의 총합으로 정의됩니다.
발레리는 식별자 세트의 복잡성이 낮을수록 주소창에 입력하는 데 걸리는 시간이 적게 든다는 것을 알고 있습니다.
문서 세트가 고정되어 있고, 검토 순서를 크게 변경하는 것은 위험할 수 있으므로(일부 문서를 누락할 가능성이 있음), 발레리가 사용할 수 있는 유일한 방법은 순서를 순환적으로 몇 자리 이동시키는 것입니다.
순환적 이동이란 배열 $a_1, a_2, \ldots, a_n$을 $t$칸 왼쪽으로 이동시키는 것으로, 이는 배열 $a_{t+1}, a_{t+2}, \ldots, a_n, a_1, a_2, \ldots, a_t$를 의미합니다.
발레리가 주어진 배열의 복잡성을 최소화하는 순환 이동을 선택할 수 있도록 도와주세요.

 

입력 형식은 다음과 같이 주어진다.

3
277659
177013
314836

 

6자리 수 하나가 $a_i$가 되는 것이고, Complexity를 계산할 때는 각 자리 별로 Inversion의 개수를 센 후 다 더해주면 된다.

 

당연히 생각해볼 수 있는 것은, 각 자리 별로 독립이기 때문에 각 자리 별로 쪼개어 생각해볼 수 있다.

 

그래서 하나의 배열에 대해서 생각해보자.

 

즉, 위의 입력 예제의 예시라면 첫번째 자리만 모은 배열 $[2, 1, 3]$을 생각해보자는 뜻이다.

 

이제 우리는 고정된 하나의 배열에 대해서 Inversion의 개수를 빠르게 구하는 방법을 먼저 생각해봐야한다.

 

배열의 최대 길이 $N$은 $100 \ 000$으로 제한되어 있기 때문에 하나의 배열을 나이브하게 탐색하며 Inversion을 구하는 $\mathcal{O}(N^2)$방법은 통하지 않는다.

 

따라서 고정된 하나의 배열에 대해서 Inversion의 개수를 $\mathcal{O}(N)$으로 구해야 한다. 이는 counting sort의 기법을 사용하면 해결할 수 있다.

 

 

예를 들어, 하나의 배열이 다음과 같이 주어져 있다고 가정해보자.

 

$$[1, 2, 1, 3, 5, 2, 1, 3, 9, 7, 6]$$

 

그리고 지금까지 숫자 $i$가 몇 번 나왔는가를 나타내는 Count[10]배열을 선언하자.

 

그리고 다음과 같은 작업을 반복 수행해주면 된다.

 

$j$번째 배열의 원소 $d_j$를 볼 때, Count배열에 $d_j$보다 큰 숫자가 몇 번 나왔는가를 확인한다. 그 값을 Inversion 개수에 더해주고, $\text{Count}[d_j]$++를 해준다.

 

왜냐하면, 지금까지 본 배열의 원소는 $d_1$부터 $d_{j-1}$까지인데, 지금까지 본 배열의 원소 중에서 $d_j$로 인해 만들어 질 수 있는 Inversion은 $d_j$가 작고, $d_j$이전의 원소가 더 커야하기 때문이다. 따라서 Count배열을 선언하고 Inversion개수를 더해주는 것이다.

 

이 과정의 시간 복잡도는 $\mathcal{O}(10N) = \mathcal{O}(N)$이 소요된다.

 

 

이제 고정된 하나의 배열에 대해서 $\mathcal{O}(N)$의 시복도로 구할 수 있음을 확인했다.

 

근데, 우리는 모든 순환 이동에 대해서 이를 구해야 한다. 당연히, 이걸 그대로 하려면 $\mathcal{O}(N^2)$의 시간 복잡도가 소요되므로, 우리는 위에서 Count배열을 선언하여 시간 복잡도를 줄인 것 처럼, 시간 복잡도를 줄이는 방법을 한 번 더 생각해봐야 한다.

 

그래서 자연스럽게 떠오르는 방식은, 맨 처음의 배열에 대한 Inversion의 개수를 구하고, 한 칸 순환시킬 때마다 직접 계산하는 것이 아니라 "차이"를 더하고 빼줘서 빠르게 구해보는 방식이다.

 

그러면 "어떻게 구할 것인가?"가 문제가 될 것이다.

 

기존 배열 $[d_1, d_2, \dots , d_n]$이 주어져 있고, 이 배열에 대한 Inversion 개수를 위의 방식처럼 구했다고 해보자.

 

그러면, $[d_2, d_3, \dots , d_n, d_1]$일 때의 Inversion개수를 구해보자.

 

$d_1$이 맨 앞에 있었는데 사라졌다. 기존에 $d_1$이 맨 앞에 있음으로써 기여했었던 Inversion의 개수는 $d_2, d_3, \dots, d_n$중 $d_1$보다 작았던 원소의 개수와 같다.

 

이는 기존 배열의 Inversion을 구하면서 생긴 Count배열로 빠르게 구할 수 있다. 최종적으로 나온 Count배열을 통해 $d_1$보다 작은 원소의 개수를 빠르게 구할 수 있기 때문이다.

 

마찬가지로, $d_1$을 뒤에서 추가한다고 생각해보자. $d_2, d_3, \dots, d_n$ 중 $d_1$보다 큰 원소들과 $d_1$이 만남으로써 Inversion의 개수가 그만큼 늘어나므로, $d_2, d_3, \dots, d_n$중 $d_1$보다 큰 원소의 개수만큼 Inversion의 개수가 늘어난다.

 

따라서, 왼쪽으로 한 칸 순환시킬 때, 다음과 같이 Inversion의 개수가 늘어나고 줄어든다.

 

$$- \sum_{0}^{d_1 - 1} \text{Count} [i] + \sum_{d_1 + 1}^{9} \text{Count} [i]$$

 

이는 맨 처음 Inversion을 세었을 때 나왔던 Count배열로 빠르게 구할 수 있으므로, $\mathcal{O}(1)$만에 구할 수 있다.

 

이걸 $N$번 반복하면, 모든 순환의 Inversion 개수를 $\mathcal{O}(N)$만에 구할 수 있다.


아래는 내가 위의 접근 방식과 같이 작성한 C++ 코드이다. 더보기를 누르면 확인할 수 있다.

더보기
// 28774번 Шестизначные документы
// 누적 합, DP
/*
번역:
회계사 발레리(Valeiry)는 회계 보고서에서 불일치를 해결하고 있습니다.
그는 총 $n$개의 문서를 확인해야 하며, $i$번째 문서는 여섯 자리 정수 식별자 $a_i$를 통해 회사 네트워크에서 접근할 수 있습니다.

**역전(inversion)**이란, 두 번호 $i$와 $j$($i < j$)에 대해 $a_i$와 $a_j$의 $k$번째 자리 숫자에서 $a_i$의 $k$번째 숫자가 $a_j$의 $k$번째 숫자보다 클 때 발생하는 경우를 말합니다.
배열 ${a_i}$의 **복잡성(complexity)**은 여섯 자리 각각에서 발생하는 모든 역전의 총합으로 정의됩니다.

발레리는 식별자 세트의 복잡성이 낮을수록 주소창에 입력하는 데 걸리는 시간이 적게 든다는 것을 알고 있습니다.
문서 세트가 고정되어 있고, 검토 순서를 크게 변경하는 것은 위험할 수 있으므로(일부 문서를 누락할 가능성이 있음), 발레리가 사용할 수 있는 유일한 방법은 순서를 순환적으로 몇 자리 이동시키는 것입니다.
순환적 이동이란 배열 $a_1, a_2, \ldots, a_n$을 $t$칸 왼쪽으로 이동시키는 것으로, 이는 배열 $a_{t+1}, a_{t+2}, \ldots, a_n, a_1, a_2, \ldots, a_t$를 의미합니다.

발레리가 주어진 배열의 복잡성을 최소화하는 순환 이동을 선택할 수 있도록 도와주세요.

접근 방법:
일단 각 자리 별로 독립이므로 각 자리별로 다 쪼개자.
하나의 자리에 대해서 생각을 해보자.
우리는 이 자리의 모든 역전을 나이브하게 구하려면 O(N^2)의 시복도가 소요됨을 알고있다.
이걸 누적합을 사용하여 O(N)으로 줄여보자.
예를 들어, 하나의 자리에 대해서 복잡성을 계산하는데 이 자리가 다음과 같이 있다고 해보자.
1 2 1 3 5 2 1 3 9 7 6

그리고 숫자가 지금까지 몇번 나왔는가를 나타내는 Count배열을 선언해.
int Count[10] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];

1. 처음에 1을 확인해. Count배열에 1보다 큰 숫자가 몇번 나왔는가를 모두 더해. 그 값은 0이야.
Count[1] += 1을 해줘.
Count = [0, 1, 0, 0, 0, 0, 0, 0, 0, 0]

2. 2를 확인해. Count배열에 2보다 큰 숫자가 몇번 나왔는가를 확인해. 그 값은 0이야.
Count[2] += 1을 해줘.
Count = [0, 1, 1, 0, 0, 0, 0, 0, 0, 0]

3. 1을 확인해. Count배열에 1보다 큰 숫자가 몇번 나왔는가를 확인해. 그 값은 1이야.
Count[1] += 1을 해줘.
Count = [0, 2, 1, 0, 0, 0, 0, 0, 0, 0]
...
이런 식으로 하나의 순환에 대해서 O(N)의 시복도로 구할 수 있다.

당연히 모든 순환에 대해서 하면 O(N^2)인데, 이러면 시간초과가 나므로 DP를 사용하여 계산해보자.

근데 이게 왜 골드임ㄷㄷㄷ
*/
#include <iostream>
#include <vector>
#include <string>
using namespace std;
#define fastio ios::sync_with_stdio(0); cin.tie(0);
#define endl '\n'
typedef long long ll;

vector<ll> solve(const vector<int>& A){
    vector<ll> ans;
    vector<int> cnt(10, 0);
    ll a = 0;
    // 초기 count구하기
    for (int i = 0; i < A.size(); i++){
        for (int j = A[i]+1; j < 10; j++){
            a += cnt[j];
        }
        cnt[A[i]]++;
    }
    ans.push_back(a);
    vector<int> cnt_accum(11, 0);
    cnt_accum[0] = cnt[0];
    for (int i = 1; i < 11; i++){
        cnt_accum[i] = cnt_accum[i-1]+cnt[i-1];
    }
    // 이후 ans 구하기
    for (int i = 0; i < A.size()-1; i++){
        a -= (ll)(cnt_accum[A[i]] - cnt_accum[0]); 
        a += (ll)(cnt_accum[10] - cnt_accum[A[i]+1]);
        ans.push_back(a);
    }
    return ans;
}

int main(void){
    int N; cin >> N;
    vector<vector<int>> A(6);
    vector<vector<ll>> ans(6);
    string S;
    for (int i = 0; i < N; i++){
        cin >> S;
        for (int j = 0; j < 6; j++){
            A[j].push_back((int)(S[j]-'0'));
        }
    }
    for (int i = 0; i < 6; i++){
        ans[i] = solve(A[i]);
    }
    ll m = 1'000'000'000'000'000'000;
    ll s = 0;
    for (int i = 0; i < N; i++){
        s = 0;
        for (int j = 0; j < 6; j++){
            s += ans[j][i];
        }
        m = min(m, s);
    }
    cout << m;
    return 0;
}

'알고리즘 > 백준 문제 풀이' 카테고리의 다른 글

[C++] 25832번 Making Connections  (0) 2025.01.06
[C++] 10246번 부동산 경매  (0) 2025.01.06
[C++] 26648번 물정수열  (0) 2025.01.04
[C++] 1007번 벡터 매칭  (0) 2024.12.27
[Python] 32871번 돌 게임 nm  (0) 2024.12.03