Jekyll2024-08-17T11:19:39+00:00https://kidw0124.github.io/rss/kidw0124’s PS Blogkidw0124 블로그입니다kidw0124UCPC 2024 예선 후기2024-07-14T10:00:00+00:002024-07-14T10:00:00+00:00https://kidw0124.github.io/contest/2024/07/14/ucpc2024-pre 7솔브로 32등을 하였습니다. 아마 결과가 나와봐야겠지만, 본선에 진출할 수 있을 듯 합니다.

+ 본선에 진출하였습니다.

최종 전체 스코어보드 펼치기/접기

문제 정보

    대회 시작 전

    원래는 ICPC 팀으로 나가려 했으나 jinhan814가 본선 당일 개인 일정이 있어 tmdghks과 같이 나갔습니다. 예선은 저와 eoaud0108은 학교에서 같이 하기로 했고, tmdghks은 다른 장소에서 디스코드를 통해 소통하기로 했습니다.

    A가 가장 쉬운 문제임은 이미 공지 되어 있는 상태여서, A는 제가 빠르게 풀기로 했습니다. eoaud0108는 앞부터 차례로 본다고 했고, tmdghks은 쉬운 문제를 찾는다고 한것 같은데, 정확히 기억나지는 않네요.

    대회 중

    우선 계획대로 A를 봤습니다. 지문은 길어보여서 일단 입출력을 읽었습니다. 입력을 보니 직사각형 모양이 주어지는 것 같고, 출력을 보니 안에 들어가는 원의 최대 반지름을 구하라는 것 같아 예제를 보니 맞아보였습니다. 단위를 굵게 칠해 준 것도 도움이 되었습니다. 보자마자 풀어 코딩에 들어갔습니다.


    A - 체육은 수학과목 입니다 [+, 0:00:57]

    A번 풀이 펼치기/접기
    사용 알고리즘
    • 사칙연산(Arithmetic)

    $\min(h,w)\times50$을 출력하면 됩니다.

    코드
    #include<bits/stdc++.h>
    using namespace std;
    #ifdef kidw0124
    constexpr bool ddebug = true;
    #else
    constexpr bool ddebug = false;
    #endif
    #define debug if constexpr(ddebug) cout << "[DEBUG] "
    
    void solve(){
        int h,w;
        cin>>h>>w;
        cout<<min(h,w)*50<<'\n';
    }
    
    int main() {
    #ifdef kidw0124
        freopen("input.txt","r",stdin);
        freopen("output.txt","w",stdout);
        clock_t st=clock();
    #else
        ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    #endif
        int t=1;
    //    cin>>t;
        while(t--)solve();
    #ifdef kidw0124
        debug<<clock()-st<<"ms\n";
    #endif
    }
    

    이 문제를 풀고 B는 eoaud0108가 보고 있어서 저는 C를 봤습니다. 그림을 보는데 얼마 전 Solved.ac 마라톤에서 본 BOJ 24536 - 정원장어문제와 같은 문제인 줄 알고 제가 잡겠다고 했습니다.

    이 사이 tmdghks이 E가 쉬운 문제인 것 같다고 하였고, 동시에 J가 상당히 많은 틀린 제출이 있다는 것을 확인했습니다. eoaud0108이 E를 보더니 그리 쉬운 문제는 아닌 것 같다고 하였고, tmdghkseoaud0108이 J는 그리디하게 하면 안되는지, 그렇다면 저렇게 많은 오답 제출이 있을 것 같지 않다고 얘기하며 다른 문제를 보고 있었습니다.

    그러는 동안 제가 잡은 C가 기존의 문제와 같은 문제가 아님을 알았고, 스코어보드에 E가 풀린 것을 확인했습니다. 그리고 tmdghks가 E가 브루트포스인 것 같다고 하였고, 저도 브루트포스로 될 것 같아서 풀었습니다.


    E - 지금 자면 꿈을 꾸지만 [+, 0:31:26]

    E번 풀이 펼치기/접기

    사용 알고리즘

    • 그리디(Greedy)
    • 정렬(Sort)
    • 브루트포스(Brute Force)
    • 시뮬레이션(Simulation)

    과제가 $N$개가 있고, 각각 기한이 있습니다. 과제 하나를 하는 데는 $A$만큼 시간이 걸립니다. 실버(문제 주인공)은 딱 한 번 원하는 시각에 원하는 $X(1\le X \le A-1)$를 골라 $BX$ 만큼 자고, 이후 과제를 하나 하는데 걸리는 시간을 $A-X$로 만들 수 있습니다. 과제를 최대한 많이 하는 개수를 구하는 문제입니다.

    $N, A, B$의 범위가 $100$이하입니다. 우선 주어진 과제의 마감 기한을 정렬합니다. 과제별 가중치가 없으므로 과제를 끝낼 수 있다면 가능한 앞쪽 과제를 끝내는 것이 이득입니다. 잠에 들기 전 $i$개의 과제를 끝내거나 포기하고, $j$만큼 자고 일어났을 때, 얼마나 많은 과제를 할 수 있을 지 구합니다. $i$와 $j$가 정해지면, 그리디하게 배정하면 되므로 $O(N)$에 $i$, $j$에서 최대값을 구할 수 있습니다. 따라서 $O(N^2A)$에 풀 수 있습니다.

    코드

    #include<bits/stdc++.h>
    using namespace std;
    #ifdef kidw0124
    constexpr bool ddebug = true;
    #else
    constexpr bool ddebug = false;
    #endif
    #define debug if constexpr(ddebug) cout << "[DEBUG] "
    
    void solve(){
        int i,j,k;
        int n,a,b;
        cin>>n>>a>>b;
        vector<int>trr(n+1);
        for(i=1;i<n+1;i++){
            cin>>trr[i];
        }
        sort(trr.begin(), trr.end());
        int ans=0;
        for(i=0;i<n;i++){
            for(j=0;j<a;j++){
                int tmp=0,now=0;
                for(k=1;k<=i;k++){
                    if(now+a<=trr[k]){
                        now+=a;
                        tmp++;
                    }
                }
                now+=b*j;
                for(k=i+1;k<=n;k++){
                    if(now+(a-j)<=trr[k]){
                        now+=a-j;
                        tmp++;
                    }
                }
                ans=max(ans,tmp);
            }
        }
        cout<<ans<<'\n';
    }
    
    int main() {
    #ifdef kidw0124
        freopen("input.txt","r",stdin);
        freopen("output.txt","w",stdout);
        clock_t st=clock();
    #else
        ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    #endif
        int t=1;
    //    cin>>t;
        while(t--)solve();
    #ifdef kidw0124
        debug<<clock()-st<<"ms\n";
    #endif
    }
    

    E를 구현하는 중 C, D, G, H, J가 풀렸습니다. eoaud0108이 D는 쉬워보인다고 해서 잡기로 했고, tmdghks는 나머지 문제들을 생각하기로 했습니다.

    제가 E를 AC를 띄우고 J를 보니 그리디하게 될 것 같아 잡았습니다. 동전이 일렬로 주어질 때 “연속된”, “같은” 두개의 동전을 뒤집어 모두 앞면을 보게 하는 최소 시행을 구하는 것인데, 제가 “연속된”만 보고 “같은”을 보지 않고 제출해 한 번 틀렸습니다.

    다시 돌아와서 J가 해당 방식으로 되지 않는 다는 것을 알고 일단 다른 문제를 읽기 시작했습니다. C는 아까 생각해봤었고, D는 eoaud0108가 풀고 있으니, G를 봤습니다. G는 문제는 이해 됐는데, 도저히 머리로 입체가 그려지지 않아 일단 넘어갔고, G에서 머리가 띵해져서 H를 읽기보다 그냥 J를 다시 생각하기로 했습니다.

    그 사이 eoaud0108이 D를 풀어 제출했으나, WA를 받았습니다. 단순 세그로 풀린다는데, WA가 왜 떴는 지 모르겠다고 하며 eoaud0108가 D 틀린 것을 찾기로 했고, tmdghks은 G를 그냥 하면 될 것 같다며 G를 풀기로 했습니다.

    저는 J를 그리디였으면 다른 팀들이 다 풀었을 텐데, HH를 뒤집어야 하는 예시가 있나 생각하다가 THTTHT라는 기깔난 예시를 찾았습니다. 그리고 이는 재귀적으로 풀리겠다 생각했으나, TLE가 나는 것이 분명해 보여 해당 문제 더 생각이 나지 않아 그냥 아까 C를 생각하던 것이 있어 다시 보기로 했습니다. 나중에 통계를 통해 알게 되었는데, J는 WA보다 TLE가 월등히 많이 제출된 문제였습니다. 그러니까 제가 이 때 “그리디가 아니여서 다른 팀들이 틀렸을거야”라고 생각한 것은 사실 아니였고 다른 팀들은 시뮬레이션처럼 진행해 그랬던 것 같습니다.

    이 때 eoaud0108가 D 코드에서 세그를 구성할 때 1<<18의 크기로 구성했고, 이것이 범위가 작아 설마 이것 때문에 틀리나 싶어 다시 제출한다 했는데, 그 설마가 맞았어서 허무하게 D를 맞았습니다.


    D - 이진 검색 트리 복원하기 [+1, 0:50:57]


    저는 C를 풀고 있고, tmdghks는 G를 풀고 있고, H가 많이 풀려 eoaud0108가 H를 보기로 했습니다. H를 보더니 eoaud0108가 만조분이라고 하며 이를 풀기로 했습니다.

    10분 쯤 지나고 eoaud0108가 H를 제출하며 이건 WA 많이 뜰수도 있다고 했습니다. 예상대로 첫 제출은 WA가 떴고 20분 뒤 두 번째 제출도 WA가 떴습니다.

    그 사이 저는 C코딩을 끝냈고, 제출했으나 WA가 나왔습니다. L과 R이 나뉘는 지점을 기준으로 모두 돌았는데, WA가 떠서 혹시나 풀이가 틀렸나 열심히 고민하던 중, LR이 안나뉘고 모두 R이거나 모두 L일 수 있음을 깨달아 이를 고쳐 제출하니 AC가 떴습니다. 자세한 설명은 아래에 있습니다.


    C - 미어캣 [+1, 1:33:13]

    C번 풀이 펼치기/접기

    사용 알고리즘

    • 그리디(Greedy)
    • 정렬(Sort)
    • 브루트포스(Brute Force)

    왼쪽 혹은 오른쪽을 보고 있는 미어캣 $N$마리가 있습니다. $N$마리의 미어캣은 서로 다른 위치에 일렬로 서 있고, 키가 서로 다릅니다. 같은 방향을 보고 있는 미어캣들의 순서를 재배치하는 것은 가능합니다. 다만 L과 R의 배치는 유지되어야 합니다. 즉, 서로 다른 방향을 보고 있는 미어캣들의 순서를 바꾸는 것은 불가능합니다.(이것이 정원장어와 다른 점입니다)

    최종적으로 각 미어캣에 대해 자신이 바라보는 방향에 자신보다 키가 큰 미어캣이 없는 경우 망을 볼 수 있습니다. 이때 망을 볼 수 있는 미어캣의 최대 수를 구하는 문제입니다.

    정원장어의 아이디어를 그대로 갖고 올 수 있습니다. 망을 보는 미어캣은 LL...LLRR...RR의 순서로, L에서는 오름차순 R에서는 내림차순의 키를 가지고 있어야 합니다. RL이 있는 경우 둘 중 키가 작은 미어캣은 다른 미어캣에 막히게 됩니다. 즉, LR의 지점이 최대 $N$개 있을 수 있고, 이를 고정시켰다고 생각해봅니다.

    기준점을 기준으로 왼쪽 L의 경우 망을 최대한 봐야 하므로 키가 커야 하며, 오른쪽 L의 경우 최대한 키가 작아야 합니다. 즉, 기준점 바로 옆 L에 가장 키가 큰 것을 놓고, 왼쪽으로 가며 차례로 내림차순으로 키가 작아지도록 배치합니다. 그리고 기준점 오른쪽 R의 경우 최대한 많이 봐야 하므로, 기준점 오른쪽의 L은 오른쪽으로 갈 수록 키가 작아지게 하면 됩니다. 만약 이 경우 RL에 막혀 보지 못한다면 L의 순서를 바꾸면 그 R은 계속 못보며 새로운 못보는 것이 생길 수 있으므로 이것이 이득입니다. 정리하자면, 기준점을 기준으로 왼쪽에 가장 키가 큰 L을 놓고, 왼쪽으로 가면서 키가 큰 순서대로 L을 차례로 놓아 준 뒤, 다시 기준점으로 와서 오른쪽으로 가며 키가 큰 순서대로 L을 배치하면 됩니다.(ex> 기준점이 |라면 5678|4321) R은 반대로 배치해 줍니다.

    모든 기준점이 $O(N)$개 있고, 각 기준점 별로 그리디하게 배치하면 $O(N)$에 해결할 수 있으므로, 총 시간복잡도 $O(N^2)$에 풀 수 있습니다. 정렬의 시간복잡도는 $O(N\lg N)$이므로, 답보다 작습니다.

    이때, RR....RRLL....LL의 경우도 고려해주어야 합니다. 구현 상에서 $0$이상 $N$이하로 해주면 됩니다. 이 경우를 고려하지 않아 $0$이상 $N$미만으로 구현해 처음에 WA가 떴습니다.

    + 기여를 보니까 정렬 안하고 투포인터로 풀 수 있는 듯 합니다.

    + 정렬을 안하고 투포인터가 아니라 정렬 후 선형 시간에 풀 수 있다는 것 이라고 합니다.

    코드

    #include<bits/stdc++.h>
    #pragma comment(linker, "/STACK:336777216")
    #pragma GCC optimize("O3,unroll-loops")
    #pragma GCC target("avx,avx2,fma")
    using namespace std;
    #ifdef kidw0124
    constexpr bool ddebug = true;
    #else
    constexpr bool ddebug = false;
    #endif
    #define debug if constexpr(ddebug) cout << "[DEBUG] "
    
    void solve(){
        int i,j,k;
        int n,a,b;
        cin>>n;
        vector<pair<int,int>> loc(n);
        vector<int> lef,rig;
        for(i=0;i<n;i++){
            cin>>loc[i].first;
            char dir;
            cin>>dir;
            if(dir=='L')lef.push_back(loc[i].first),loc[i].second=0;
            else rig.push_back(loc[i].first),loc[i].second=1;
        }
        if(lef.empty()||rig.empty()){
            cout<<n<<'\n';
            return;
        }
        sort(lef.begin(), lef.end());
        sort(rig.begin(), rig.end());
        int ans=0;
        for(i=-1;i<n;i++){
            vector<int> tmp(n);
            int nowl=lef.size()-1,nowr=rig.size()-1;
            for(j=i;j>=0;j--){
                if(loc[j].second==0){
                    tmp[j]=lef[nowl--];
                }
            }
            for(j=i+1;j<n;j++){
                if(loc[j].second==1){
                    tmp[j]=rig[nowr--];
                }
            }
            for(j=i;j>=0;j--){
                if(loc[j].second==1){
                    tmp[j]=rig[nowr--];
                }
            }
            for(j=i+1;j<n;j++){
                if(loc[j].second==0){
                    tmp[j]=lef[nowl--];
                }
            }
            int now=0;
            vector<int>maxlef(n),maxrig(n);
            maxlef[0]=tmp[0];
            for(j=1;j<n;j++){
                maxlef[j]=max(maxlef[j-1],tmp[j]);
            }
            maxrig[n-1]=tmp[n-1];
            for(j=n-2;j>=0;j--){
                maxrig[j]=max(maxrig[j+1],tmp[j]);
            }
            for(j=0;j<n;j++){
                if(loc[j].second==0){
                    if(j==0||tmp[j]>maxlef[j-1]) now++;
                }
                else{
                    if(j==n-1||tmp[j]>maxrig[j+1]) now++;
                }
            }
            ans=max(ans,now);
        }
        cout<<ans<<'\n';
    }
    
    int main() {
    #ifdef kidw0124
        freopen("input.txt","r",stdin);
        freopen("output.txt","w",stdout);
        clock_t st=clock();
    #else
        ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    #endif
        int t=1;
    //    cin>>t;
        while(t--)solve();
    #ifdef kidw0124
        debug<<clock()-st<<"ms\n";
    #endif
    }
    

    이걸 풀고 나서 eoaud0108가 혹시 H에서 자기가 빼먹은 케이스가 있는 지 확인해 달라고 하며 저에게 문제를 설명하고 케이스들을 설명하고 고민하는 과정에서 하나의 케이스가 더 있음을 발견했습니다. 이를 풀어서 제출하니 AC가 떴습니다.


    H - 만보기 대행 서비스 [+2, 1:42:01]

    H번 풀이 펼치기/접기

    사용 알고리즘

    • 정렬(Sort)
    • 많은 조건 분기(Case-Work)
    • 애드 혹(Ad-Hoc)

    부호가 같은 보관함은 가장 원점에서 먼 보관함만 보면 됩니다.

    • 부호가 모두 같다면 가장 먼걸 집고 $D$만큼 이동했다가 오면 됩니다.
    • 부호가 다른 것이 있다면, 가장 작은 것(음수)와 가장 먼 것을 보고 아래 중 최소를 택합니다.
      • 부호가 같을 때의 행위를 두 번 합니다.
      • 한쪽을 집고 와서 반대쪽에서 부호가 같을 때의 행위를 하고, 처음 집었던 것을 놓고 옵니다.
      • 만약 둘 사이 거리가 $D$보다
        • 크다면, 하나를 집고 반대로 갔다가 다시 놓고 다시 반대로 갔다가 옵니다
        • 작다면, 하나를 집고 반대를 가서 집고 처음 집었던 것의 남은 거리를 움직이고 다시 순서대로 놓고 옵니다.

    이 경우를 모두 봐 최소를 출력합니다. 정렬을 하거나 최소/최대를 찾아야 하므로 $O(N\lg N)$ 혹은 $O(N)$에 풀 수 있습니다. 범위가 int를 넘어갈 수 있으므로 long long으로 처리해야 합니다.

    코드

    #include<bits/stdc++.h>
    using namespace std;
    using ll = long long;
    #ifdef kidw0124
    constexpr bool ddebug = true;
    #else
    constexpr bool ddebug = false;
    #endif
    #define debug if constexpr(ddebug) cout << "[DEBUG] "
    
    void solve(){
        int i,j,k;
        int n,d;
        cin>>n>>d;
        vector<ll> arr(n);
        for(ll &x:arr)cin>>x;
        sort(arr.begin(),arr.end());
        if(arr.front()*arr.back()>=0){
            cout<<max(abs(arr.front()),abs(arr.back()))*2+d<<'\n';
        }
        else{
            cout<<min({
                -arr.front()*2+arr.back()*2+d*2,
                -arr.front()*4+arr.back()*2+d*1,
                -arr.front()*2+arr.back()*4+d*1,
                (arr.back()-arr.front())*2>=d?
                    (arr.back()-arr.front())*4:
                    -arr.front()*2+arr.back()*2+d,
            })<<'\n';
        }
    }
    
    int main() {
    #ifdef kidw0124
        freopen("input.txt","r",stdin);
        freopen("output.txt","w",stdout);
        clock_t st=clock();
    #else
        ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    #endif
        int t=1;
    //    cin>>t;
        while(t--)solve();
    #ifdef kidw0124
        debug<<clock()-st<<"ms\n";
    #endif
    }
    

    이제 남은 문제는 B, F, G, I, J, K였습니다. G는 tmdghks이 보고 있고, 솔브 수를 보니 반드시 J를 풀어야겠다고 생각했습니다. 제가 J를 아까부터 보고 있었어서 J를 보고, eoaud0108이 남은 문제를 읽고 생각하고 있었습니다.

    그러다가 tmdghks이 G 케이스가 $81$가지인데, 정리하는 것 좀 도와달라고 해 eoaud0108이 도와주기로 했습니다.

    저는 J에서 어떤 T를 발견하면 다음 연속된 TT혹은 HH를 뒤집어야 겠다 생각했습니다. 더불어 그렇다면 사이에 반복되어 있을 것이며, THTHTHTHTHTT를 뒤집게 되면 하나씩 밀려 결국 HHTHTHTHTHTH가 됨을 알았습니다. 그래서 연속된 구간을 set에 저장해두고, 앞에서 부터 T를 만나면 다음 구간에서 뒤집고, 길이만큼 더한 뒤 다시 넣는 방식으로 반복하는 코드를 작성해 제출했으나 WA가 나왔습니다. 나중에 풀이를 알고 보니 해당 set을 depth마다 만들면 정해의 stack에서 size랑 대응되어 동치인 풀이가 나온다는 것을 알게 되었습니다.

    일단 WA가 나온 시점에서 저는 J에서 아이디어를 떠올리기 힘들다고 생각을 했고, 아까 나온 “재귀”의 아이디어를 분할정복으로 바꾸어 풀 수 있을 지 고민했습니다. 그래서 대충 생각했으나 THHHTNO가 나온다는 것을 알고, 가능/불가능 판정이 해당 방식으로는 어려울 것 같아 해당 방식은 버리게 되었습니다. 이 시점에서 J는 제 문제가 아닌가보다 하고 eoaud0108에게 넘기기로 하였습니다.

    남은 B, F, I, K 중 B, F, I는 지문 읽으면서 어려운 스멜이 났고, K는 그나마 구성적이여서 할만해 보였습니다. 실제로 스코어보드도 G, J를 제외하면 K처럼 보여 K를 잡기로 했습니다. eoaud0108가 읽었어서 문제를 설명해주었고, 저는 다른 제한보다 $A+B$가 변의 길이 이하라는 제한에 집중해보기로 했습니다.

    우선 홀수 차수가 홀수 개면 안되는 것은 자명했고, 일렬로 쭉 나열한 것을 생각해보니 홀수가 $2$개 있는 경우는 항상 가능함을 알 수 있었습니다. 더 나아가 홀수가 $2$개 이상일 때는 반드시 만드는 경우가 있음을 알았고, 남은 경우는 홀수가 $0$개 있을 때 짝수가 문제였습니다. 자세한 설명은 아래 풀이에 적어두었습니다.

    대충 이 시점에서 스코어보드는 프리즈 되었습니다.

    프리즈 시점 전체 스코어보드 펼치기/접기

    프리즈 시점에 5솔 1등으로 42등이였기 때문에, 패널티는 저희 팀도 관리를 잘 못했지만, 전체적으로 관리가 어렵다는 판단을 했고, 못해도 1솔브는 더, 웬만하면 2솔브는 더 풀어야겠다고 생각했습니다. G번의 경우 맞는 것을 바라는 것은 너무 도박일 수 있었기 때문에, J와 K를 푸는 것을 목표로 했습니다. K는 거의 풀렷다고 생각했기 때문에 이 시점에서 tmdghks에게 부담을 가지지 말라고 했고, G는 맞으면 좋다의 마인드로 접근했습니다.

    그리고 조금 더 생각하다가 K에서 가능한 경우들을 찾아 풀었습니다.


    K - 나무 심기 [+1, 2:49:44]

    K번 풀이 펼치기/접기

    사용 알고리즘

    • 구성적(Constructive)
    • 많은 조건 분기(Case-Work)
    • 애드 혹(Ad-hoc)

    우선 홀수가 $2$개 이상 있다면 아래와 같이 배치할 수 있습니다. 우선 양 끝에 홀수 차수 점 $2$개를 배치한 다음 남는 홀수 차수는 짝수개 이므로, 지그재그로 배치합니다. 그리고 짝수들에 대해서는 차수가 2가 되도록 일렬로 배치해주면 됩니다.

    .O.O.O.......
    OOOOOOOOOOOOO
    ..O.O.O......
    

    K에서 홀수가 $0$개 있을 때 짝수를 몇개 채우는 것을 고민해봤습니다. 우선 ㅁ모양으로 채우면 4개가 가능하며, 예제처럼 ㅁ에 끝 모서리에 $3$개씩 더해가면 $3k+1$꼴이 된다는 것을 알았습니다. 덤으로 ㅁ도 점 하나에서 $3$개를 더했다고 생각했습니다.

    O    OO    OO     OO
         OO    OOO    OOO
                OO     OOO
                        OO
    

    또한 $8$, $12$일 때 아래 모양 처럼됨을 알았습니다. 나중에 알았는데 $8$은 예제에 있었습니다. 이거에서 $3$개씩은 붙여나갈 수 있으므로 8이상의 $3k+2$, 12이상의 $3k$꼴은 모두 됨을 알 수 있습니다.

    OOO    OOOO    OOO
    O.O    O..O    O.O
    OOO    O..O    OOOO
           OOOO      OO
    

    추가로, 이걸 짜고 제출 후에 생각난 방식으로 아래와 같이 $8$이상의 짝수를 모두 만들 수도 있습니다. 또한 오른쪽 아래에 3개를 붙이는 방식으로 $11$이상의 홀수도 모두 만들 수 있습니다.

    OOO    OOOO    OOOOO
    O.O    O..O    O...O
    OOO    OOOO    OOOOO
    

    남는 수는 $2$, $3$, $5$, $6$, $9$인데, $9$는 예제에 안된다고 주어져 있고, 나머지는 아무리 해봐도 안되길래 확신의 믿음을 가지고 제출했습니다.

    그러나 WA가 나오길래 아무리 봐도 맞는데 구현 실수인가 하다가 YES인 경우에 YES와 행과 열의 크기를 출력하지 않았음을 알아 다시 제출해 AC를 띄웠습니다. 시간복잡도는 유의미하지 않지만 $O(N^2+A+B)$입니다. $N$은 $200$으로 고정했습니다.

    코드1(대회 중에 낸 방식)

    #include<bits/stdc++.h>
    using namespace std;
    #ifdef kidw0124
    constexpr bool ddebug = true;
    #else
    constexpr bool ddebug = false;
    #endif
    #define debug if constexpr(ddebug) cout << "[DEBUG] "
    
    void solve(){
        int i,j,k;
        int n,m;
        cin>>n>>m;
        vector<string> arr(200,string(200,'.'));
        if(m==0){
            if(n%3==1){
                arr[0][0]='O';
                i=1;
                for(j=0;j<(n-1)/3;j++){
                    arr[i][i-1]='O';
                    arr[i][i]='O';
                    arr[i-1][i]='O';
                    i++;
                }
            }
            else if(n%3==2){
                if(n<8){
                    cout<<"NO\n";
                    return;
                }
                arr[0][0]=arr[0][1]=arr[0][2]
                        =arr[1][0]          =arr[1][2]
                        =arr[2][0]=arr[2][1]=arr[2][2]='O';
                i=3;
                for(j=0;j<(n-8)/3;j++){
                    arr[i][i-1]='O';
                    arr[i][i]='O';
                    arr[i-1][i]='O';
                    i++;
                }
            }
            else{
                if(n<12){
                    cout<<"NO\n";
                    return;
                }
                arr[0][0]=arr[0][1]=arr[0][2]=arr[0][3]
                        =arr[1][0]                  =arr[1][3]
                        =arr[2][0]                  =arr[2][3]
                        =arr[3][0]=arr[3][1]=arr[3][2]=arr[3][3]='O';
                i=4;
                for(j=0;j<(n-12)/3;j++){
                    arr[i][i-1]='O';
                    arr[i][i]='O';
                    arr[i-1][i]='O';
                    i++;
                }
            }
        }
        else if(m&1){
            cout<<"NO\n";
            return;
        }
        else{
            arr[0][2]='O';
            i=1;
            for(j=0;j<(m-1)/2;j++){
                arr[i][2]='O';
                if(j&1){
                    arr[i][1]='O';
                }
                else{
                    arr[i][3]='O';
                }
                i++;
            }
            for(j=0;j<n;j++){
                arr[i][2]='O';
                i++;
            }
            arr[i][2]='O';
        }
        cout<<"YES\n";
        cout<<"200 200\n";
        for(i=0;i<200;i++) {
            cout << arr[i] << '\n';
        }
    }
    
    int main() {
    #ifdef kidw0124
        freopen("input.txt","r",stdin);
        freopen("output.txt","w",stdout);
        clock_t st=clock();
    #else
        ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    #endif
        int t=1;
    //    cin>>t;
        while(t--)solve();
    #ifdef kidw0124
        debug<<clock()-st<<"ms\n";
    #endif
    }
    

    코드2(제출 후 생각한 더 조건이 적은 방식)

    #include<bits/stdc++.h>
    using namespace std;
    #ifdef kidw0124
    constexpr bool ddebug = true;
    #else
    constexpr bool ddebug = false;
    #endif
    #define debug if constexpr(ddebug) cout << "[DEBUG] "
    
    void solve(){
        int i,j,k;
        int n,m;
        cin>>n>>m;
        vector<string> arr(200,string(200,'.'));
        if(m==0){
            if(n%3==1){
                arr[0][0]='O';
                i=1;
                for(j=0;j<(n-1)/3;j++){
                    arr[i][i-1]='O';
                    arr[i][i]='O';
                    arr[i-1][i]='O';
                    i++;
                }
            }
            else{
                if(n%2==1&&n<11){
                    cout<<"NO\n";
                    return;
                }
                if(n<8){
                    cout<<"NO\n";
                    return;
                }
                int x=n%2;
                if(x)n-=3;
                for(i=0;i<(n-2)/2;i++){
                    arr[i][0]=arr[i][2]='O';
                }
                arr[0][1]='O';
                arr[i-1][1]='O';
                if(x){
                    arr[i][2]=arr[i][3]=arr[i-1][3]='O';
                }
            }
        }
        else if(m&1){
            cout<<"NO\n";
            return;
        }
        else{
            arr[0][2]='O';
            i=1;
            for(j=0;j<(m-1)/2;j++){
                arr[i][2]='O';
                if(j&1){
                    arr[i][1]='O';
                }
                else{
                    arr[i][3]='O';
                }
                i++;
            }
            for(j=0;j<n;j++){
                arr[i][2]='O';
                i++;
            }
            arr[i][2]='O';
        }
        cout<<"YES\n";
        cout<<"200 200\n";
        for(i=0;i<200;i++) {
            cout << arr[i] << '\n';
        }
    }
    
    int main() {
    #ifdef kidw0124
        freopen("input.txt","r",stdin);
        freopen("output.txt","w",stdout);
        clock_t st=clock();
    #else
        ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    #endif
        int t=1;
    //    cin>>t;
        while(t--)solve();
    #ifdef kidw0124
        debug<<clock()-st<<"ms\n";
    #endif
    }
    

    이걸 짜는 동시에 eoaud0108가 J풀이를 찾아 구현해 AC를 받았습니다.


    J - 동전 쌍 뒤집기 [+2, 2:53:05]

    J번 풀이 펼치기/접기

    사용 알고리즘

    • 스택(Stack)
    • 그리디(Greedy)
    • 애드 혹(Ad-hoc)

    우선 T를 발견했을 때, 뒤에 HH를 가져오게 되면 HHHT가 됩니다. 즉, 이는 또다른 T와 만나야 하므로, 사이에 짝수개가 있는(이렇게 되면 홀짝성에 의해 가운데에 HH가 반드시 존재합니다) 다음 T를 발견하는 것이 중요합니다. 즉, 사이에 짝수개인, 인덱스 차이가 홀수인 T를 발견하면 스택에서 뽑고 아니면 스택에 넣는 것을 하여 최종적으로 스택이 비어있는 지를 검사하면 됩니다. $O(N)$에 풀립니다. 이는 결국 set풀이에서 depth를 준 것과 동치입니다. 답이 int범위를 넘어갈 수 있으므로 long long을 사용했습니다.

    코드

    #include<bits/stdc++.h>
    using namespace std;
    using ll = long long;
    #ifdef kidw0124
    constexpr bool ddebug = true;
    #else
    constexpr bool ddebug = false;
    #endif
    #define debug if constexpr(ddebug) cout << "[DEBUG] "
    
    void solve(){
        int i,j,k;
        int n;
        cin>>n;
        string str;
        cin>>str;
        stack<int>stk;
        ll ans=0;
        for(i=0;i<n;i++){
            if(str[i]=='T'){
                if(stk.empty() || (stk.top()-i)%2==0){
                    stk.push(i);
                }
                else{
                    ans+=i-stk.top();
                    stk.pop();
                }
            }
        }
        if(stk.size())cout<<"-1\n";
        else cout<<ans<<'\n';
    }
    
    int main() {
    #ifdef kidw0124
        freopen("input.txt","r",stdin);
        freopen("output.txt","w",stdout);
        clock_t st=clock();
    #else
        ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    #endif
        int t=1;
        cin>>t;
        while(t--)solve();
    #ifdef kidw0124
        debug<<clock()-st<<"ms\n";
    #endif
    }
    

    이후 마지막 1분을 남기고 tmdghks이 G를 짜 제출했으나 틀렸습니다.


    G - 석고 모형 만들기 [-6, -:–:–]


    대회 후 총평

    개인적인 피드백

    우선 J에서 틀린 풀이는 어쩔 수 없다 쳐도, C에서 끝을 탐색하지 않은 것, 그리고 K에서 말도 안되는 출력을 안한 것으로 패널티를 쌓은 것은 정말 하면 안되었던 것 같습니다. 대회 초반 E나 C구현할 때 엄청 머리 아프고 집중이 되지 않았었는데, 다음부터는 대회 전에 일찍 일어나 머리를 풀어 두어야 하겠습니다.

    팀 피드백

    오늘은 tmdghks가 온라인으로 같이 해 소통을 잘 하지 못했지만, 본선 때 잘 해보도록 하겠습니다.

    ]]>
    kidw0124
    [BOJ] 백준 28042 - City Folding2024-07-07T04:02:00+00:002024-07-07T04:02:00+00:00https://kidw0124.github.io/boj/2024/07/07/boj-28042

    문제 정보

      사용 알고리즘

      • 비트마스크(Bitmask)
      • 수학(Mathematics)

      문제

      처음에 아래 그림과 같이 $1$부터 $2^N$까지의 수가 일렬로 적혀있는 종이 조각이 있습니다. 이 종이 조각을 왼쪽 혹은 오른쪽 절반을 위로 접는 것을 $N$번 반복해 각 층마다 $1$개의 종이 조각씩 총 $2^N$층으로 만들었습니다.(매번 왼쪽, 오른쪽을 정할 수 있습니다) 이때, 처음에 $P$번이 아래서부터 $H$번째가 되도록 접는 방법을 찾아 출력하는 문제입니다. 아래 그림은 $2^3$개에서 $4$번이 $7$층이 되도록 LRL로 접는 방법을 나타냅니다.

      문제 제한

      • $3 \leq N \leq 60$
      • $1 \leq P, H \leq 2^N$
      • 답이 유일하게 존재함이 보장됩니다.(증명 가능합니다)

      풀이

      예제의 상황인 $N=3$, $P=4$, $H=7$을 생각해 봅시다. 또, $1$부터 $2^N$까지를 비트마스킹을 위해 $0$부터 $2^N-1$로 생각하겠습니다. 그리고 아래와 그림과 같이 빨간 글씨대로 자리의 번호를 정합니다. 이제 최종 마지막 상태에서 역으로 생각해 보면 다음을 알 수 있습니다.

      • 4번째에서 $6$번 자리였다면, 3번째에서는 $2$ 혹은 $3$번 자리입니다.
      • 3번째에서 $2$ 혹은 $3$번 자리였다면, 2번째에서는 $4$, $5$, $6$, $7$번 자리중 하나입니다.
      • 2번째에서 $4$, $5$, $6$, $7$번 자리였다면, 1번째에서는 모든 자리가 가능합니다.

      이 자리들을 보면 층수가 같습니다. 따라서 이를 층수에 따라 생각해보겠습니다. 마찬가지로 가장 아래를 $0$층이라 하겠습니다. 층수는 위에 파란 글씨로 표시해두었습니다.

      • 4번째에서 $6(110_2)$층이였다면, 3번째에서는 $1(01_2)$층입니다.
      • 3번째에서 $1(01_2)$층이였다면, 2번째에서는 $1(1_2)$층입니다.
      • 2번째에서 $1(1_2)$층이였다면, 1번째에서는 $0(0_2)$층입니다.

      또한, $i$번째 단계에서 층수가 총 $2^{i-1}$개의 층이 있는데, 이 중 절반($2^{i-2}$) 이상이라고 하면 접힌 쪽에서 접혔을 것이며, 이하라고 하면 위쪽이 접혀 올라왔습니다. 즉, 아래쪽이면 원래의 층수를 유지하며, 위쪽이면 원래 층수에서 절반에 해당하는 $2^{i-2}$을 빼준 후 비트를 뒤집어 주면 됩니다. 즉, 최상위 비트를 제거하고 비트를 flip합니다. 예를 들어 9번째의 높이가 이진수로 $11010010$의 경우 최상위 비트를 제거하면 $1010010$이 되고, 이를 flip하면 $0101101$이 8단계의 층이 됩니다. 9번째의 높이가 $01010010$일 경우 그 자리를 유지해(최상위 비트 $0$만 제거) $1010010$이 됩니다.

      이를 통해 각 단계에서의 층수를 모두 구할 수 있습니다. 이제 다시 원래에서 접어야 하는데, 우선 현재 위치가 절반보다 왼쪽인지 오른쪽인지 구한 뒤, 만약 다음 단계의 높이가 절반 이상이라면 원하는 종이조각이 있던 곳을 위로 올려야 하므로 해당 방향을, 아니면 그대로 두어야 하므로 반대 방향을 출력하면 됩니다.

      구현

      비트 플립을 구현할까 하다가 어차피 $x$자리라면 2진수로 $2^x-1$에서 빼면 됨을 통해 그냥 빼서 구현했습니다. 시프트 할 때 1<<x와 같이 하면 오버플로우가 나니까 1LL<<x혹은 1ll<<x로 해야 합니다. 또한, 사용하는 많은 변수를 64비트 정수형으로 선언해야 합니다.

      #include <bits/stdc++.h>
      using namespace std;
      typedef long long ll;
      typedef pair<ll, ll> pll;
      
      void solve() {
          ll i,j;
          ll n,p,h;
          cin>>n>>p>>h;
          p--,h--;
          vector<ll>arr(n);
          for(i=0;i<n;i++){
              j=(1LL<<(n-i-1));
              if(h&j){
                  arr[i]=1LL<<i;
                  h=(j<<1)-1-h;
              }
          }
          reverse(arr.begin(), arr.end());
          for(i=0;i<n;i++){
              j=(1LL<<(n-i-1));
              if((p&j)==arr[i]){
                  cout<<'R';
                  if(p&j) {
                      p=(j<<1)-1-p;
                  }
              }
              else{
                  cout<<'L';
                  if(p&j){
                      p^=j;
                  }
                  else{
                      p=(j-1-p);
                  }
              }
          }
      }
      
      int main() {
      #ifndef ONLINE_JUDGE
          freopen("input.txt", "r", stdin);
          freopen("output.txt", "w", stdout);
      #else
          cin.tie(0)->sync_with_stdio(0);
      #endif
          int t = 1;
          //cin >> t;
          while (t--) solve();
      }
      
      ]]>
      kidw0124
      [BOJ] 백준 28040 - Asking for Money2024-07-07T04:00:00+00:002024-07-07T04:00:00+00:00https://kidw0124.github.io/boj/2024/07/07/boj-28040

      문제 정보

        사용 알고리즘

        • 그래프(Graph)
        • 그래프 탐색(Graph Search)
        • 깊이 우선 탐색(DFS)

        문제

        $N$명의 사람들이 있고, 각자 돈을 빌릴 수 있는 두 명이 주어집니다. 이때, $N$명의 사람 중 한 명에게 외부에서 돈을 빌려달라고 요청합니다. 이제 다음을 반복합니다.

        • 만약, $i$번 사람이 돈을 빌려달라는 요청을 처음 받았다면 돈을 빌려줍니다.
        • 돈을 빌려준 이후, 자신이 돈을 빌릴 수 있는 두 명에게 돈을 빌려달라는 요청을 보냅니다.
        • 돈을 빌릴 수 있는 두 명 모두 이미 다른 사람에게 돈을 빌려달라는 요청을 받았다면, $i$번 사람은 돈을 받지 못하므로 돈을 잃습니다.
        • 이 때, 두 명 모두에게 빌려달라는 요청을 해 두 명 모두에게 돈을 받을 수도 있습니다.
        • 또한, 돈을 빌려달라는 요청이 오는 즉시 돈을 빌려주어야 하지만, 돈을 빌려달라는 요청을 보내는 순서는 상관 없습니다.

        이제, $N$명의 사람 각각에 대해 모든 경우에 있어 돈을 잃는 경우가 없다면 N을, 그렇지 않고 한 가지 경우라도 돈을 잃을 수 있다면 Y를 출력합니다.

        문제 간략화

        모든 정점의 out-degree가 $2$인 $N$개의 정점으로 이루어진 방향 그래프가 주어집니다. 모든 $i$번 정점에 대해 원하는 하나의 정점에서 시작해 원하는 순서로(이미 탐색된 정점들의 인접 정점들 중 임의로 다음 정점을 고름) 탐색을 할 때, $i$번 정점을 탐색하였을 때, $i$번 정점에서 나가는 방향으로 직접 연결된 두 정점이 이미 탐색된 상태로 할 수 있는지 판단하는 문제입니다.

        문제 제한

        • $3 \leq N \leq 1000$
        • $i$번 정점에서 나가는 다음 정점은 $i$가 아니며, 서로 다르다.

        풀이

        $i$번 정점에서 나가는 두 정점 $X_i, Y_i$에 대해 단 하나의 경우라도 $X_i, Y_i$가 모두 탐색되었는 지 보면 됩니다. 즉, $i$번 정점에 대한 최악의 경우를 생각하면, 어떤 정점에서 탐색을 시작하여 최대한 늦게 마지막으로 $i$번 정점을 탐색하였을 때, $i$번 정점에서 나가는 두 정점이 모두 탐색되게 할 수 있는 지 판단하면 됩니다.

        즉, 이는 다음과 같은 조건으로 바뀌게 됩니다.

        모든 $i$번 정점에 대해 순서대로 다음 조건을 만족하는 $v_i\ne i$가 존재하면 Y를, 그렇지 않으면 N을 출력합니다.

        1. $v_i$에서 출발해서 $i$번 정점을 탐색할 수 있어야 합니다.
        2. $v_i$에서 출발해서 $i$번 정점을 거치지 않고 $X_i$와 $Y_i$를 모두 탐색할 수 있어야 합니다.

        이를 나이브하게 구현한다면 $O(N)$개의 정점에 대해, $O(N)$개의 출발 정점을 설정하여 $O(N)$의 DFS 혹은 BFS를 수행해야 하므로, $O(N^3)$의 시간복잡도를 가지게 됩니다. 그러나 시간 제한이 $0.5$초이므로 시간 복잡도를 줄여야 합니다.

        정점 $a$에서 출발하여 $b$번 정점을 거치지 않고 $c$에 도착할 수 있는 지를 판단하는 것은 역방향 간선 그래프에서, $c$번 정점에서 출발하여 $b$번 정점을 거치지 않고 $a$에 도착할 수 있는 지를 판단하는 것과 동일합니다. $c$와 $b$가 정해져 있다면, 탐색 가능한 모든 $a$를 $O(N)$의 시간 복잡도에 한번에 찾을 수 있습니다.

        즉, 위의 조건을 역방향 그래프 상에서 다음 동치 명제로 바꿀 수 있습니다.

        모든 $i$번 정점에 대해 순서대로 다음 조건을 만족하는 $v_i\ne i$가 존재하면 Y를, 그렇지 않으면 N을 출력합니다.

        1. $i$번 정점에서 출발하여 $v_i$에 도착할 수 있어야 합니다.
        2. $X_i$에서 출발해서 $i$번 정점을 거치지 않고 $v_i$에 도착할 수 있어야 합니다.
        3. $Y_i$에서 출발해서 $i$번 정점을 거치지 않고 $v_i$에 도착할 수 있어야 합니다.

        즉, 역방향 간선 그래프를 만들어, $i$번 정점에 대하여 $i$, $X_i$, $Y_i$에 대해 DFS를 수행하여 얻은 세개의 정점집합의 교집합이 공집합이 아니라면 Y를, 그렇지 않으면 N을 출력하면 됩니다. $O(N)$개의 정점에 대해, $3$개의 출발 정점을 설정하여 $O(N)$의 DFS 혹은 BFS를 수행해야 하므로, $O(N^2)$의 시간복잡도에 문제를 해결할 수 있습니다.

        구현

        먼저 그래프를 받고 역방향 그래프를 만들었습니다. 이후 시작 정점과 제외할 정점을 인자로 받아, 시작 정점에서 출발하여 제외할 정점을 제외하고 도달할 수 있는 정점들을 반환하는 dfs 함수를 작성하였습니다. 제외할 정점이 없을 때는 -1을 인자로 넘겨주었습니다.

        #include <bits/stdc++.h>
        using namespace std;
        
        void solve() {
            int i,j;
            int n;
            cin>>n;
            vector<pair<int,int>>grp(n+1);
            vector<vector<int>>rgrp(n+1);
            string ans;
            for(i=1;i<=n;i++){
                cin>>grp[i].first>>grp[i].second;
                rgrp[grp[i].first].push_back(i);
                rgrp[grp[i].second].push_back(i);
            }
            function<vector<bool>(int,int)> dfs=
                    [&rgrp,&n](int s, int inter)->vector<bool>{
                stack<int>stk;
                stk.push(s);
                vector<bool>vis(n+1);
                vis[s]=1;
                while(stk.size()){
                    int now=stk.top();
                    stk.pop();
                    for(auto k:rgrp[now]){
                        if(vis[k]||k==inter)continue;
                        else{
                            vis[k]=1;
                            stk.push(k);
                        }
                    }
                }
                return vis;
            };
            for(i=1;i<=n;i++){
                auto vis1=dfs(grp[i].first,i);
                auto vis2=dfs(grp[i].second,i);
                auto vis3=dfs(i,-1);
                for(j=1;j<=n;j++){
                    if(vis1[j]&&vis2[j]&&vis3[j]){
                        ans+='Y';
                        break;
                    }
                }
                if(j==n+1)ans+='N';
            }
            cout<<ans;
        }
        
        int main() {
        #ifdef kidw0124
            freopen("input.txt", "r", stdin);
            freopen("output.txt", "w", stdout);
        #else
            cin.tie(0)->sync_with_stdio(0);
        #endif
            int t = 1;
            //cin >> t;
            while (t--) solve();
        }
        
        ]]>
        kidw0124
        AtCoder ABC 3602024-06-30T21:00:00+00:002024-06-30T21:00:00+00:00https://kidw0124.github.io/atcoder/abc/2024/06/30/abc360

        서론

        바로 전날에 현대모비스 1차와 팀연습을 좋지 않게 마무리하고 Atcoder를 시작했습니다.

        Contest

        A - A Healthy Breakfast[+, 1:21]

        사용 알고리즘

        • 구현(Implementation)
        • 문자열(String)

        문제 상황

        R, M, S 가 하나씩 있는 길이 $3$의 문자열이 주어질 때, RM보다 먼저인지 검사하는 문제입니다.

        풀이

        abc A번 치고는 구현할 것이 있는(?) 문제였습니다. 매번 어떤 문자열과 같은지 비교하시오 이런거 나오다가 R의 위치와 S의 위치를 찾아야 하는 문제였습니다. string::find를 사용하여 간단히 풀 수 있었습니다.

        void solve(){
            ll i,j,k;
            string str;
            cin>>str;
            if(str.find('R')<str.find('M')){
                yes();
            }
            else{
                no();
            }
        }
        

        B - Vertical Reading[+, 5:21]

        사용 알고리즘

        • 구현(Implementation)
        • 문자열(String)
        • 브루트포스(Brute Force)

        문제 상황

        두개의 문자열 $S$와 $T$가 주어집니다. $S$를 길이 $w$씩 잘라서 이차원으로 적고 이를 세로로 읽을 때, 한 열이라도 $T$와 같은 문자열이 있는지 확인하는 문제입니다. 이 때, $w$는 $S$의 길이 \textbf{미만}입니다. 대회 끝나고 알았는데, $\lvert T\rvert < \lvert S\rvert$라는 조건이 처음에 적혀있었으나, 실제로는 $\lvert T\rvert\le \lvert S\rvert$였습니다. 이 이슈가 1시간 넘게 지난 후 수정되었고, 이것 때문에 WA받은 사람이 있어 어떻게 처리할 지 atcoder가 고민하고 있고, Rating 반영이 많이 늦게 되었습니다. 저는 예제를 보고 이해한지라 해당 이슈가 있는 지 몰랐습니다.

        풀이

        문자열 길이가 $100$이므로 가능한 모든 경우를 해줍니다. $O(\lvert S \rvert^2)$에 해결가능합니다.

        void solve(){
            ll i,j,k;
            string s,t;
            cin>>s>>t;
            ll n=s.size();
            for(i=1;i<n;i++){
                vector<string> vct(i);
                for(j=0;j<n;j++){
                    vct[j%i]+=s[j];
                }
                if(find(all(vct),t)!=vct.end()){
                    yes();
                    return;
                }
            }
            no();
        }
        

        C - Move It[+, 8:06]

        사용 알고리즘

        • 구현(Implementation)
        • 정렬(Sort)

        문제 상황

        $N$개의 물건이 있고, 각각의 무게와 어떤 상자에 들어있는지 주어집니다. 각 상자별로 물건을 한 개만 남기고 다 빼고 싶을 때, 빼야 하는 무게의 합을 구하는 문제입니다.

        풀이

        그냥 각 상자별로 무게를 정렬하고, 가장 무거운 것을 제외한 나머지를 더해주면 됩니다. $O(N\log N)$에 해결가능합니다. 개인적으로 B번보다 쉬운 것 같습니다.

        굳이 정렬을 하지 않아도, 각 상자별로 가장 무거운 것을 합한 후 전체 합에서 빼주면 $O(N)$에 해결가능합니다.

        void solve(){
            ll i,j,k;
            ll n;
            cin>>n;
            vector<ll> arr(n),w(n);
            vector<vector<ll>>rarr(n+1);
            for(i=0;i<n;i++){
                cin>>arr[i];
            }
            for(i=0;i<n;i++){
                cin>>w[i];
            }
            for(i=0;i<n;i++){
                rarr[arr[i]].pb(w[i]);
            }
            for(i=1;i<=n;i++){
                sort(all(rarr[i]));
            }
            ll ans=0;
            for(i=1;i<=n;i++){
                for(j=0;j+1<rarr[i].size();j++){
                    ans+=rarr[i][j];
                }
            }
            cout<<ans<<'\n';
        }
        

        D - Ghost Ants[+, 14:37]

        사용 알고리즘

        • 정렬(Sort)
        • 덱(Deque)

        문제 상황

        개미들이 일차원 수직선 위 서로 다른 위치에서 왼쪽 혹은 오른쪽을 보고 있습니다. $1$초에 $1$만큼 움직일때, $T+0.1$초 후 만나는 개미 쌍의 수를 구하는 문제입니다. 개미는 서로 만나더라도 서로 통과해 지나갑니다.

        풀이

        왼쪽을 보는 개미와 오른쪽을 보는 개미는 서로 만날 수 있지만, 서로 같은 방향을 보는 개미는 만나지 않습니다. 우선 예제는 좌표가 다 정렬되어 주어지지만, 실제로는 정렬되지 않은 데이터가 주어지기에 좌표를 먼저 정렬합니다. 이후 좌표가 증가하는 순서대로 개미를 보며, 오른쪽을 보는 개미라면 덱에 넣어주고, 왼쪽을 보는 개미라면 현재 덱에 들어있는 가장 왼쪽의 개미(덱의 front)가 $2T$이하의 거리에 있도록 pop_front를 해줍니다. 그러면 덱에는 현재 좌표에서 $2T$이내에 있는 오른쪽을 보는 개미들만 들어있게 됩니다. 즉, 덱의 크기를 더해주면 됩니다. 정렬에 $O(N\log N)$, 이후 $O(N)$에 해결가능합니다.

        void solve(){
            ll i,j,k;
            ll n,m;
            string str;
            cin>>n>>m>>str;
            vector<ant> ants(n);
            for(i=0;i<n;i++){
                cin>>ants[i].x;
                ants[i].dir=str[i]-'0';
            }
            sort(all(ants));
            ll ans=0;
            deque<ll>lef;
            for(i=0;i<n;i++){
                if(ants[i].dir==1){
                    lef.push_back(ants[i].x);
                }
                else{
                    while(lef.size() && ants[i].x-lef.front()>2*m){
                        lef.pop_front();
                    }
                    ans+=lef.size();
                }
            }
            cout<<ans<<'\n';
        }
        

        E - Random Swaps of Balls[+, 26:26]

        사용 알고리즘

        • 확률론(Probability)
        • 기댓값의 선형성(Linearity of Expectation)
        • 모듈로 곱셈 역원(Modular Inverse)

        문제 상황

        $N$개의 공이 있고, $1$번공은 검정색, 나머지는 흰색입니다. $1$이상 $N$이하의 범위에서 Uniform하게 $i$와 $j$를 독립적으로 선택해 두 공을 바꾸는 작업을 $K$번 수행할 때, 최종적으로 검정색 공이 있는 번호의 기댓값을 구하는 문제입니다.

        풀이

        현재 $x$번에 검정색 공이 있다고 가정하면, 한 번의 작업에 대해 케이스를 네 가지로 나눌 수 있습니다.

        1. $i\ne x$, $j\ne x$: 이 경우에는 $x$번 공이 그대로 있으므로 변화가 없습니다.
        2. $i=x$, $j\ne x$: 이 경우에는 $x$번 공이 $j$번 공으로 이동합니다.
        3. $i\ne x$, $j=x$: 이 경우에는 $i$번 공이 $x$번 공으로 이동합니다.
        4. $i=x$, $j=x$: 이 경우에는 변화가 없습니다.

        위의 각각을 살펴보면 1번의 경우 $\frac{(N-1)^2}{N^2}$의 확률로 발생합니다. $y\ne x$에 대해 한 번의 작업 후 검정색 공이 $y$번으로 바뀔 확률은 2, 3번 각각 1가지 경우가 있으므로, $\frac{2}{N^2}$입니다. $x$번에 유지될 확률은 1번 혹은 4번의 경우이므로, $\frac{(N-1)^2+1}{N^2}$입니다.

        그런데, $x$번에 유지될 확률을 다르게 해석하여 $\frac{N^2-2N}{N^2}+\frac{2}{N^2}$로 생각하게 되면, 확률/기댓값의 선형성에 따라 다음 두가지 케이스로 해석할 수 있습니다.

        1. 검정색 공이 $1$번부터 $N$번까지의 번호에 대해 동일한 $\frac{2}{N^2}$의 확률로 이동합니다. 여기에는 원래 검정공의 위치도 포함됩니다.
        2. $\frac{N^2-2N}{N^2}$의 확률로 공이 $x$번에 유지됩니다.

        만약 $K$번의 작업 중 1번이 한번이라도 일어난다면, 모든 번호에 대해 uniform해지기 때문에, 검정색 공이 최종적으로 $i$번의 위치에 있을 확률은 모든 $i$에 대해 $\frac{1}{N}$으로 동일합니다. 즉, 이 경우 기댓값은 $\frac{N+1}{2}$입니다.

        만약 $K$번의 작업 중 단 한번도 1번이 일어나지 않으면, 검정색공은 $1$번에 유지됩니다. 이 경우 기댓값은 $1$입니다.

        $K$번의 작업 중 단 한번도 1번이 일어나지 않을 확률은 $(\frac{N^2-2N}{N^2})^K$입니다. 즉, 최종적으로 기댓값은 $1\cdot(\frac{N^2-2N}{N^2})^K+\frac{N+1}{2}\cdot(1-(\frac{N^2-2N}{N^2})^K)$입니다. 모듈로 역원을 $O(\lg P)$에 구할 수 있으므로, $O(\lg P)$에 해결가능합니다. AtCoder에서 제공하는 ACL을 처음으로 사용해본 문제였습니다.

        + 이 문제의 더 쉬운 풀이로 $O(K)$ DP가 있습니다. 제한을 보고 DP인가 했지만 DP보다 위의 풀이가 먼저 생각나 이렇게 풀었습니다. 사실 대회 끝나고 changhw(now_cow)의 코드를 보기 전까지 DP 풀이는 생각나지 않았습니다.

        void solve(){
            ll i,j,k;
            ll n,m;
            cin>>n>>m;
            ll q=n*n-2*n,p=n*n;
            q%=mod,p%=mod;
            ll prob=q*inv_mod(p,mod)%mod;
            prob=pow_mod(prob,m,mod);
            ll rprob=(1-prob+mod)%mod;
            ll ans=0;
            ans=prob+(1+n)%mod* inv_mod(2,mod)%mod*rprob%mod;
            ans%=mod;
            cout<<ans<<'\n';
        }
        

        F - InterSections[-]

        사용 알고리즘

        • 세그먼트 트리(Segment Tree)
        • 이분탐색(Binary Search)

        문제 상황

        두 구간이 \texttt{겹친다}는 것은 두 구간이 abab형태로 exclusive하게 겹친다는 것을 의미합니다. $N$개의 구간이 주어질 때 최대한 많은 구간과 겹치도록 하는 정수 구간을 만드는 문제입니다. 만약 그런 구간이 많다면 $(l,r)$의 사전순으로 가장 작은 구간을 출력합니다.

        풀이

        가능한 답의 두 끝점은 주어진 구간들의 $l+1$ 혹은 $r+1$ 중 있습니다. 따라서 좌표압축을 시킨 뒤 정렬하고, 원하는 범위에 +, - 시키고 원하는 범위의 최댓값을 찾는 세그를 만든 뒤 우선 모든 구간의 범위를 + 시켜두고, 이후 구간을 정렬해 sweeping하면서 빼주고 하면 됩니다. 대회 끝나고 여러번 제출해봤는데, 아직도 왜틀렸는지 모르겠습니다.

        풀이는 에디토리얼과 같고, 심지어 맞은 사람의 코드와 코드도 거의 동일한데 구현을 뭔가 lower_bound, upper_bound나 +1 할 때 안할 때 이런 구분을 잘못한 것 같습니다. $O(N\log N)$에 해결가능합니다.

        실제 대회 때는 E푼시점에 F 푼사람 10 언더, G 푼사람 20정도여서 G로 바로 갔습니다.

        G - Suitable Edit for LIS[+, 51:42]

        사용 알고리즘

        • LIS(Longest Increasing Subsequence)

        문제 상황

        수열이 주어질때, 원하는 수 하나를 바꾸어서 LIS(Longest Increasing Subsequence)의 길이를 최대로 만드는 문제입니다.

        풀이

        수를 하나 바꾸기 때문에 답은 원래 수열의 LIS의 길이 혹은 +1입니다. 그럼 언제 LIS길이 +1이 답이 되는지 살펴보면 다음 경우입니다.

        1. 첫번째 혹은 마지막 수 중 하나를 포함하지 않고 LIS를 구했을 때, 원래 수열의 LIS와 길이가 같다면 포함하지 않은 수를 $-\infty$혹은 $\infty$로 바꾸면 +1이 됩니다.
        2. LIS를 적었을 때, LIS의 연속된 두 수의 차이가 $2$이상이고 사이에 다른 수가 있다면, 그 사이에 있는 수를 작은수+1로 바꾸면 +1이 됩니다.

        즉, 각각 구해주면 됩니다. 1번 경우는 LIS를 한번씩 더구하면 되고, 2번 경우는 이제 여러 LIS가 있을 수 있는데, 다음과 같이 구하면 됩니다.

        • 먼저, 가능한 LIS 중 최대한 수들이 가장 작은 수열을 구합니다.
        • 이 수를 원래 수열과 인덱스를 매칭시키는데, 이때 인덱스가 가장 작은 수열을 구합니다.
        • 해당 수열에서 검사한 후, 반대로 최대-최대가 되도록 해줍니다.
        • 저는 이 경우는 원래 수열을 뒤집고 음수로 바꾼 다음 위의 과정을 똑같이 진행했습니다.

        위의 두 가지 경우(최소-최소, 최대-최대)를 검사하면 되는데, 간단한 증명은 다음과 같습니다.

        • 두 LIS a-b-c와 a-b’-c에 대해
        • b와 b’간 대소가 있어 일반성을 잃지 않고 $b<b’$이라고 합시다.
          • 그렇다면 $a<b<b’<c$가 되고, a와 b’, 그리고 b와 c는 각각 $2$ 이상 차이가 납니다.
          • 만약 a와 b’사이에 수가 있다면 해당 수를 a+1로 바꿀 수 있습니다.
          • 만약 b와 c사이에 수가 있다면 해당 수를 c-1로 바꿀 수 있습니다.
          • 결국, 둘 모두 캡쳐되지 않으려면 a-b’-b-c로 연속된 경우임을 알 수 있습니다.
          • 이 경우를 제외하면, 최소-최소, 최대-최대 둘 모두 캡쳐됩니다.
        • $b=b’$인 경우를 봅시다.
          • 그렇다면 $a<b=b<c$인 a-b-b’-c가 됩니다.
          • 만약 $a+1=b=c-1$이라면 +1되지 않으므로 문제 없습니다.
          • 만약 $a+2\le b$라면 최대-최대에서 LIS가 a-b’-c가 되므로 a-(b->a+1)-b’-c를 잡을 수 있습니다.
          • 만약 $b\le c-2$라면 최소-최소에서 LIS가 a-b-c가 되므로 a-b-(b’->c-1)-c+1를 잡을 수 있습니다.

        LIS만 잘 구하고 역추적만 잘하면 되므로 $O(N\log N)$에 해결가능합니다.

        // 대회 당시 짰던 코드가 너무 더러워서 다시 짰습니다.
        void solve(){
            int i,j,k;
            int n;
            cin>>n;
            vector<int>arr(n);
            for(i=0;i<n;i++)cin>>arr[i];
            // find LIS with O(nlogn) with smallest sequence
            auto lis=[](const vector<int>&arr)->vector<int>{
                vector<int>lis;
                vector<int>idx;
                vector<int>prev(arr.size(),-1);
                for(int i=0;i<arr.size();i++){
                    auto it=lower_bound(all(lis),arr[i]);
                    if(it==lis.end()){
                        lis.push_back(arr[i]);
                        idx.push_back(i);
                        if(idx.size()>1){
                            prev[i]=idx[idx.size()-2];
                        }
                    }
                    else{
                        *it=arr[i];
                        idx[it-lis.begin()]=i;
                        if(it!=lis.begin()){
                            prev[i]=idx[it-lis.begin()-1];
                        }
                    }
                }
                vector<int>ret;
                int i=idx.back();
                while(i!=-1){
                    ret.push_back(arr[i]);
                    i=prev[i];
                }
                reverse(all(ret));
                return ret;
            };
            auto check1=[&lis](const vector<int>&arr)->int{
                vector<int>brr=lis(arr);
                if(lis(vector<int>(arr.begin()+1,arr.end())).size()==brr.size()){
                    return brr.size()+1;
                }
                else if(lis(vector<int>(arr.begin(),arr.end()-1)).size()==brr.size()){
                    return brr.size()+1;
                }
                return 0;
            };
            auto check2=[&lis](const vector<int>&arr)->int{
                vector<int>brr=lis(arr);
                vector<int> lis_idx;
                int now = 0,i;
                for (i = 0; i < brr.size(); i++) {
                    while (arr[now] != brr[i])now++;
                    lis_idx.push_back(now);
                    now++;
                }
                for (i = 0; i < brr.size() - 1; i++) {
                    if (brr[i] + 1 == brr[i + 1]) {
                        continue;
                    } else if (lis_idx[i] + 1 == lis_idx[i + 1]) {
                        continue;
                    } else {
                        return brr.size() + 1;
                    }
                }
                return 0;
            };
            int ans;
            if(n>1&&(ans=check1(arr))){
                cout<<ans<<'\n';
            }
            else if(ans=check2(arr)){
                cout<<ans<<'\n';
            }
            else{
                reverse(all(arr));
                for(auto&x:arr)x=-x;
                if(ans=check2(arr)){
                    cout<<ans<<'\n';
                }
                else{
                    cout<<lis(arr).size()<<'\n';
                }
            }
        }
        

        총평

        개인적 평가

        전날 쳤던 모든 대회/연습이 망해서 걱정했으나 2400퍼폼에 전체 76등이라는 준수한 성적을 거두어 기분이 좋습니다. 두라운드 연속해서 2000+ 퍼폼인 만큼 유지해보도록 해야겠습니다. 아직도 F는 왜틀렸는지 모르겠는데, 혹시 제 코드들 보고 틀린 부분이 있으면 알려주시면 감사하겠습니다.

        해당 라운드 평가

        B번 문제에 문제가 있어(…) 조금 그런 대회 셋이였습니다. 그걸 차치하고 보더라도 개미 문제나 LIS등 웰노운 문제들을 살짝 변형한 문제들이 많아 아쉽습니다. G가 너무 쉬웠고, 그 중간에 확률문제는 DP말고 수학으로 푸는 버전으로 나왔다면(물론 그럼 F 갈 확률이 높지만) 합니다.

        ]]>
        kidw0124
        AtCoder ABC 3592024-06-22T21:00:00+00:002024-06-22T21:00:00+00:00https://kidw0124.github.io/atcoder/abc/2024/06/22/abc359

        서론

        지난주 토요일 ABC 358을 망치고 첫 atcoder였습니다. 이번주 월요일 연습때 폼이 나쁘지 않아 걱정반 기대반으로 시작했습니다.

        Contest

        A - Count Takahash[+, 1:01]

        사용 알고리즘

        • 구현(Implementation)
        • 문자열(String)

        문제 상황

        $N$개의 문자열이 주어지고 이 중 Takahashi의 개수를 세는 문제입니다. 대회 후 이 글을 작성 중에 알았는데, 모든 문자열은 Takahashi 혹은 Aoki입니다.

        풀이

        항상 그렇듯 빠르게 해석하고 구현하는 것이 관건입니다. count함수를 사용하여 Takahashi의 개수를 세었습니다.

        void solve(){
            int i,j,k;
            ll n;
            cin>>n;
            vector<string>str(n);
            for(i=0;i<n;i++)cin>>str[i];
            cout<<count(str.begin(), str.end(),"Takahashi");
        }
        

        B - Couples[+, 3:55]

        사용 알고리즘

        • 구현(Implementation)

        문제 상황

        $1$부터 $N$까지 각각 두 개 씩 총 $2N$개의 정수가 주어지고, 이 중 같은 정수 사이에 다른 수가 정확히 한 개 있는 정수의 개수를 구하는 문제입니다.

        풀이

        들어오는 수의 위치 두개를 저장해 $2$가 차이나는 지 확인하면 됩니다. 처음에 $N$개의 정수만 입력받아 로컬에서 RTE가 나와 이를 찾아 수정하느라 3분정도 소요되었습니다.

        void solve(){
            int i,j,k;
            ll n;
            cin>>n;
            vector<vector<ll>>arr(n+1);
            for(i=0;i<n*2;i++){
                cin>>j;
                arr[j].pb(i);
            }
            ll cnt=0;
            for(i=1;i<=n;i++){
                if(arr[i][0]+2==arr[i][1])cnt++;
            }
            cout<<cnt;
        }
        

        C - Tile Distance 2[+1, 12:43]

        사용 알고리즘

        • 구현(Implementation)
        • 많은 조건 분기(Case-Work)

        문제 상황

        아래와 같은 그림에서 출발점과 도착점이 블럭 내부에 주어질 때 최소로 블록을 바꾸어 도착점까지 이동하는 문제입니다.

        풀이

        범위가 $2\times 10^{16}$이라 직접하는 것은 안됩니다.

        가로와 세로 중 어디가 더 긴 지 확인하고, 일단 대각선으로 이동하여, 가로가 크다면 마지막에 두 칸 씩, 세로가 크다면 한 칸 씩 이동하는 것이 최적이라는 것을 알 수 있습니다.

        void solve(){
            int i,j,k;
            ll a,b,c,d;
            cin>>a>>b>>c>>d;
            ll dx=abs(c-a),dy=abs(d-b);
            if(dy>=dx){
                cout<<dy;
            }
            else{
                ll ans=dy;
                dx-=dy;
                if(a<c){
                    if((a+b)%2==1){
                        ans+=(dx+1)/2;
                    }
                    else{
                        ans+=dx/2;
                    }
                }
                else{
                    if((a+b)%2==0){
                        ans+=(dx+1)/2;
                    }
                    else{
                        ans+=dx/2;
                    }
                }
                cout<<ans;
            }
        }
        

        + 대회 후 changhw(now_cow)의 코드를 봤는데 깔끔하게 잘 짜서 첨부합니다

        int main() {
            fastio;
            ll sx, sy, tx, ty;
            cin >> sx >> sy >> tx >> ty;
            if((sx + sy) % 2 == 0) sx++;
            if((tx + ty) % 2 == 0) tx++;
            cout << abs(sy - ty) + max(0LL, (abs(sx - tx) - abs(sy - ty) + 1)/2);
            return 0;
        }
        

        D - Avoid K Palindrome[+, 22:08]

        사용 알고리즘

        • DP(Dynamic Programming)
        • 비트마스크(Bitmask)

        문제 상황

        길이 $N$의 A, B, ?로만 이루어진 문자열과 $K$가 주어지고 ?A 혹은 B를 넣을 때, 길이 $K$의 substring이 palindrome이 되지 않도록 하는 문자열의 개수를 구하는 문제입니다.

        풀이

        $K$의 범위가 $10$이하이고, $N$이 $1000$이하이므로, $N\times 2^K$ DP로 풀 수 있습니다.

        i번째 문자까지 고려하고, 마지막 $K$개의 문자를 A0, B1이라 생각할 때의 비트마스크를 j라고 할 때 조건을 만족하는 경우의 수를 DP[i][j]로 놓고 구할 수 있습니다.

        첫 $K-1$개의 문자에 대해서는 조건을 항상 만족하므로 별도의 처리 없이 진행하고, 이후에는 j의 비트마스크가 팰린드롬이 아닐 때만 DP[i][j]를 갱신합니다. 만약 팰린드롬이라면 0을 넣어줍니다. (i,j)에서 갈 수 있는 상태가 (i+1,x)라고 하면 xj의 첫 비트를 지우고, 비트를 하나씩 시프트 시키고, 마지막 비트만 0 혹은 1로 설정해주면 됩니다. 즉, $N\times 2^K$가지의 상태에 대해 각각 $2$개 씩 전파해주면 됩니다.

        미리 팰린드롬을 모두 구해둔다면, $O(N\times 2^K)$로 풀 수 있습니다. 매번 구한다면 $O(NK\times 2^K)$도 시간 내에 들어갈 것 같습니다. 아래 코드에서는 $K$대신 m을 사용하였습니다.

        void solve(){
            int i,j,k;
            ll n,m;
            cin>>n>>m;
            string str;
            cin>>str;
            vector<vector<ll>> dp(n+1,vector<ll>(1<<m,0));
            vector<bool> isok(1<<m,true);
            for(i=0;i<(1<<m);i++){
                vector<int> cnt(m,0);
                for(j=0;j<m;j++){
                    if(i&(1<<j)){
                        cnt[j]++;
                    }
                }
                vector<int> cnt2=cnt;
                reverse(all(cnt2));
                if(cnt==cnt2)isok[i]=false;
            }
            dp[0][0]=1;
            for(i=1;i<=m-1;i++){
                for(j=0;j<(1<<m);j++){
                    if(str[i-1]!='B'){
                        ll nex=(j<<1)&((1<<m)-1);
                        dp[i][nex]+=dp[i-1][j];
                        dp[i][nex]%=mod;
                    }
                    if(str[i-1]!='A'){
                        ll nex=(j<<1)&((1<<m)-1)|1;
                        dp[i][nex]+=dp[i-1][j];
                        dp[i][nex]%=mod;
                    }
                }
            }
            for(i=m;i<=n;i++){
                for(j=0;j<(1<<m);j++){
                    if(str[i-1]!='B'){
                        ll nex=(j<<1)&((1<<m)-1);
                        if(isok[nex]) {
                            dp[i][nex] += dp[i - 1][j];
                            dp[i][nex] %= mod;
                        }
                    }
                    if(str[i-1]!='A'){
                        ll nex=(j<<1)&((1<<m)-1)|1;
                        if(isok[nex]) {
                            dp[i][nex] += dp[i - 1][j];
                            dp[i][nex] %= mod;
                        }
                    }
                }
            }
            ll ans=0;
            for(i=0;i<(1<<m);i++){
                ans+=dp[n][i];
                ans%=mod;
            }
            cout<<ans<<'\n';
        }
        

        E - Water Tank[+, 33:17]

        사용 알고리즘

        • 스택(Stack)

        문제 상황

        $N+1$개의 수조가 순서대로 붙어 있고, $i$번과 $i+1$번 사이의 벽의 높이 $H_i$가 주어집니다. 각 수조의 밑면적은 같고, $1$초에 한 번씩 $0$번 수조에 높이 $1$의 물을 채웁니다. 물은 벽을 넘어가면 다음 수조로 흘러 들어갑니다. $1$번 부터 $N$번까지의 각 수조에 물이 언제 처음 넘어 들어가는 지 구하는 문제입니다.

        풀이

        언제 물이 처음으로 넘어가는 지를 예제를 보며 관찰하다 보면 다음 사실을 알 수 있습니다.

        • 뒤쪽 벽이 앞쪽 벽보다 더 높다면, 앞쪽 수조도 앞쪽 벽의 높이와 상관없이 뒤쪽 벽의 높이 만큼 물이 차야 넘어갈 수 있다.
        • 앞쪽 벽이 뒤쪽 벽보다 더 높다면, 앞쪽 수조를 가득 채운 후 뒤쪽 벽의 높이만큼 물이 차야 넘어갈 수 있다. 즉, 앞쪽 벽이 더 높고, 뒤쪽 수조에 물이 들어가게 되면, 이후 넘어오는 물은 뒤쪽 수조로 넘어가게 된다.

        이를 통해 물이 걸리는 벽들의 높이는 감소함을 알 수 있습니다. LIS 구할 때와 마찬가지로 스택에 높이가 감소하도록 유지시키며, 넣어주면 됩니다. 각각의 벽에 대해 순차적으로 다음을 해줍니다.

        • 만약 스택이 비었거나, top의 높이가 현재 벽보다 높다면, top의 인덱스(비어있다면 0)과 현재 벽의 인덱스의 차이 만큼의 길이를 현재 벽의 높이만큼의 물로 채워야 하므로, 직전부터 현재 벽까지 채우는데 걸리는 시간을 계산해줍니다. 이후 현재 벽의 높이와 인덱스, 채우는데 걸리는 시간을 넣어줍니다.
        • 그렇지 않다면 top을 pop하고 반복합니다.

        이를 반복하면 $i$번 벽까지 채우는데 걸리는 시간이 마지막에 저장되어 있을 것이므로, 여기에 $+1$해 출력하면 됩니다.

        스택에 원소가 들어갈 때, 나갈 때 보여지고, top만 참조하므로 $O(N)$에 풀 수 있습니다.

        void solve(){
            int i,j,k;
            ll n,m;
            cin>>n;
            vector<ll> arr(n);
            for(auto &x:arr)cin>>x;
            stack<tlll> st;
            for(i=0;i<n;i++){
                while(st.size()){
                    auto [h,idx,sum]=st.top();
                    if(h>arr[i]){
                        st.push({arr[i],i,sum+arr[i]*(i-idx)});
                        break;
                    }
                    else{
                        st.pop();
                    }
                }
                if(st.empty()){
                    st.push({arr[i],i,arr[i]*(i+1)});
                }
                cout<<get<2>(st.top())+1<<" ";
            }
        }
        

        F - Tree Degree Optimization[+, 40:36]

        사용 알고리즘

        • 정렬(Sort)
        • 그리디(Greedy)
        • 우선순위 큐(Priority Queue)

        문제 상황

        $N$개의 양의 정수 $A_1,A_2,\cdots,A_N$이 주어지고, $N$개의 정점을 가진 트리를 만들어야 합니다. $i$번 정점의 차수가 $d_i$라고 할 때, $\sum_{i=1}^{N}d_i^2A_i$의 값을 최소로 하도록 트리를 구성하는 문제입니다.

        풀이

        만약 차수가 모두 정해져 있다고 합시다. 이 때 $\sum_{i=1}^{N}d_i^2A_i$의 값을 최소로 하려면, 재배열 부등식에 의해 $d_i$와 $A_i$는 서로 정렬해 역순으로 매칭시켜야 합니다. 즉, 이는 결국 $A_i$가 작은 것이 최대한 많은 정점에 연결되어 있어야 하며, $A_i$가 큰 것끼리 연결될 필요가 없으므로, $A_i$를 오름차순으로 정렬하여 하나의 트리에 연결해나가는 것이 최적임을 의미합니다.

        그렇다면 주어진 $A_i$를 오름차순으로 정렬하고, 해당 순서대로 $1$, $2$, $\cdots$, $N$번 정점으로 재배열 합니다. 이 상황에서 $i$번 정점을 삽입한다고 가정하면, $i$번 정점이 $j$번 정점과 연결된다고 가정할 때, $d_j$가 $1$ 증가하고, $d_i$가 1이 됩니다. 즉, 값은 $((d_j+1)^2-d_j^2)\cdot A_j+A_i=(2d_j+1)\cdot A_j+A_i$만큼 증가합니다.

        즉, $(2d_j+1)\cdot A_j$가 가장 작은 정점에 그리디하게 연결해 주는 것이 최적임을 추측할 수 있습니다. 만약 다른 정점에 연결된다면, 더 큰 값을 가져가게 될 것입니다. 이 경우가 이후에 더 이득을 볼 수 있는 지 검사하면 $2d_j+1$은 증가함수이므로 점점 추가되는 값이 커지므로 그럴 수 없습니다.

        만약 $(2d_j+1)\cdot A_j=(2d_k+1)\cdot A_k$인 경우가 있고, 이 경우가 최소라고 하더라도 어떤 걸 선택하든 다음 선택에서 다른 것을 선택할 것이므로, 신경쓰지 않아도 됩니다.

        이를 우선순위 큐를 사용하여 구현하면 됩니다. 우선순위 큐에는 추가되는 값과 현재 차수를 저장하고, 가장 작은 값을 가진 정점을 빼서 연결해주면 됩니다.

        void solve(){
            int i,j,k;
            ll n,m;
            cin>>n;
            vector<ll> arr(n);
            for(auto &x:arr)cin>>x;
            sort(all(arr));
            priority_queue<pll,vector<pll>,greater<pll>> pq;
            ll ans=0;
            pq.push({arr[0],0});
            for(i=1;i<n;i++){
                auto [val,cnt]=pq.top();
                pq.pop();
                ll ai=val/((cnt+1)*(cnt+1)-cnt*cnt);
                ans+=val+arr[i];
                ll nex=ai*((cnt+2)*(cnt+2)-(cnt+1)*(cnt+1));
                pq.push({nex,cnt+1});
                pq.push({arr[i]*3,1});
            }
            cout<<ans<<'\n';
        }
        

        G - Sum of Tree Distance[-]

        TODO

        총평

        개인적 평가

        F까지는 빠르게 밀었지만, G 풀이를 20분 정도만에 생각하고 구현했지만, 예제가 나오지 않아 디버깅 하는 과정에서 시간이 종료되었습니다. 이후 찾아보니까 ETT+트리압축 혹은 Small to Large를 사용하면 풀 수 있는 문제였습니다. 저는 전자로 생각하고 풀었는데 트리압축이라는 개념을 처음 보았는데, 이런 문제를 풀어봤다면 더욱 빠르게 풀 수 있었을 것 같다는 아쉬움이 듭니다. 최근에 회사를 나와서 백수로 살고 있어서 시간이 많은데, 이 많은 시간을 효율적으로 투자해서 정진해야 할 것 같습니다.

        다음주 토요일에는 현대모비스 알고리즘 대회 1차, 종료후 팀연습이 예정되어 있어 ARC를 풀지 못할 것 같지만, 일요일 ABC를 참여해보도록 하겠습니다.

        해당 라운드 평가

        C번을 제외하고는 마음에 드는 셋이였습니다. D번이나 여러 문제에서 예외처리를 해주지 않도록 제한 조건을 주어서 좋았습니다. 평소 ABC보다 약간 쉬운 느낌이 들었습니다.

        ]]>
        kidw0124