티스토리 뷰

대회/아레나

solved.ac Grand Arena #1

79brue 2023. 8. 6. 17:02

역사적인 solved.ac 아레나 첫 대회에서 모든 문제를 풀어 4등을 했다. 한동안 OI style의 문제들만 풀다 ICPC style의 문제들을 접하니 조금 난감했는데, 다행히도 운이 좋아 좋은 성적을 받았다. 대회가 성공적으로 마무리된 점은 한국 PS계에 상당히 고무적일 것으로 보인다. 아레나 시스템에 대한 다양한 생각을 해봤는데, 이런 이야기들은 후기 끝에 짧게 써 보겠다.

 

A. 양말 짝 맞추기 (0:00:31)

다섯 개의 수 중 한 번 등장하는 수를 찾는 문제이다. 다양한 풀이 방법이 있지만, 다섯 개의 수를 XOR하는 것이 가장 코딩하기 빠르다.

 

더보기
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int a, b, c, d, e;

int main(){
    cin>>a>>b>>c>>d>>e;
    cout<<(a^b^c^d^e);
}

 

B. 끝말잇기 (0:02:44)

여러 가지 케이스를 처리하는 문제이다. 다음 조건을 만족하는 단어를 찾으면 된다.

  • 끝말잇기 기록에 등장하지 않는다. (Naive하게 확인하거나, map 등으로 확인 가능)
  • ? 앞에 단어가 있다면, 해당 단어의 끝 글자와 선택한 단어의 첫 글자가 같다.
  • ? 뒤에 단어가 있다면, 해당 단어의 첫 글자와 선택한 단어의 끝 글자가 같다.

이상 조건들은 모두 충분히 빠른 시간 복잡도에 처리할 수 있다.

 

더보기

 

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int n;
string str[102];
int k;
string cand[102];
int pnt;

int main(){
    cin>>n;
    for(int i=1; i<=n; i++){
        cin >> str[i];
        if(str[i] == "?") pnt = i;
    }
    cin>>k;
    for(int i=1; i<=k; i++){
        cin>>cand[i];
    }

    for(int i=1; i<=k; i++){
        bool duplicate = 0;
        for(int j=1; j<=n; j++) if(cand[i] == str[j]) duplicate = 1;
        if(duplicate) continue;

        if(pnt != 1){
            if(str[pnt-1].back() != cand[i][0]) continue;
        }
        if(pnt != n){
            if(str[pnt+1][0] != cand[i].back()) continue;
        }
        cout << cand[i];
        return 0;
    }
}

 

H. 행렬 연산 (행렬 계산하기) (0:07:23)

두 문제를 빠르게 풀고 가장 쉬운 문제를 찾아 좀 헤매다가, 빠른 시간에 풀린 H번 문제를 잡았다. 풀이는 간단하다. $i$행 $j$열의 칸의 값이 바뀌는 것은, $i$행에 더하는 업데이트나 $j$열에 더하는 업데이트가 주어졌을 때 두 경우 뿐이다.

 

$A_i$를 $i$번 행에 더해진 값, $B_i$를 $i$번 열에 더해진 값이라고 하자. 이 두 값을 모든 연산을 보며 갱신하고, 마지막에 답을 출력할 때 $i$행 $j$열의 값은 $A_i + B_j$를 출력하면 된다. 시간 복잡도는 $O(Q+NM)$이다.

 

더보기
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int n, m, q;
ll row[500002], col[500002];

int main(){
    scanf("%d %d %d", &n, &m, &q);
    while(q--){
        int a, x; ll v;
        scanf("%d %d %lld", &a, &x, &v);
        if(a==1) row[x]+=v;
        else col[x]+=v;
    }
    for(int i=1; i<=n; i++){
        for(int j=1; j<=m; j++){
            printf("%lld ", row[i]+col[j]);
        }
        puts("");
    }
}

 

G. 막대 만들기 (0:16:57)

$DP[x]$를 길이 $x$의 막대를 만드는 데 드는 비용이라고 하자. $DP[x]$를 찾기 위해서는 기존에 길이가 $x$인 막대의 개수에 $DP[y]$ ($y$는 $x$의 약수)를 모두 더해야 한다. 약수를 더할 때는 잘 알려진 테크닉인 $y \le \sqrt{x}$ 이하까지만 보는 방법을 사용한다. 이때 모든 약수 $y$에 대해 $y$나 $\frac{x}{y}$ 둘 중 하나의 정수를 볼 수 있으므로, $O(L \sqrt L)$ 시간에 문제를 해결할 수 있다.

 

더보기
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int n;
int arr[100002];
int q;
int query[100002];

ll DP[100002];

int main(){
    scanf("%d", &n);
    for(int i=1; i<=n; i++) scanf("%d", &arr[i]), DP[arr[i]] = 1;
    scanf("%d", &q);
    for(int i=1; i<=q; i++) scanf("%d", &query[i]);

    for(int i=1; i<=100000; i++){
        for(int j=1; j*j<=i; j++){
            if(i==j) break;
            if(i%j==0){
                DP[i] += DP[j];
                if(j!=1 && j*j != i) DP[i] += DP[i/j];
            }
        }
    }
    for(int i=1; i<=q; i++) printf("%d ", DP[query[i]]);
}

 

E. 배수 피하기 (0:18:34)

간단한 조합론 문제이지만 몇 가지 실수를 해서 페널티가 늘었다.

 

일단 당연히도 나머지 기준으로 수들을 분류한다. $K$로 나눈 나머지가 $i$인 수의 개수를 $A_i$라 하자. 이때 아래와 같이 두 경우로 나눠서 처리한다. 집합의 크기가 2 이상이라는 조건은 무시하고 처리한다.

  • $i=0$이거나 $2i=K$인 경우: 각 경우 모두 집합에서 두 개 이상의 원소를 고르면 안 되므로, $A_i + 1$을 답에 곱한다.
  • 위의 경우가 아닌 경우: $i$와 $K-i$를 함께 생각한다. 양쪽 집합 모두에서 원소를 고르면 안 되므로, $2^{A_i} + 2^{A_{K-i}} - 1$을 답에 곱한다. 1을 뺀 것은 공집합이 두 번 세어지기 때문이다.

마지막으로 집합의 크기가 2 이상이라는 조건을 처리하기 위해 답에서 $n+1$을 빼 주면 된다. 시간 복잡도는 $O(N+K)$이다.

 

더보기
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const ll MOD = 1'000'000'007;

ll mpow(ll x, ll y){
    if(!y) return 1;
    if(y&1) return mpow(x, y-1) * x % MOD;
    ll tmp = mpow(x, y/2);
    return tmp * tmp % MOD;
}

int n, k;
ll cnt[100002];
ll fact[100002];
ll factInv[100002];
ll ans = 1;

int main(){
    fact[0] = 1;
    for(int i=1; i<=100000; i++) fact[i] = fact[i-1] * i % MOD;
    factInv[100000] = mpow(fact[100000], MOD-2);
    for(int i=99999; i>=0; i--) factInv[i] = factInv[i+1] * (i+1) % MOD;

    scanf("%d %d", &n, &k);
    for(int i=1; i<=n; i++){
        ll x;
        scanf("%lld", &x);
        cnt[x%k]++;
    }
    for(int i=0; i+i<=k; i++){
        if(i+i==k || i==0){
            ans = (ans * (cnt[i] + 1)) % MOD;
        }
        else ans = (ans * ((mpow(2, cnt[i]) + mpow(2, cnt[k-i]) + MOD - 1) % MOD) % MOD);
    }
    ans = (ans + MOD - n - 1) % MOD;
    printf("%lld", ans);
}

 

I. 행렬 연산 (연산 찾기) (0:29:31)

어딘가 이미 있을 법한 문제인데 본 적은 없다.

 

일단 답이 가능한지부터 살펴보자. 입력으로 주어진 행렬을 $P$라고 하자. 앞에서 H번 문제를 풀면서 $P_{i,j}$ = $A_i + B_j$임을 보았다. 따라서 $P_ij = A_i + B_j = (A_i + B_1) + (A_1 + B_j) - (A_1 - B_1) = P_{i,1} + P_{1,j} - P_{1,1}$임을 알 수 있다. 이 조건을 $2 \le i$, $2 \le j$인 모든 칸에 대해 시험해 본다. 이 조건을 만족하지 않는다면, 불가능이다. 위 조건을 만족한다면 무조건 가능함은 자명한데, $A$와 $B$를 적당히 골라 $i=1$ 혹은 $j=1$인 칸들이 입력과 같도록 할 수 있다면 $i \ge 2$, $j \ge 2$인 칸들은 위 등식에 따라 당연히 입력과 같은 값이 되기 때문이다. $i=1$이거나 $j=1$인 칸은 매우 자명하게도 우리가 자유롭게 정할 수 있다.

 

이제 $A_1$과 $B_1$을 정할 것이다. 당연히도 $A_1 + B_1 = P_{1,1}$을 만족해야 한다. 그리고 이것이 결정되면 나머지 $A_i$, $B_i$ 값들도 자동결정된다. 우리 목표는 $A$와 $B$ 수열 값 중 0의 개수를 최대화하는 것이다. $A_i$ 또는 $B_i$ ($i \ge 2$)가 1일 조건은 각각 $B_1 = P_{1, i}$ 또는 $A_1 = P_{i, 1}$ 꼴로 정리된다. 따라서 여기에 해당하는 $(A_1, B_1)$ 값들만 시도해 보며 각각의 경우에 최적의 답을 찾아주면 문제를 $O(NM)$ 시간에 해결할 수 있다. $A_1$과 $B_1$이 0이 될 수 있음에 주의하자.

 

더보기
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int n, m;
vector<int> arr[500002];
vector<vector<int> > ans;

unordered_map<ll, int> mpUp, mpLeft;

int main(){
    scanf("%d %d", &n, &m);
    for(int i=1; i<=n; i++){
        arr[i].resize(m+1);
        for(int j=1; j<=m; j++) scanf("%d", &arr[i][j]);
    }

    for(int i=1; i<=n; i++){
        for(int j=1; j<=m; j++){
            if(arr[1][1] + arr[i][j] != arr[1][j] + arr[i][1]){
                printf("-1");
                return 0;
            }
        }
    }

    vector<ll> cand;
    cand.push_back(0);
    for(int i=1; i<=m; i++) cand.push_back(arr[1][i]);
    for(int i=1; i<=n; i++) cand.push_back(arr[1][1] - arr[i][1]);
    for(int i=2; i<=m; i++) mpUp[arr[1][i]]++;
    for(int i=2; i<=n; i++) mpLeft[arr[i][1]]++;

    int minAns = 1e9;
    ll minV = -1;
    for(ll v: cand){
        ll val = n + m - !v - !(arr[1][1] - v) - mpUp[v] - mpLeft[arr[1][1]-v];
        if(minAns > val) minAns = val, minV = v;
    }

    printf("%d\n", minAns);
    ll v = minV, nv = arr[1][1] - v;
    if(minV != 0) printf("1 1 %lld\n", v);
    if(nv != 0) printf("2 1 %lld\n", nv);
    for(int i=2; i<=n; i++) if(arr[i][1] != nv) printf("1 %d %lld\n", i, arr[i][1]-nv);
    for(int i=2; i<=m; i++) if(arr[1][i] != v) printf("2 %d %lld\n", i, arr[1][i]-v);
}

 

C. 게리멘더링 (0:33:02)

(맞춤법에는 게리맨더링이 맞다. 맞춤법을 매우 강조하시는 분이 검수진에 있으니 의도적인 표기일 거라고 생각한다.)

 

DP를 해 보자. $DP[i]$는 1번부터 $i$번 수까지를 구간으로 나누었고, 이때 (합이 양수인 구간 수) - (합이 음수인 구간 수)의 최댓값이라고 하자. $DP[n]$이 양수이면 가능하고, 0 이하이면 불가능할 것이다.

 

위 DP는 Naive하게 하면 $O(N^3)$, 누적 합을 쓰면 $O(N^2)$까지는 줄일 수 있지만, 그 이하로는 어렵다. 시간 복잡도를 더 줄이기 위해 Segment Tree를 사용한다. $DP[i]$의 값을 $x = A_1 + \cdots + A_i = S_i$ 인 지점에 저장한다고 생각하자. (당연히 좌표 압축을 먼저 한다.) 이렇게 하면, $DP[i] = \max_{0 \le j < i} DP[j] + cost(j, i)$와 같이 계산할 때 $S_j < S_i$면 $cost(j, i) = 1$이고, $S_j > S_i$이면 $cost(j, i) = -1$이 되는 식으로 생각할 수 있다. 세그먼트 트리에 DP 값을 $S_i$가 증가하는 순으로 배열했으므로 이들 조건을 만족하는 것들은 각각 구간을 이루고, 구간 쿼리를 통해 DP 값을 구할 수 있다. 시간 복잡도는 $O(N \log N)$이다. 

 

더보기
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

struct segTree{
    int tree[800002];

    void init(int i, int l, int r){
        tree[i] = -1e9;
        if(l==r) return;
        int m = (l+r)>>1;
        init(i*2, l, m);
        init(i*2+1, m+1, r);
    }

    void update(int i, int l, int r, int x, int v){
        if(l==r){
            tree[i] = v;
            return;
        }
        int m = (l+r)>>1;
        if(x<=m) update(i*2, l, m, x, v);
        else update(i*2+1, m+1, r, x, v);
        tree[i] = max(tree[i*2], tree[i*2+1]);
    }

    int query(int i, int l, int r, int s, int e){
        if(r<s || e<l) return -1e9;
        if(s<=l && r<=e) return tree[i];
        int m = (l+r)>>1;
        return max(query(i*2, l, m, s, e), query(i*2+1, m+1, r, s, e));
    }
} tree;

int t;
int n;
ll arr[200002], sum[200002];

int main(){
    scanf("%d", &t);
    while(t--){
        scanf("%d", &n);
        for(int i=1; i<=n; i++){
            scanf("%lld", &arr[i]);
            sum[i] = sum[i-1] + arr[i];
        }
        vector<ll> sums (1, 0);
        for(int i=1; i<=n; i++) sums.push_back(sum[i]);
        sort(sums.begin(), sums.end());
        sums.erase(unique(sums.begin(), sums.end()), sums.end());

        int L = (int)sums.size();
        tree.init(1, 0, L-1);
        tree.update(1, 0, L-1, lower_bound(sums.begin(), sums.end(), 0) - sums.begin(), 0);

        for(int i=1; i<=n; i++){
            int idx = lower_bound(sums.begin(), sums.end(), sum[i]) - sums.begin();
            int val = tree.query(1, 0, L-1, idx, idx);
            val = max(val, tree.query(1, 0, L-1, 0, idx-1) + 1);
            val = max(val, tree.query(1, 0, L-1, idx+1, L-1) - 1);
            tree.update(1, 0, L-1, idx, val);
            if(i==n){
                printf("%s\n", val > 0 ? "YES" : "NO");
            }
        }
    }
}

 

D. Same Range (1:19:09)

먼저 답을 만족하는 구간 $[l, r]$이 있다고 해 보자. 이때 이 구간을 평면상에 $(l, r)$ 좌표로 플로팅할 것이다. 각 좌표가 격자 칸이라고 생각하면, 답을 만족하는 격자 칸에 모두 색칠했을 때 답은 색칠된 총 넓이와 같다.

 

각 구간 $[l, r]$ 중 어떤 것은 min 조건만 만족할 것이고, 어떤 것은 max 조건만 만족할 것이고, 어떤 것은 두 조건 중 하나 이상을 만족할 것이다. 각각의 개수를 $Q$, $R$, $S$라고 하면, 답은 $Q+R-S$와 같다.

 

이제 각 조건이 만족하는 구간의 개수를 어떻게 세는지 알아보자. 먼저 $Q$를 구하는 방법을 알아볼 것이다. 이때 $\min(A_l, \cdots, A_r) = \min(B_l, \cdots, B_r)$ 조건을 만족하는 구간의 개수를 세는 것이 목표가 된다. 이 최솟값을 $v$라고 고정하자. $A_i$, $B_i$ 중 값이 $v$보다 작은 것이 있다면, 해당 $i$는 구간에 포함될 수 없으므로 이들을 기준으로 구간을 나눠 준다. 이때 $A_i$나 $B_i$가 $v$인 것을 모두 찾고, 나눠진 구간을 기준으로 같은 구간에 있는 것끼리 묶어 준다. 이 index들 중 $A$의 것 하나, $B$의 것 하나를 포함하는 구간은 (나눠진 구간 밖으로 넘어가지 않는다면) 답이 될 것이다. 자세히 생각해 보면, 위 인덱스들을 그 위치에 따라 정렬했을 때, 인접한 두 개의 인덱스 중 A와 B에서 온 인덱스가 하나씩 있는 쌍이 있다면, 그 쌍만 고려해도 충분함을 알 수 있다. 따라서 이 정점을 오른쪽 아래 끝 점으로 하고, 구간의 왼쪽/오른쪽 끝 경계 $L$, $R$에 대해 $(L+1, R-1)$을 왼쪽 위 끝 점으로 하는 직사각형 내부의 점에 해당하는 구간은 모두 답이 될 수 있음을 알 수 있다. 여기까지 하고 나면 $O(N)$개 정도의 직사각형이 생긴다. 이 직사각형의 합집합의 넓이를 세는 방법은 Plane Sweeping 등으로 잘 알려져 있는 문제이고, $O(N \log N)$에 문제를 해결할 수 있다.

 

$Q$와 $R$은 이렇게 구할 수 있고, $S$는 $Q$와 $R$에 해당하는 직사각형들을 모두 한번에 처리해 넓이를 구하면 된다.

 

더보기
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

struct dat{
    int p, x, v;
    dat(){}
    dat(int p, int x, int v): p(p), x(x), v(v){}
    bool operator<(const dat &r)const{
        if(v != r.v) return v<r.v;
        return x<r.x;
    }
};

struct Rect{
    int sx, ex, sy, ey, color;
    Rect(){}
    Rect(int sx, int ex, int sy, int ey, int color): sx(sx), ex(ex), sy(sy), ey(ey), color(color){}
};

struct segTree{
    int cnt[1<<19][3], sum[1<<19][3];

    void update(int i, int l, int r, int s, int e, int col, int v){
        if(r<s || e<l) return;
        if(s<=l && r<=e){
            cnt[i][col] += v;
            sum[i][col] = cnt[i][col] ? r-l+1 : l==r ? 0 : sum[i*2][col] + sum[i*2+1][col];
            return;
        }
        int m = (l+r)>>1;
        update(i*2, l, m, s, e, col, v);
        update(i*2+1, m+1, r, s, e, col, v);
        sum[i][col] = cnt[i][col] ? r-l+1 : l==r ? 0 : sum[i*2][col] + sum[i*2+1][col];
    }

    int query(int i, int l, int r, int s, int e, int col){
        if(r<s || e<l) return 0;
        if(s<=l && r<=e) return sum[i][col];
        int m = (l+r)>>1;
        return query(i*2, l, m, s, e, col) + query(i*2+1, m+1, r, s, e, col);
    }
} tree;

int n;
int a[250002];
int b[250002];
set<int> st;
vector<Rect> rects;

void makeRectangles(int col){
    vector<dat> occ;
    for(int i=1; i<=n; i++) occ.push_back(dat(0, i, a[i]));
    for(int i=1; i<=n; i++) occ.push_back(dat(1, i, b[i]));
    sort(occ.begin(), occ.end());

    st.clear();
    st.insert(0);
    st.insert(n+1);
    for(int s=0; s<n*2; s++){
        if(st.find(occ[s].x) != st.end()) continue; /// 이미 덮인 영역

        int e = s;
        while(e+1 < n*2 && occ[s].v == occ[e+1].v && *st.lower_bound(occ[s].x) > occ[e+1].x) e++;
        vector<pair<int, int> > vec;
        for(int i=s; i<=e; i++) vec.push_back(make_pair(occ[i].p, occ[i].x));

        int L = *prev(st.lower_bound(occ[s].x));
        int R = *st.upper_bound(occ[e].x);
        for(int i=0; i<(int)vec.size()-1; i++){
            if(vec[i].first == vec[i+1].first) continue;
            if(L+1 <= vec[i].second && vec[i+1].second <= R-1){
                rects.push_back(Rect(L+1, vec[i].second, vec[i+1].second, R-1, col));
            }
        }

        for(int i=s; i<=e; i++) st.insert(occ[i].x);
        s=e;
    }
}

struct Event{
    int y, xl, xr, col, v;
    Event(){}
    Event(int y, int xl, int xr, int col, int v): y(y), xl(xl), xr(xr), col(col), v(v){}
    bool operator<(const Event &r)const{
        return y<r.y;
    }
};

ll sum0, sum1, sumboth;

int main(){
    scanf("%d", &n);
    for(int i=1; i<=n; i++) scanf("%d", &a[i]);
    for(int i=1; i<=n; i++) scanf("%d", &b[i]);

    makeRectangles(0);
    for(int i=1; i<=n; i++) a[i] = -a[i], b[i] = -b[i];
    makeRectangles(1);

    vector<Event> vec;
    for(Rect p: rects){
        vec.push_back(Event(p.sy, p.sx, p.ex, p.color, 1));
        vec.push_back(Event(p.ey+1, p.sx, p.ex, p.color, -1));
    }
    sort(vec.begin(), vec.end());

    int pnt = 0;
    for(int y=1; y<=n; y++){
        while(pnt < (int)vec.size() && vec[pnt].y == y){
            tree.update(1, 1, n, vec[pnt].xl, vec[pnt].xr, vec[pnt].col, vec[pnt].v);
            tree.update(1, 1, n, vec[pnt].xl, vec[pnt].xr, 2, vec[pnt].v);
            pnt++;
        }
        sum0 += tree.query(1, 1, n, 1, n, 0);
        sum1 += tree.query(1, 1, n, 1, n, 1);
        sumboth += tree.query(1, 1, n, 1, n, 2);
    }
    printf("%lld", sum0 + sum1 - sumboth);
}

 

F. 돌 옮기기 (2:09:44)

조금 생각해 보면, 각 돌에 대해 다음 둘 중 하나가 최적이다. 더 많은 턴을 유효하게 가져가는 사람이 승리라는 관점에서 보기로 하자.

  • 이 돌을 게임이 끝날 때까지 밀지 않는다. 이것의 목적은 더 뒤에 있는 상대 돌의 전진을 막기 위함이다.
  • 이 돌을 끝까지 계속 민다. 일단 밀기 시작하면 무조건 끝까지 민다. 이렇게 하면 자신이 가능한 턴의 개수를 늘릴 수 있다.

각 돌에 대해 위 둘 중 어느 유형에 속하는지 볼 것이다. 맨 뒤에 있는 돌부터 하나씩 차례대로 본다. 돌의 종류가 모두 정해지고, 자신 뒤에 있는, 여기까지 밀 수 있는 돌들 중 내 돌이 많은지, 상대 돌이 많은지를 본다. 내 돌이 많으면 앞으로 보내는 게 이득이다. 그렇지 않다면, 막는 것이 이득이다. (이 사실은 간단한 식 정리로 보일 수 있다)

 

이렇게 각 돌에 대해 밀지 말지를 결정해준 후, 이것을 바탕으로 최종 승자를 구해야 한다. 이번에는 돌을 앞부터 보면서, 돌을 몇 칸이나 볼 수 있는지를 계산한다. 대략적인 방법은, 어떤 돌 앞에 있는 고정된 돌 중 가장 뒤에 있는 것을 찾은 뒤, 그 사이에 있는 빈 칸의 개수를 세는 식이다. 이렇게 한 후 흑과 백 중 더 많은 턴을 밀 수 있는 사람이 이긴다. 동률이면 후자인 흑이 이긴다. 시간 복잡도는 $O(N)$이다.

 

더보기
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const ll INF = 1e18;

int t, n;
char str[500002];
ll blank[500002];
bool fix[500002];

int main(){
    scanf("%d", &t);
    while(t--){
        scanf("%s", str+1);
        n = strlen(str+1);
        for(int i=0; i<=n+1; i++){
            blank[i] = 0;
            fix[i] = 0;
        }
        for(int i=1; i<=n; i++) blank[i] = blank[i-1] + (str[i] == '.');

        ll w = 0, b = 0;
        for(int i=n; i>=1; i--){
            if(str[i] == '.') continue;
            if(str[i] == 'W'){
                if(w>=b) fix[i] = 0, w++;
                else fix[i] = 1, w=b=0;
            }
            else{
                if(b>=w) fix[i] = 0, b++;
                else fix[i] = 1, w=b=0;
            }
        }

        ll wsum = 0, bsum = 0, cnt = 0;
        for(int i=1; i<=n; i++){
            if(str[i] == '.') cnt++;
            else if(str[i] == 'W'){
                if(fix[i]) cnt = 0;
                else wsum += cnt;
            }
            else if(str[i] == 'B'){
                if(fix[i]) cnt = 0;
                else bsum += cnt;
            }
        }
        printf("%s\n", wsum > bsum ? "WHITE" : "BLACK");
    }
}

 

아레나 시스템에 관한 고찰

사실 한국에서 이런 경쟁형 프로그래밍 웹사이트가 나온 것이 처음은 아니다. 정확한 시기는 잘 기억이 나지 않지만, 2017년 전후에 sundaycoding.xyz가 선보였던 적이 있다. 하지만 참가자 수가 빠르게 감소해서 결국 묻혔다. 이번에는 다르기를 기도했는데, 첫 라운드가 예상보다 잘 마무리된 점은 고무적이다.

 

아레나가 주는 또 다른 장점은 체계화된 대회 개최 시스템이다. 앞으로 유저들은 아레나 대회의 경우 다른 대회보다 더 잘 검증되었다는 인식을 가지게 될 수 있다. 하지만 이런 대회들로 참가가 몰리게 될 수 있다는 것은 오히려 단점으로 작용할 수 있다.

 

대회 운영진들 사이에서는 대회를 아레나화하는 것에 대한 고심이 더 커질 것으로 보인다. 아레나로 만든다면 다른 대회에 비해 몇 배 이상의 참가자를 확보할 수 있겠지만, 대회 운영진의 자율적인 대회 운영 등에서는 방해가 될 수 있다. 물론 대부분의 경우 solved.ac에서의 대회 검수가 대회 운영에 방해가 되지는 않을 것이라고 생각하지만, 예외는 항상 있을 수 있는 법이다.

 

한 가지 특이할 만한 사항은 레이팅의 즉각적인 반영이다. 대회가 끝나자마자 레이팅이 반영되는 것은 Codeforces 등과 비교했을 때 상당히 큰 차이점이다. 물론 Codeforces의 경우 Hack 시스템 등 다양한 원인이 있어서 바로 레이팅이 변동되지 않는 점도 감안해야 할 것이다.

 

종합하자면 아레나 시스템은 이제 막 첫 발을 내딛은 것으로 보이며, 아직까지는 매우 희망적이다. 앞으로도 solved.ac 아레나가 성공적으로 운영될 수 있기를 희망한다.

공지사항
최근에 올라온 글
Total
Today
Yesterday