ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • AtCoder Beginner Contest 249 후기
    AtCoder 2022. 5. 1. 16:10

    A. Jogging​

     두 사람 A와 B가 있을 때, A는 a분동안 걷고, c분을 쉰다. B는 d분동안 걷고, f분을 쉰다. 둘다 같은 x시간에 동시에 출발했을 때, 누가 더 멀리 가있는지 확인하는 간단한 수학문제이다. A는 1초에 b만큼 가고, B는 1초에 e만큼 간다. 접근법은 간단하다. 걷고 쉬는 것을 하나의 사이클이라고 할 때, 전체 사이클을 구한다. 이후 남은 시간과 걸을 수 있는 시간을 비교하여 더 작은 쪽만큼 걸으면 전체 거리가 된다

    import sys
    
    # 입력부
    a,b,c,d,e,f,x = map(int, sys.stdin.readline().split())
    taka = (a * (x // (a + c)) + min((x % (a + c)), a)) * b
    aoki = (d * (x // (d + f)) + min((x % (d + f)), d)) * e
    if taka > aoki:
        print("Takahashi")
    elif taka < aoki:
        print('Aoki')
    else:
        print("Draw")

    B. Perfect String

     간단한 문제이다. 알파벳 대문자부터 소문자까지 한번만 등장하면 Yes, 그렇지 않으면 No인데 알파벳 대문자와 소문자가 적어도 하나 이상 나오는 조건만 주의하면 된다

    import sys, string
    
    # 입력부
    arr = list(sys.stdin.readline().rstrip())
    # cache : 특정 알파벳이 몇번 나왔는지 저장하는 dictionary
    cache = dict()
    
    # flag1 : 소문자가 한번이라도 나오는지 확인하는 변수
    # flag2 : 대문자가 한번이라도 나오는지 확인하는 변수
    flag1, flag2 = False, False
    
    for i in arr:
    	# 중복이면 절대로 조건을 만족할 수 없다
        if i in cache:
            print('No')
            sys.exit(0)
        # 소문자 등장 시 flag1 변수 변경
        if i in string.ascii_lowercase:
            flag1 = True
        # 대문자 등장 시 flag2 변수 변경
        if i in string.ascii_uppercase:
            flag2 = True
        cache[i] = 1
        
    # 조건에 따라 정답 출력
    print('Yes' if flag1 and flag2 else 'No')

    C. Just K

     브루트 포스문제이다. N이 15로 매우 작기 때문에 복잡도를 $O(Max\ Len\ of(S_i) \ * \ N \ 2^N)$으로 해도 문제가 없다. 심지어 $S_i$는 알파벳 소문자로만 이루어지며, 중복된 알파벳이 없기 때문에 최대 26자가 됨을 알 수 있다. 따라서 전체 문자열을 비트마스킹으로 순회한 다음, 비트마스킹에 속한 문자열의 알파벳들의 출현 횟수를 체크하여 조건에 맞게 최대값을 변경시키면 정답을 구할 수 있다.

    import sys
    
    # 입력부
    n, k = map(int, sys.stdin.readline().split())
    arr = []
    for _ in range(n):
        arr.append(sys.stdin.readline().rstrip())
    ans = 0
    
    # 비트마스킹으로 가능한 부분집합을 전부 뽑는다
    for i in range(1 << n):
    	# cache : 현재 부분집합에 속한 문자열의 알파벳 출현 횟수를 저장하는 dictionary
        cache = dict()
        cnt = 0
        for j in range(n):
            # 현재 부분 집합에 속하는 문자열인 경우
            if (1 << j) & i != 0:
                # 알파벳의 출현 횟수 저장
                for q in arr[j]:
                    if q not in cache:
                        cache[q] = 1
                    else:
                        cache[q] += 1
        # 정확히 k번 등장한 알파벳 수 계산
        for j in cache:
            if cache[j] == k:
                cnt += 1
        # 정답 갱신
        ans = max(ans, cnt)
        
    # 정답 출력
    print(ans)

    D. Index Trio

     대회중에 풀긴 했으나, 정해는 아니었다. 우선 필자의 풀이는 다음과 같다. $A_i$로 들어오는 숫자들의 갯수를 저장한다. 전체 수를 순회하면서 분해되는 수 (문제 기준 $A_i$)를 약수의 대칭성을 이용하여 순회한 후에 가능한 두 약수가 캐싱된 갯수에 있는지 확인하고 둘 다 있다면 더해준다. 이 때 2 x 2 = 4처럼 두 약수가 같은 경우는 중복을 제거하기 위해 빼준다. 

    #include <bits/stdc++.h>
    
    typedef long long ll;
    using namespace std;
    
    int n;
    // temp : 입력으로 들어오는 수의 집합
    unordered_set<int> temp;
    // cnt : 현재 수가 i일 때 몇 개의 숫자가 i인지 저장하는 배열
    ll cnt[200001];
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(NULL);
        cout.tie(NULL);
    
        // 입력부
        cin >> n;
        ll ans = 0;
        memset(cnt, 0, sizeof(cnt));
        for (int i = 1; i <= n; i++) {
            int a;
            cin >> a;
            temp.insert(a);
            cnt[a] += 1;
        }
        // set을 순회한다
        for (int num : temp) {
        	// 약수의 대칭성을 이용하여 sqrt(N)까지 순회한다
            for (int j = 1; j <= int(sqrt(num)); j++) {
                // 만일 두 약수가 있다면 포함
                if (num % j == 0 && cnt[j] != 0 && cnt[num / j] != 0) {
                	// 2를 곱해주는 이유는, j와 num / j가 자리를 서로 바꿀 수 있기 때문이다
                    ans += 2 * (cnt[num] * cnt[j] * cnt[num / j]);
                    // 만일 두 약수가 같다면 배제
                    if (j == num / j) {
                        ans -= cnt[num] * cnt[j] * cnt[num / j];
                    }
                }
            }
        }
        // 정답 출력
        cout << ans;
    }

     물론 시간복잡도 상의로 $O(N \sqrt(N))$으로 무리가 없으나, 자칫 N이 더 커진다면 TLE를 피할 수 없을 것이다. 현재는 N이 20만이나, N이 50만 정도만 되어도 3억번의 연산을 해야한다. 대회가 끝나고 공식 풀이를 읽어보았고 공식 풀이를 소개하겠다.

     인덱스 i,j,k가 있다고 하자, 이 때 각각의 $A_i, A_j, A_k$ 중 세 수가 모두 입력의 최대값(이후 M이라고 하겠다)보다 클 수 없다. 기껏해봐야 1이 있는 경우 두 수가 최대값이 될 수 있을 뿐이다. 따라서, $ 1 \leq i,j,k \leq M$이 되며, $A_i = A_j * A_k$ 이고 $A_i, A_j, A_k$는 모두 양의 정수이다. 

     따라서, $A_j$를 기준으로 생각해보자. 어차피 $A_i$는 $A_k$에 의해 결정되므로 $A_i$는 신경쓰지 않아도 된다. 어차피 $A_j$가 가능한 범위는 M, 즉 입력의 최대값이다. 그렇다면 $A_k$가 가능한 범위는 어디까지 일까? M을 $A_j$로 나눈 수의 몫, 즉  $\left \lfloor \frac{M}{A_j}\right \rfloor $이다. 

     이제 복잡도를 구해보자. 최악의 경우 1부터 N까지 단 한번만 들어올 수도 있기 때문에, 이 경우를 가정하고 계산을 하겠다. 이 때 $A_j$의 범위는 1이상 M이하이다. 따라서, $A_j$를 범위에 따라 순회하게 되면 $A_k$의 복잡도는 $\sum_{A_j=1}^{M}\left \lfloor \frac{M}{A_j}\right \rfloor$가 된다. 그런데, 이것은 자명하게도 $\sum_{A_j=1}^{M}\left \lfloor \frac{M}{A_j}\right \rfloor \leq \sum_{A_j=1}^{M} \frac{M}{A_j}$ 가 성립한다. 왜냐하면 단순히 정수값인 몫만 더한 것과 몫과 소숫점까지 같이 포함해서 더한 것은 당연히 후자가 더 크거나 같기 때문이다. 또한 이것은 $\sum_{A_j=1}^{M} \frac{M}{A_j} \leq \frac{M}{1} + \left ( \frac{M}{2} + \frac{M}{2}\right ) + \left (\frac{M}{4} + \frac{M}{4} + \frac{M}{4} + \frac{M}{4} \right)+ ... + \left(\frac{M}{2^d} + ... + \frac{M}{2^d} \right)$로 확장할 수 있다. 이유는 아래의 그림과 같기 때문이다

    <그림 1. 증명>

     원래 위의 식을 아래와 같이 바꾸게 되면 분모가 더 작은 수, 즉 더 큰 수로 바꾸기 때문에 전체적인 합 역시 아래가 크거나 같을 수 밖에 없다. 같은 경우는 1까지만 있는 경우거나, $\frac{1} {2}$까지만 있는 경우이다. 따라서, 전체적인 식을 정리하면 $\sum_{A_j=1}^{M}\left \lfloor \frac{M}{A_j}\right \rfloor \leq M(d + 1)$이 된다. 이 때, $2^d = M \rightarrow d = log_2 M$가 되므로 전체적인 복잡도는 $O(M log M)$이 된다. 

    #include <bits/stdc++.h>
    
    typedef long long ll;
    using namespace std;
    
    int n;
    // cache : 현재 수가 i일 때 i가 몇번 나왔는지 저장하는 배열
    int cache[200001];
    // temp : 입력 수들의 set
    unordered_set<int> temp;
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(NULL);
        cout.tie(NULL);
    
        // 입력부
        cin >> n;
        memset(cache, 0, sizeof(cache));
        ll ans = 0;
        int m = 0;
        for (int i = 0; i < n; i++) {
            int x;
            cin >> x;
            cache[x] += 1;
            temp.insert(x);
            m = max(m, x);
        }
        // 입력 set을 돌면서
        for (int num : temp) {
        	// A_j와 A_k의 곱이 최대값 이하일 때 까지 순회하면서
            // 가능한 쌍을 더해준다
            for (int k = 1; k * num <= m; k++) {
                // 오버플로우가 발생할 수 있으니 long long으로 캐스팅
                ans += (ll) cache[num] * cache[k] * cache[num * k];
            }
        }
        // 정답 출력
        cout << ans;
    }

     

    대회 후기 : 우테코 방학시기에 방도 구하고 레벨 1 조원들이랑 1박 2일 회식도 해서 상당히 바쁜 나날들이었다. 거기다 선릉으로 출근도 하면서 적응기도 거치고 자취방에 필요한 것들고 구입하고 청소도 하면서 최초로 여유로울 시기에 본 대회라 당연히 제때 참여하지는 못하고 버추얼로 참여했다. 그럼에도 D 풀이를(정해는 아니지만) 떠올려서 운 좋게 맞아서 나름 괜찮다고 생각했으나 공식 풀이를 보고 아직 부족하다고 느꼈다. 그럼에도 불구하고 여전히 페널티를 많이 먹어서 짜증난다. 특히 간단한 A에서 상당히 고전하여 스스로에게 많이 실망했다.. 열심히 하자가 아니라 꾸준히 하자!

     

    ==== E번 업솔빙 중

    'AtCoder' 카테고리의 다른 글

    Atcoder Beginner Contest 252 후기  (0) 2022.05.29
    AtCoder Beginner Contest 250 후기  (0) 2022.05.08
    Atcoder Beginner Contest 243 후기  (2) 2022.03.13
    AtCoder Beginner Contest 242 후기  (0) 2022.03.06
    AtCoder Beginner Contest 241 후기  (0) 2022.02.27

    댓글

Designed by Tistory.