ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • NYPC 2021 본선 1519부문 3번 풀이 (BOJ 23347)
    문제풀이 2021. 11. 1. 12:58

    대회장에선 섭태 1만 긁고 접근도 못했는데 백준에 올라왔길래 업솔빙을 했습니다.

    서브태스크 2와 3의 풀이를 urd05님에게 듣고 100점 풀이를 찾았습니다.

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

     

    23347번: 수열 연산

    모든 수가 $1$ 이상 $K$ 이하의 수로 구성된 수열이 있다. 이 수열에 다른 연속한 부분수열을 삽입하거나 삭제하는 연산을 원하는 만큼 할 수 있다. 단, 이 수열은 길이가 $M$ 이상이어야 하며, 수열

    www.acmicpc.net

    실제 대회에서의 서브태스크는 다음과 같습니다.

    서브태스크 1: $2M \le K$

    서브태스크 2: $M, K, |C|, |D| \le 6$

    서브태스크 3: $M, K, |C|, |D| \le 10$

    서브태스크 4: $M, K, |C|, |D| \le 30$

    서브태스크 5: $M, K, |C|, |D| \le 50$

     

    문제에서 쓸 수 있는 추가 쿼리와 삭제 쿼리를 다음과 같이 표기하겠습니다.

    $add(idx, S)$: 인덱스 $idx$에 집합 $S$를 오름차순 정렬하여 삽입한다.

    $delete(idx, n)$: 구간 $[idx, idx+n)$을 삭제한다.

     

    편의를 위해 다음 집합을 정의하겠습니다.

    $S_{all} = \left\{ 1, 2, \cdots, K \right\}$

    서브태스크 1

    인덱스 $idx$에 있는 수 $x$를 삭제하는 함수 $delete\_number(idx, x)$를 다음과 같이 구현할 수 있습니다.

     

    Case 1: $x \le \frac{K}{2}$일 때

    $add \left(idx+1, \left \{ x+1, x+2, \cdots, K \right \} \right)$

    $delete(idx, K-x+1)$

    ({x} -> {x x+1 x+2 ... K} -> {})

     

    Case 2: 나머지 경우

    $add \left(idx, \left \{ 1, 2, \cdots, x-1 \right \} \right)$

    $delete(idx, x)$

    ({x} -> {1 2 ... x-1 x} -> {})

     

    입력으로 주어지는 수열 $C$와 $D$에 대해, 위 방식으로 각 수열의 원소를 모두 제거해준 다음, $D$를 제거할 때 호출했던 쿼리들의 순서와 부호(추가/삭제)를 모두 뒤집어주면 답이 된다는 것을 알 수 있습니다.

     

    서브태스크 2

    $M$이 커지게 되면 서브태스크 1에서 사용했던 전략을 쓸 수 없게 됩니다. 일단 몇 가지 쉬운 경우부터 생각해봅시다.

     

    Case 1: $K<M$인 경우

    이 경우에는 어떤 쿼리도 사용할 수 없습니다. 따라서 $C$와 $D$가 같은지만 확인해주면 됩니다.

     

    Case 2: $K=M$인 경우

    서브태스크 1에서 했던 것처럼 $C$와 $D$ 각각의 길이를 최대한 줄여봅시다. 추가하거나 삭제할 수 있는 수열은 $1, 2, \cdots, K$로 유일하기 때문에 수열 전체를 돌면서 $1, 2, \cdots, K$를 계속 제거해주면 수열의 길이를 최소화할 수 있고, 동일한 길이의 다른 수열은 만들 수 없음을 알 수 있습니다. (자세한 증명은 생략하겠습니다.)

     

    따라서, $C$와 $D$ 각각의 길이를 최소화시킨 다음 수열이 일치한다면 YES, 다르다면 NO를 출력하면 됩니다.

    방법 출력은 서브태스크 1에서 했던 것처럼 $C$는 그대로 출력하고 $D$는 사용한 쿼리들의 순서와 부호를 모두 뒤집어서 출력해주면 됩니다.

     

    Case 3: $K>M$인 경우

    놀랍게도 이 경우는 항상 YES입니다. $K=M-1$인 경우를 해결할 수 있다면, $K>M-1$인 경우도 동일하게 풀어주면 되기 때문에 $K=M-1$일 때의 풀이만 설명하겠습니다.

     

    서브태스크 1에서 했던 것처럼 $delete\_number(idx, x)$함수를 구현할 수 있다면 문제를 해결할 수 있습니다.

     

    인덱스 $idx$에 있는 수 $x$를 삭제하는 함수 $delete\_number(idx, x)$와 인덱스 $idx$에 수 $x$를 추가하는 함수 $add\_number(idx, x)$를 다음과 같이 구현할 수 있습니다.

     

    $delete\_number(idx, x)$:

        $add(idx, S_{all} - \left\{ x \right\})$

        for $i = x+1$ to $K$:

            $delete\_number(idx+x-1, i)$

        for $i = x+1$ to $K$:

            $add\_number(idx+i-1, i)$

        $delete(idx, K)$

     

    ({x} -> {1 2 ... x-1 x+1 ... K x} -> {1 2 ... x-1 x} -> {1 2 ... x-1 x x+1 ... K} -> {})

     

    $add\_number(idx, x)$도 동일한 방식으로 구현해주면 됩니다.

     

    $x$를 제거할 때 호출하는 쿼리의 개수는 $2 \times 3^{K-x}$이므로, 최대 쿼리 횟수는 $2 \times 3^5 \times 6 \times 2 = 5832$이 되기 때문에 서브태스크 2를 통과할 수 있습니다.

     

    서브태스크 3

    $3^{10}=59049$이므로 서브태스크 2의 풀이로는 서브태스크 3을 풀 수 없습니다.

    서브태스크 2에서 재귀호출을 하는 부분을 생각해보면 $x$를 제거할 때 $x+1, \cdots, K$를 호출하기 때문에 1을 제거하려면 너무 많은 재귀호출이 발생합니다.

     

    $delete\_number(idx, x)$의 구현을 약간 바꿔봅시다.

     

    $delete\_number(idx, x)$:

        $add(idx+1, S_{all} - \left\{ x \right\})$

        for $i = 1$ to $x-1$:

            $delete\_number(idx+1, i)$

        for $i = 1$ to $x-1$:

            $add\_number(idx+i-1, i)$

        $delete(idx, K)$

     

    ({x} -> {x 1 2 ... x-1 x+1 ... K} -> {x x+1 ... K} -> {1 2 ... x-1 x x+1 ... K} -> {})

     

    위 구현을 사용하면 $x$를 제거할 때 $1, \cdots, x-1$을 호출하게 됩니다. 따라서, $\frac{K}{2}$이하인 수는 $x$를 감소시키는 방향, 나머지 수는 $x$를 증가시키는 방향으로 구현해주면 쿼리 횟수가 $O(K3^{\frac{K}{2}})$가 되기 때문에 서브태스크 3을 맞을 수 있습니다.

    (최악의 경우 쿼리 횟수 $2 \times 3^{4} \times 10 \times 2 = 3240$)

     

    서브태스크 4

    제한이 30이 되었기 때문에 더 이상 지수복잡도로는 해결이 불가능해 보입니다.

    쿼리 횟수가 지수복잡도가 되는 이유를 생각해보면 $x$를 제거할 때 $x+1, \cdots, K$를 재귀호출하기 때문에 횟수가 지수적으로 증가하게 됩니다.

    그렇기 때문에 $x+1, \cdots, K$를 각각 제거하지 않고, 한 번에 제거해보겠습니다.

     

    구간 $[idx, idx+K-x-1)$에 $x, x+1, \cdots, K$이 차례대로 있을 때, 이를 제거하는 함수 $delete\_suffix(idx, x)$를 다음과 같이 구현할 수 있습니다.

     

    $delete\_suffix(idx, x)$:

        $add(idx, S_{all} - \left\{ x \right\})$

        if $x<K$:

            $delete\_suffix(idx+x-1, x+1)$

        $delete(idx, K)$

     

    ({x x+1 ... K} -> {1 2 ... x-1 x+1 ... K x x+1 ... K} -> {1 2 ... x-1 x x+1 ... K} -> {})

     

    동일한 방식으로 suffix를 추가하는 $add\_suffix(idx, x)$도 구현할 수 있습니다.

     

    $delete\_suffix(idx, x)$의 쿼리 횟수는 $2(K-x+1)$이므로 $delete\_number(idx, x)$의 쿼리 횟수는 $4(K-x)+2$가 됩니다.

    최악의 경우 $(4 \times (30-1)+2) \times 30 \times 2 = 7080$번 쿼리를 호출하기 때문에 서브태스크 4를 맞을 수 있습니다. 

     

    서브태스크 5

    서브태스크 4 풀이로 구현하면 최악의 경우 쿼리 횟수가 $(4 \times (50-1)+2) \times 50 \times 2 = 19800$이 됩니다.

    서브태스크 3에서 사용했던 아이디어를 가져와서, x가 작을 때는 suffix 대신 prefix를 제거하고 추가하는 방식으로 구현을 해주면 상수를 절반으로 줄일 수 있습니다.

     

    이때, 최악의 경우는 수열의 모든 원소가 25 또는 26인 경우고, 쿼리 횟수를 계산해보면 $(4 \times (50-26)+2) \times 50 \times 2 = 9800$이 되어 문제를 해결할 수 있습니다.

     

    구현

    #include <bits/stdc++.h>
    
    typedef long long ll;
    using namespace std;
    struct Query{
        char op;
        int i, len;
        vector<int> v;
        Query(){}
        Query(char _op, int _i, vector<int>::iterator _s, vector<int>::iterator _e): op(_op), i(_i) {
            for (auto iter = _s;iter!=_e;iter++) v.push_back(*iter);
            len = v.size();
        }
        void convert(){
            if (op=='+') op = '-';
            else op = '+';
        }
    };
    vector<Query> Q1, Q2;
    vector<int> a, b, _all, _all_1[101];
    int K, M, A, B;
    
    void _add(vector<int> &C, int idx, vector<int> &D, vector<Query> &Q){
        Q.emplace_back('+', idx, D.begin(), D.end());
        C.insert(C.begin()+idx, D.begin(), D.end());
    }
    
    void _delete(vector<int> &C, int idx, int num, vector<Query> &Q){
        Q.emplace_back('-', idx, C.begin()+idx, C.begin()+idx+num);
        C.erase(C.begin()+idx, C.begin()+idx+num);
    }
    
    void _delete_suffix(vector<int> &C, int idx, int num, vector<Query> &Q){
        _add(C, idx, _all_1[num], Q);
        if (num<K) _delete_suffix(C, idx+num-1, num+1, Q);
        _delete(C, idx, K, Q);
    }
    
    void _add_suffix(vector<int> &C, int idx, int num, vector<Query> &Q){
        _add(C, idx, _all, Q);
        if (num<K) _add_suffix(C, idx+num-1, num+1, Q);
        _delete(C, idx, K-1, Q);
    }
    
    void _delete_prefix(vector<int> &C, int idx, int num, vector<Query> &Q){
        _add(C, idx+num, _all_1[num], Q);
        if (num>1) _delete_prefix(C, idx+num, num-1, Q);
        _delete(C, idx, K, Q);
    }
    
    void _add_prefix(vector<int> &C, int idx, int num, vector<Query> &Q){
        _add(C, idx, _all, Q);
        if (num>1) _add_prefix(C, idx+num, num-1, Q);
        _delete(C, idx+num, K-1, Q);
    }
    
    void solve1(vector<int> &C, vector<Query> &Q){
        for (int i=(int)C.size()-1;i>=0;i--){
            int x = C[i];
    
            if (x>K/2){
                _add(C, i, _all_1[x], Q);
                if (x<K){
                    _delete_suffix(C, i+x-1, x+1, Q);
                    _add_suffix(C, i+x, x+1, Q);
                }
                _delete(C, i, K, Q);
            }
            else{
                _add(C, i+1, _all_1[x], Q);
                if (x>1){
                    _delete_prefix(C, i+1, x-1, Q);
                    _add_prefix(C, i, x-1, Q);
                }
                _delete(C, i, K, Q);
            }
    
        }
    }
    
    void solve2(vector<int> &C, vector<Query> &Q){
        while(true){
            bool flag = 0;
            for (int i=0;i+K<=(int)C.size();i++){
                bool flag2 = 1;
                for (int j=1;j<=K;j++) if (C[i+j-1]!=j) {flag2 = 0; break;}
                if (flag2){
                    flag = 1;
                    _delete(C, i, K, Q);
                    break;
                }
            }
            if (!flag) break;
        }
    }
    
    void output(){
        reverse(Q2.begin(), Q2.end());
        for (auto &q:Q2) q.convert();
    
        printf("YES\n%d\n", (int)Q1.size()+(int)Q2.size());
        for (auto &q:Q1){
            printf("%c %d %d ", q.op, q.i, q.len);
            if (q.op=='+'){
                for (auto &x:q.v) printf("%d ", x);
            }
            printf("\n");
        }
        //printf("------------------------------------------\n");
        for (auto &q:Q2){
            printf("%c %d %d ", q.op, q.i, q.len);
            if (q.op=='+'){
                for (auto &x:q.v) printf("%d ", x);
            }
            printf("\n");
        }
    }
    
    int main(){
        scanf("%d %d", &K, &M);
        scanf("%d", &A);
        a.resize(A);
        for (int i=0;i<A;i++) scanf("%d", &a[i]);
        scanf("%d", &B);
        b.resize(B);
        for (int i=0;i<B;i++) scanf("%d", &b[i]);
    
        for (int i=1;i<=K;i++){
            _all.push_back(i);
            for (int j=1;j<=K;j++) if (i!=j) _all_1[j].push_back(i);
        }
    
        if (K<M){
            if (a==b) printf("YES\n0\n");
            else printf("NO\n");
            return 0;
        }
        else if (K>M){solve1(a, Q1);solve1(b, Q2);}
        else {
            solve2(a, Q1); solve2(b, Q2);
            if (a!=b) {printf("NO\n"); return 0;}
        }
        output();
        return 0;
    }

     

    '문제풀이' 카테고리의 다른 글

    PS 일지 (1/3 ~ 1/23)  (1) 2023.01.24
    IOI21 Keys 풀이  (0) 2022.06.17
    백준 18779 풀이 (Help Yourself)  (0) 2021.06.15
    백준 9208번 풀이 (링월드)  (0) 2021.05.28
    백준 8481번 풀이 (Generator)  (1) 2021.04.29
Designed by Tistory.