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;
    }

     

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

    IOI21 Keys 풀이  (0) 2022.06.17
    백준 18779 풀이 (Help Yourself)  (0) 2021.06.15
    백준 9208번 풀이 (링월드)  (0) 2021.05.28
    백준 8481번 풀이 (Generator)  (1) 2021.04.29
    백준 18799번 풀이 (이상한 편집기)  (3) 2021.03.26

    댓글

Designed by Tistory.