Closest Pick

  • 문제 설명: N개의 티켓이 주어질 때 각 티켓은 1~K 사이의 정수를 가진다. 다른 티켓이 같은 정수를 가질 수도 있다. 1~K 사이의 정수 c가 랜덤으로 선택될 때 가지고 있는 티켓 중 하나가 N개의 티켓들보다 c에 가장 가까운 숫자일수록 이길 확률은 올라간다. 티켓 두 장을 구입해서 이길 확률을 최대화할 때 확률을 구하라.
    • N개의 티켓에 적힌 숫자와 같은 티켓일 경우 어떤 점수도 얻지 못한다. 두 티켓이 모든 c에 대해 같은 차이를 가지기 때문이다.
  • 입력
    • 1 <= T <= 100
    • 1 <= N <= 30
    • Test Set 1: 1 <= K <= 30
    • Test Set 2: 1 <= K <= 10^9
  • 출력: 오차 10^-6 이하이면 ok
  • 첫번째 접근법: Test Set 1은 K가 적기 때문에 브루트 포싱으로 해결할 수 있을 것이다.
    • 먼저 N개의 티켓에 적힌 숫자들의 집합을 P라고 할 때, 1~K 사이의 모든 숫자에 대해 P에 있는 숫자들 중 가장 작은 차이(min_diff)를 구한다.
    • P를 제외한 다른 숫자들 중에서 min_diff 보다 작은 차이를 가진 숫자를 찾는다. 참고로 숫자가 하나도 없다면 즉, P에 1~K 사이의 모든 숫자가 있다면 확률은 0이 될 것이다.
    • 이전에 선택한 숫자 2개에 대해 확률을 계산해야 한다. 확률은 P에 있는 숫자들보다 가까운 c의 개수를 K로 나눈 것이 된다. 이 확률의 의미는 모든 가능한 c에서 몇 개의 c가 가장 가까운지에 대한 것이다.
    • 여기서 P에 속한 숫자들을 제외한 것들 중 2개를 고르는 경우의 수는 (K-|P|)C(2) 이다. 여기서 C는 이항계수이다. Test Set 1은 K가 적기 때문에 브루트 포싱으로 해결할 수 있다.
    • 시간 복잡도: O(K^3)
#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
#include <cmath>
using namespace std;

void solve() {
    int N, K;
    cin >> N >> K;
    set<int> tickets;
    for (int i = 0, tmp; i < N; i++) {
        cin >> tmp;
        tickets.insert(tmp);
    }
    
    int min_diff[K];
    for (int c = 1; c <= K; c++) {
        int& diff = min_diff[c-1];
        diff = 1e9;
        for (auto& t : tickets)
            diff = min(diff, abs(t-c));
    }
    
    int max_wins = 0;
    for (int a = 1; a <= K; a++) {
        if (tickets.find(a) != tickets.end()) continue;
        for (int b = a; b <= K; b++) {
            if (tickets.find(b) != tickets.end()) continue;
            int wins = 0;
            for (int c = 1; c <= K; c++)
                if (abs(a-c) < min_diff[c-1] or abs(b-c) < min_diff[c-1])
                    wins++;
            max_wins = max(max_wins, wins);
        }
    }
    
    double ans = (double)max_wins / K;
    ans = round(ans * 1e6) / 1e6;
    cout << ans << endl;
}

int main() {
    int T;
    cin >> T;
    for (int tc = 1; tc <= T; tc++) {
        cout << "Case #" << tc << ": ";
        solve();
    }
    return 0;
}
  • 두번째 접근법: Test Set 2 부터는 K가 너무 크기 때문에 모든 c에 대해 시도해볼 수 없다. 두 숫자를 선택할 때 후보의 개수를 줄일 필요가 있다. P에 속한 숫자의 좌우 양 옆의 숫자들이 후보에 적합한데 그 이유는 P에 속한 숫자로부터 멀리 떨어진 숫자를 선택하게 되면 P에 속한 숫자와 선택한 숫자 사이의 중앙값을 기준으로 가까운 숫자들이 나뉘게 되기 때문이다. 예를 들어 P=[1,2,3,7] 이고 K=7일 때 3 4 5 6 7 에서 5를 선택하는 것보다 4 또는 6을 선택하는 것이 가까운 숫자들을 더 많이 포함하게 된다. (5를 선택하면 숫자 5 1개만 가까운 숫자이지만, 4와 6을 선택하게 되면 4,5 또는 5,6을 가까운 숫자로 얻게 된다.)
    • 또한, 후보에 있는 숫자들은 가까운 c의 개수를 계산할 때 어차피 P에 속한 숫자 사이에만 위치하기 때문에 1~K 사이의 모든 숫자에 대해서 가까운 c의 개수를 계산할 필요가 없다. 단순히 후보 숫자의 양 옆에 있는 Pi-1과 Pi 와의 차이를 계산하면 될 것이다. 그 외의 숫자는 어차피 Pi-1과 Pi와 더 가깝기 때문이다.
    • 처음에 후보 숫자들을 고를 때 가까운 c의 개수를 미리 계산할 수 있고, 이후에 가까운 c의 개수를 내림차순으로 정렬했을 때 2개를 선택하면 된다.
    • 참고로 P에 속한 숫자 집합의 양 끝에 대해서도 검사해주어야 한다. 범위 안에만 있다면 P1 - 1 과 Pn + 1 위치를 후보에 넣고 P1 - 1 그리고 K - Pn 을 미리 계산해준다.
    • 예외 처리: Pi-1 과 Pi 사이에 숫자가 3개 이상 있을 때 홀수 개수이면 중간에 1개 숫자가 겹치므로 빼주어야 한다. 예를 들어 3 4 5 6 7 에서 3과 7이 P에 속할 때 4를 선택하면 4,5 해서 2점이고 6을 선택하면 5,6 해서 2점인데 여기서 큰 값 2개를 고르는데 후보가 4 와 6 밖에 없다면 5가 중복되니 가까운 c의 개수를 4와 6중에서 1개 빼주어야 한다. 후보가 무엇인지는 중요하지 않기 때문에 4,6을 선택하거나 4,X 또는 6,X 를 선택할 때 가까운 c의 개수는 정확하게 계산될 수 있다.
    • 시간 복잡도는 O(N)
#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
#include <cmath>
#define pb push_back
#define sz(c) (int)(c).size()
using namespace std;

const static int _ = []() {
    cin.tie(0); cout.tie(0); ios_base::sync_with_stdio(false);
    return 0;
}();

void solve() {
    int N, K;
    cin >> N >> K;
    set<int> pset;
    for (int i = 0, tmp; i < N; i++) {
        cin >> tmp;
        pset.insert(tmp);
    }

    vector<int> tickets(pset.begin(), pset.end());
    vector<int> scores;
    if (tickets.front() > 1) scores.pb(tickets[0]-1);
    if (tickets.back() < K) scores.pb(K - tickets.back());
    for (int i = 1; i < sz(tickets); i++) {
        int diff = tickets[i] - tickets[i-1];
        if (diff > 1) { // 사이에 숫자가 1개 이상 있을 때
            scores.pb(diff >> 1); // 가까운 c의 개수 계산 (두 숫자의 차이 / 2)
            if (diff > 2) { // 숫자가 2개 이상 있을 때
                if ((diff-1) & 1) scores.pb((diff >> 1) - 1); // 홀수개이면 중복 제외
                else scores.pb(diff >> 1); // 짝수개이면 중복이 없으므로 그대로 계산
            }
        }
    }
    
    // 내림차순 정렬
    sort(scores.begin(), scores.end(), [&](int a, int b) { return a > b; });
    int wins = 0;
    // 최대 2개를 선택
    for (int i = 0; i < min(2, sz(scores)); i++) wins += scores[i];
    double ans = (double)wins / K;
    ans = round(ans * 1e6) / 1e6;
    cout << ans << endl;
}

int main() {
    int T;
    cin >> T;
    for (int tc = 1; tc <= T; tc++) {
        cout << "Case #" << tc << ": ";
        solve();
    }
    return 0;
}

 

Roaring Years

  • 문제 설명: 입력으로 년도가 주어질 때 다음 roaring 년도를 구하라.
    • roaring 의 유무는 숫자가 연속되는 숫자로 이루어진 것을 의미한다.
      2021 -> 20 / 21
      78910 -> 7 / 8 / 9 / 10
      99100 -> 99 / 100

      778, 122, 2020 같은 경우는 해당되지 않는다.

  • 입력
    • Test Set 1: 1 <= Y <= 10^6
    • Test Set 2: 1 <= Y <= 10^18
  • 첫번째 접근법: Y가 작기 때문에 1씩 증가시켜서 roaring 인지 파악하는 것으로 해결할 수 있다. 연속되려면 시작 번호는 최대 전체 자릿수의 절반정도 차지해야 하므로 L/2 부터 1까지 숫자를 나누는 단위를 줄이면서 roaring인지 시도해볼 수 있다. 큰 단위로 나누어질수록 가장 가까운 숫자이기 때문이다. 주의할 점은 자릿수 올림이 발생할 수 있기 때문에 L만큼 단위를 나눴을 때 연속되지 않는다면 L+1만큼 단위를 나눠서 검사할 필요가 있다. 또한 숫자의 맨 앞에 0이 오는 경우 예외 처리를 해주어야 한다.
    • 시간 복잡도: O(Y)
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#define sz(c) (int)(c).size()
using namespace std;

bool is_roaring(const string y, int l) {
    int prev = stoi(y.substr(0, l)), curr = -1;
    for (int i = l; i < sz(y); i += l, prev = curr) {
        if (y[i] == '0') return false;
        if (i+l > sz(y)) return false;
        curr = stoi(y.substr(i, l));
        if (prev + 1 == curr) continue;
        if (i+l+1 > sz(y)) return false;
        curr = stoi(y.substr(i, l+1));
        if (prev + 1 == curr) {
            l++;
            continue;
        } else return false;
    }
    return true;
}

void solve() {
    string year;
    cin >> year;
    int next = stoi(year);
    while (next++) {
        string y = to_string(next);
        for (int l = sz(y)>>1; l > 0; l--) {
            if (is_roaring(y, l)) {
                cout << next << endl;
                return;
            }
        }
    }
}

int main() {
    int T;
    cin >> T;
    for (int tc = 1; tc <= T; tc++) {
        cout << "Case #" << tc << ": ";
        solve();
    }
    return 0;
}
  • 두번째 접근법: Y의 범위가 10^18 이기 때문에 위의 방법대로 하면 시간 초과가 발생한다. 풀이 아이디어는 모든 roaring year의 집합에서 시작 번호를 이분법(bisection method)으로 찾는 것이다. 시작번호만 알면 뒤에 연속되는 자릿수에 따라 roaring year를 알 수 있기 때문에 시작번호만 이분법으로 탐색한다. 개인적으로 다른 사람 풀이를 봤을 때는 매개변수 탐색이라고 보는 것이 맞는 듯하다. 알고리즘은 다음과 같다.
    • 최초 연속되는 숫자의 개수를 2부터 시작하고 한 자릿수일 때 10^18까지 검사하기 위해 최대 18개까지 이분법을 수행한다.
    • 시작 번호는 10^18의 절반인 10^9가 최댓값이 되고 최솟값은 1이 된다. 이분 탐색으로 시작번호를 찾을 때는 시작번호로 만들어진 숫자가 입력 Y보다 크면 시작번호를 줄이고, Y보다 작거나 같으면 시작번호를 늘린다. 모든 집합의 원소에서 Y 다음으로 올 수 있는 가장 작은 값을 찾는 것이기 때문에 입력 Y보다 크면 시작번호를 줄여야 된다.
    • 매 연속되는 숫자 개수(2~18)마다 이분법을 수행해서 찾은 roaring year 중 최솟값을 반환하면 된다.
    • 시간 복잡도: O(log2(1e9) * 18 * 18) = 29.897352853986263 * 18 * 18 < 9,720
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#define sz(c) (int)(c).size()
using namespace std;
typedef long long ll;

const ll INF = 2e18;
ll Y;

ll make_roaring_year(ll nbr, int num_consecutives) {
    auto digits = [&](ll v) {
        ll x = v, y = 1;
        while (x) x /= 10, y *= 10;
        return y;
    };

    ll ret = 0;
    for (int i = 0; i < num_consecutives; i++, nbr++) {
        ll d = digits(nbr);
        // 이미 범위를 넘는 경우 roaring year은 없는 것이므로 INF를 반환한다.
        if (ret > INF/d) return INF;
        ret = ret * d + nbr;
        ret = min(ret, INF);
    }
    return ret;
}

ll search(int num_consecutives) {
    ll lo = 0, hi = 1e9;
    while (lo + 1 < hi) {
        ll mid = (lo + hi) >> 1;
        if (make_roaring_year(mid, num_consecutives) > Y) hi = mid;
        else lo = mid;
    }
    ll ret = make_roaring_year(hi, num_consecutives);
    return ret;
}

void solve() {
    cin >> Y;
    ll ans = INF;
    for (int n = 2; n <= 18; n++)
        ans = min(ans, search(n));
    cout << ans << endl;
}

int main() {
    int T;
    cin >> T;
    for (int tc = 1; tc <= T; tc++) {
        cout << "Case #" << tc << ": ";
        solve();
    }
    return 0;
}

 

Double or NOTing

 

Broken Clock

  • 문제 설명: 시계의 3개의 침이 눈금(tick) 단위로 주어질 때 정확한 시간을 "시간 분 초 나노초"로 출력하라. 시간은 정확히 자정~정오 사이의 시간(0~12)이다. 각 침 중 어느것이 시침, 분침, 초침인지는 알 수 없으며, 주어진 3개의 값은 각각 특정 침을 기준으로 움직인 눈금 수이다.
    • 시침은 매 나노초마다 정확히 1 눈금씩 움직임
    • 분침은 매 나노초마다 정확히 12 눈금씩 움직임
    • 초침은 매 나노초마다 정확히 720 눈금씩 움직임
    • 1 눈금 = 1/12 * 1/1e10 도
    • 1초 = 10^9 나노초
  • 입력: (0 ≤ A ≤ B ≤ C < 360×12×10^10)
  • 출력: h m s nano
  • 접근법: 이 문제는 순서 상관없이 상대적으로 움직인 눈금 수를 받아서 (1) 순서를 맞추고 (2) 눈금 수로 원래 시간을 맞춰야 하는 문제이다. 먼저 순서를 맞추는 방법부터 고민해보자. 입력으로 받은 A, B, C는 각각 시침, 분침, 초침과 매칭되어야 하는데 3개를 나열하는 순열은 3! = 6 으로 굉장히 작은 수임을 알 수 있다. 따라서 시침/분침/초침에 각각 3개의 눈금 수를 놓아보면 될 것이다.
  • 순서를 어떻게 맞출 것인가
    • 시침, 분침, 초침은 각각 1의 배수, 12의 배수, 720의 배수이다.

      0   0    0 <- 맨 처음 눈금
      1  12  720 <- 1나노초 뒤
      2  24 1440 <- 2나노초 뒤
      ...
      x 12x 720x <- x나노초 뒤

    • 그러나 회전하는 경우도 주어지기 때문에 맨 처음 눈금의 상태를 t라고 하자. 그럼 x나노초 뒤의 시침, 분침, 초침은

      t + x    = y1
      t + 12x  = y2
      t + 720x = y3

    • 를 가지게 된다. 여기서 2개의 수식을 이용하면 x와 t를 알아낼 수 있다.

        t + x    = y1
      - t + 12x  = y2
      ----------------
           -11x  = y1 - y2  =>  x = (y2 - y1) / 11
        =>  t = y1 - x

    • 그러나, x를 계산할 때 이 시계가 회전된 상태임을 포함해야 정확히 t를 계산할 수 있게 된다. 1회전의 눈금 수는 M = 360×12×10^10 이며, 두 눈금의 차이 y2 - y1 은 매 회전마다 값은 같기 때문에 diff를 미리 계산해놓는다.
      • 여기서 y2 < y1 인 경우에 대해 y2 += M 을 해준다.
    • 회전은 1시간 단위로 11번 회전해야 시침 y1으로 시작 상태를 구할 수 있다. 여기서 360도 회전할 때 눈금 수가 360×12×10^10 = M 이기 때문에 M/11, 2M/11, 3M/11 ... 11M/11 을 매 회전마다 diff/11 에 더해준다.
      • diff/11 + i*M/11 = (diff + i*M) / 11
      • 단 여기서 11x  = y2 - y1 도 만족해야 되기 때문에 위의 결과에서 11의 배수인지 검사하는 조건문을 추가해야 한다.
    • 그리고 입력으로 받은 A, B, C는 각각 y1, y2, y3 중 하나에 매칭되어야 한다. 3개의 수식에 대해 모두 일치하면 그 때의 X값을 시간 단위로 바꾸어 준다.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;

const ll M = 360*12*1e10;
const ll TICK_UNIT[3] = {1, 12, 720};
const ll MAX_NANO = 1e9;

void solve() {
    vector<ll> in(3);
    for (int i = 0; i < 3; i++) cin >> in[i];
    do {
        ll diff = in[1] - in[0];
        if (diff < 0) diff += M; // in[1] + M - in[0]

        for (int i = 0; i < 12; i++) {
            ll x = diff + i * M;
            if (x % 11 != 0) continue;
            x /= 11;
            ll t = in[0] - x;
            if (t < 0) t += M; // t = in[0] + M - x

            bool good = true;
            for (int i = 0; i < 3 and good; i++)
                if ((t + TICK_UNIT[i] * x) % M != in[i])
                    good = false;

            if (good) {
                ll divider[3] = {MAX_NANO, 60, 60};
                vector<ll> ans;
                for (int i = 0; i < 3; i++) {
                    ans.push_back(x % divider[i]);
                    x /= divider[i];
                }
                ans.push_back(x);
                for (auto it = ans.rbegin(); it != ans.rend(); it++)
                    cout << *it << " ";
                cout << endl;
                return;
            }
        }
    } while (next_permutation(in.begin(), in.end()));
}

int main() {
    int T;
    cin >> T;
    for (int tc = 1; tc <= T; tc++) {
        cout << "Case #" << tc << ": ";
        solve();
    }
    return 0;
}

 

Subtransmutation

  • 문제 설명: i번 금속을 파괴해서 (i-A)번 금속과 (i-B)번 금속을 만들 수 있다. 단 i > A 이거나 i > B 이어야 한다. 그렇지 않다면 단순히 파괴만 된다. 만약 A <= i <= B 일 경우 (i-A)번 금속만 만들어질 것이다. 입력으로 만들어야 할 금속 번호와 각 금속의 개수 및 A, B가 주어질 때 입력으로 주어진 모든 금속을 만들 수 있는 금속 번호 중 최솟값을 찾아야 한다. 만약 만들 수 없다면 IMPOSSIBLE을 출력
    • 금속은 아래 그림처럼 계속해서 분할할 수 있다. 예를 들어, A=1, B=2일 때 4번 금속은 최종적으로 1번 금속 1개와 2번 금속 2개를 생성한다.

  • 입력
    • 1 <= N <= 20
    • 1 <= Ui <= 20 : 각 금속 번호(=i)의 개수
    • 1 <= Un
    • 2 <= U1 + U2 + ... + Un
    • Test Set 1: A = 1, B = 2
    • Test Set 2: 1 <= A < B <= 2
  • 출력: 최소 금속 번호
  • 첫번째 접근법: 금속을 가장 많이 만들어야 하는 최악의 경우는 20번 금속을 20회 만드는 것이다. i번 금속을 분할해서 i-1번 금속과 i-2번 금속을 만들 때 생성된 두 금속을 분할하면 반드시 i-1번 금속은 i-2번 금속을 포함한다. Test Set 1 에서는 A=1, B=2 이기 때문에 20번 금속을 20회 만드려면 금속 번호가 최소 27이어야 한다. 1~19번 금속 번호를 생성하는 것까지 생각해본다면 넉넉잡아 금속 번호를 29번까지만 검사해보면 될 것이다. 검사는 단순히 A와 B를 빼주면서 분할되는 모든 금속번호를 카운트하는 것으로, 각 금속 번호에서 1씩 빼주면(분할의 최악의 경우) 전체 경우의 수는 2^20 = 1048576 인데 이를 29번 시도하니 29 * 1,048,576 = 30,408,704 만큼 반복문을 돌게 된다. 따라서 충분히 시간 안에 실행할 수 있다.
    • 21 => 20번 금속 1개
    • 22 => 20번 금속 2개
    • 23 => 1 + 2 = 3개
    • 24 => 2 + 3 = 5개
    • 25 => 3 + 5 = 8개
    • 26 => 5 + 8 = 13개
    • 27 => 8 + 13 = 21개
    • 28 => 13 + 21 = 34개
    • 29 => 21 + 34 = 55개
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int N, A, B;
vector<int> U;

void divide(int n, vector<int>& v) {
    if (n <= 0) return;
    if (n < v.size()) {
        if (v[n] > 0) {
            v[n]--;
            return;
        }
    }
    divide(n-A, v);
    divide(n-B, v);
}

void solve() {
    cin >> N >> A >> B;
    int maxunits = 0, ans = -1;
    U = vector<int> (N+1);
    for (int i = 1; i <= N; i++) {
        cin >> U[i];
        if (U[i] > maxunits) {
            maxunits = U[i];
            ans = i + min(A, B);
        }
    }
    while (ans < 30) {
        vector<int> v = U;
        divide(ans, v);
        // 분할했을 때 요구된 모든 금속을 생성할 수 있는 경우
        if (all_of(v.begin(), v.end(), [](int x) { return x <= 0; })) {
            cout << ans << endl;
            return;
        }
        ans++;
    }
    cout << "IMPOSSIBLE" << endl;
}

int main() {
    int T;
    cin >> T;
    for (int tc = 1; tc <= T; tc++) {
        cout << "Case #" << tc << ": ";
        solve();
    }
    return 0;
}
  • 두번째 접근법: Test Set2는 A와 B가 1~20 사이의 숫자로, 최악의 경우 A=19, B=20일 때 20번 금속을 20개 생성하는 것이다. 19번 금속부터 20씩 증가할 때 분할 시 20번 금속의 개수가 1씩 증가한다. 따라서 20개를 만드려면 금속 번호가 최대 19 + 20 * 20 = 419 는 되어야 한다.
    • 39 => 20번 금속 1개
    • 40 => 20번 금속 1개
    • 41 ~ 58 => 0개
    • 59 => 2개
    • 60 => 1개
    • 61 ~ 78 => 0개
    • 79 => 3개
    • 80 => 1개
    • ...
    • 19 + 20 * k => k개
  • 단, Test Set 1에서 했던 브루트 포싱은 419번 하게 되면 시간 초과가 발생하게 된다. 즉, 다른 방식으로 금속을 분할해서 검사해야 한다.
  • 해설에 적힌 아이디어는 M번 금속을 분할할 때 M-A번 금속과 M-B번 금속의 개수를 "M번 금속 개수 - U[M]"만큼 더해주면서 M을 1씩 줄여주고 계속 분할해간다. 단, M번 금속 개수 < U[M] 일 경우 분할해도 개수가 모자르기 때문에 더 이상 분할하는 것이 의미가 없다. 브루트-포싱과의 차이점은 이 방식은 반복문이 M회 돌기 때문에 시간 복잡도가 굉장히 작다는 것이다. 그럼에도 개수를 모두 확인할 수 있어서 최대 419번 금속까지 시도해보고 그래도 분할되지 않는다면 IMPOSSIBLE을 출력하면 된다.
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;

int N, A, B;
const int MAXNUM = 419;
int U[MAXNUM];

bool canPossible(int m) {
    vector<int> v(m+1, 0);
    v[m] = 1;
    for (int i = m; i >= 1; i--) {
        if (v[i] < U[i]) return false;
        int x = v[i] - U[i];
        if (i > A) v[i-A] += x;
        if (i > B) v[i-B] += x;
    }
    return true;
}

void solve() {
    cin >> N >> A >> B;
    int ans = -1;
    memset(U, 0, sizeof(U));
    for (int i = 1; i <= N; i++) {
        cin >> U[i];
        if (U[i] > 0) ans = i;
    }
    ans += A;
    while (ans < MAXNUM) {
        if (canPossible(ans)) {
            cout << ans << endl;
            return;
        }
        ans++;
    }
    cout << "IMPOSSIBLE" << endl;
}

int main() {
    int T;
    cin >> T;
    for (int tc = 1; tc <= T; tc++) {
        cout << "Case #" << tc << ": ";
        solve();
    }
    return 0;
}

 

Digit Blocks

  • 문제 설명: N개의 타워를 지어야 하는데, 각 타워는 최대 B개의 블록만 쌓을 수 있다. i번째 블록은 밑에서부터 i번째 임을 의미하며, 밑에서부터 블록을 쌓을 수 있다. 각 블록은 숫자가 한 면에 존재하며 항상 앞 쪽에 숫자 면이 존재한다. 블록을 쌓을 때는 B개 미만으로 있는 타워에 위에 쌓거나, 아니면 타워가 N개 미만일 때 새로운 타워의 첫번째 블록으로 쌓을 수 있다. 전부 다만든 뒤에 타워의 점수는 왼쪽 타워부터 오른쪽 타워 순으로 위에서부터 숫자를 붙여서 만든 정수들의 합이다. 예를 들어, bottom [ ... ] top 으로 타워를 표기할 때 [ 3 2 1 ], [ 5 4 3 ], [ 6 9 0 ] 으로 쌓인 3개의 타워는 123 + 345 + 96 = 564 를 점수로 갖는다. 각 블록마다 숫자는 0~9 사이이며, 서로 독립적으로 랜덤으로 균등하게 생성된다. 정답이 맞도록 하기 위해, 테스트 케이스의 모든 점수의 합은 적어도 P여야 한다.
    • 입력으로 T, N, B, P 가 주어지며, P는 테스트셋을 통과하기 위해 필요한 최소 점수 총합이다.
    • 이 문제는 interactive problem 으로, 처음에 놓아야할 블록에 적힌 숫자 D가 주어지고 프로그램은 놓길 원하는 타워의 번호 (1~N)를 응답한다.
    • 마지막 테스트 케이스를 제외하고 위 과정을 반복하고 나면 즉시 다음 테스트 케이스가 진행된다. 마지막 테스트 케이스가 끝나면 judge 프로그램은 총 점수가 P 이상이면 1 아니면 -1을 응답한다.
    • 위의 조건들 중 하나라도 잘못된 입력을 줄 경우 즉시 -1을 응답한다.
  • 입력
    • T <= 50
    • N <= 20
    • B <= 15
    • D = 0 ~ 9
    • Test Set 1: P <= 860939810732536850
    • Test Set 2: P <= 937467793908762347
    • 각 테스트 케이스당 N*B 번 judge 를 실행한다.
  • 첫번째 접근법: 점수 총합을 최대화해야 하는데, 매우 극단적으로 생각해보면 9보다 작은 수는 맨 아래에 위치시키고 9를 맨 위에 놓는 방법이 있다. 그러나 항상 9가 B번째 위치에 올 수는 없는데 그럴 경우 현재 상태에서 가장 높은 타워(height <= B-1)의 위에 놓는다.
#include <iostream>
#include <vector>
#include <algorithm>
#define pb push_back
#define sz(c) (int)(c).size()
using namespace std;
typedef long long ll;

int T, N, B;
ll P;

int put(int D, vector<vector<int>>& towers) {
    int loc;
    if (D < 9) {
        for (int i = 0; i < N; i++) {
            int h = sz(towers[i]);
            if (h < B-1) {
                loc = i+1;
                break;
            }
            if (h < B) loc = i+1;
        }
    } else {
        int idx = 0, highest = -1;
        for (int i = 0; i < N; i++) {
            int h = sz(towers[i]);
            if (h < B and h > highest) {
                highest = h;
                idx = i;
            }
        }
        loc = idx+1;
    }
    towers[loc-1].pb(D);
    return loc;
}

void solve() {
    vector<vector<int>> towers(N, vector<int> (0));
    for (int i = 0, D; i < N*B; i++) {
        cin >> D;
        cout << put(D, towers) << endl;
    }
}

int main() {
    cin >> T >> N >> B >> P;
    while (T--)
        solve();
    int correct;
    cin >> correct;
    return 0;
}
  • 두번째 접근법: 위의 전략은 그리디 알고리즘이다. 두번째 테스트셋부터 정확도를 확 올려야 하는데 확률을 계산하기에는 복잡할 것 같아서 위의 방법에서 디테일(?)을 추가했다. 테스팅 툴을 돌려보면 D >= 8 일 때 높은 위치에 놓을 경우 첫번째 테스트셋을 통과한다. 즉, 각 타워의 상위 2개의 블록에 8과 9를 적절히 배치하는 것으로 정확도를 올릴 수 있을 것 같았다.
    • 주의할 점은 put() 함수에서 D < 8 조건문에서 블록을 놓을 때 B-2 까지만 놓을 수 있도록 해야 하는 것이다.
    • 또한, 상위 2칸만 남았을 때 8보다 9가 항상 더 위에 오도록 하려면 높은 타워 중 2번째로 높은 타워에 8을 배치해야 한다. 따라서 높이 배열 2칸을 준비하고 8이 놓일 자리가 없을 경우에만 9가 놓일 자리에 8을 놓는다.
    • 상위 2칸이 남지 않았더라도 항상 가장 높은 위치에 9를, 그 다음 높은 위치에 8을 놓도록 해야 한다.
#include <iostream>
#include <vector>
#include <algorithm>
#define pb push_back
#define sz(c) (int)(c).size()
using namespace std;
typedef long long ll;

int T, N, B;
ll P;

int put(int D, vector<vector<int>>& towers) {
    int loc;
    if (D < 8) {
        for (int i = 0; i < N; i++) {
            int h = sz(towers[i]);
            if (h < B-2) {
                loc = i+1;
                break;
            }
            if (h < B) loc = i+1;
        }
    } else {
        int idx[2] = {0, 0}, highest[2] = {-1, -1};
        for (int i = 0; i < N; i++) {
            int h = sz(towers[i]);
            if (h < B and h > highest[0]) {
                highest[0] = h;
                idx[0] = i;
            }
            if (h < B-1 and h > highest[1]) {
                highest[1] = h;
                idx[1] = i;
            }
        }
        if (D == 8 and highest[1] == -1) idx[1] = idx[0];
        loc = idx[(D == 9 ? 0 : 1)] + 1;
    }
    towers[loc-1].pb(D);
    return loc;
}

void solve() {
    vector<vector<int>> towers(N, vector<int> (0));
    for (int i = 0, D; i < N*B; i++) {
        cin >> D;
        cout << put(D, towers) << endl;
    }
}

int main() {
    cin >> T >> N >> B >> P;
    while (T--)
        solve();
    int correct;
    cin >> correct;
    return 0;
}

 

Append Sort

  • 문제 설명: N개의 정수가 주어질 때, 각 숫자의 뒤에 0~9 사이의 숫자를 붙이는 방법으로만 정렬을 수행할 수 있다. 오름차순으로 정렬되도록 숫자를 붙일 때 최소 횟수를 반환하라.
  • 접근법: 정렬을 만족하려면 A[i-1] < A[i] 이어야 하고, 최소한으로 숫자를 붙이려면 A[i-1] >= A[i] 일 때 A[i]에 숫자를 붙이고 난 수가 가능한 작은 수여야 함. 숫자를 붙이는 방법은 A[i-1] >= A[i] 일 때 3가지 경우로 나뉨
    • 맨 처음에 A[i]의 자릿수만큼 A[i-1]의 앞에서부터 비교 --> A[i-1]의 prefix 라고 하자.
    • 숫자가 같은 경우 "남은 A[i-1]의 숫자에서 +1" 한 숫자들을 A[i]의 뒤에 붙여준다.
      • e.g. A[i-1] = 3289, A[i] = 32 --> 3290
      • 주의할 점: +1할 때 자리 올림이 발생해서 prefix 에서 숫자가 바뀌어야 되는 경우 예외 처리를 해주어야 함. 남은 A[i-1]의 숫자 개수 + 1 만큼 0을 A[i]의 뒤에 붙여준다.
      • e.g. A[i-1] = 1999, A[i] = 19 --> 19000
    • A[i]가 더 큰 경우, 남은 A[i-1]의 숫자 개수만큼 0을 A[i]의 뒤에 붙여준다.
      • e.g. A[i-1] = 3223, A[i] = 41 --> 4100
    • A[i-1]이 더 큰 경우, 남은 A[i-1]의 숫자 개수 + 1 만큼 0을 A[i]의 뒤에 붙여준다.
      • e.g. A[i-1] = 312, A[i] = 23 --> 23000
  • 주의할 점: N <= 100 이고 숫자가 10^9 까지 있으므로 최대 10^109 인 숫자가 잇을 수 있기 때문에 문자열로 처리해야 함
  • 맨 처음에 숫자를 비교할 때 문자열의 경우 앞 글자부터 큰 값이면 큰 것으로 간주하기 때문에 먼저 길이를 비교해주어야 함
    • A[i]의 길이가 길다면 skip
    • A[i-1]과 A[i]의 길이가 같은데 A[i]가 사전순으로 뒤에 오면 skip
    • A[i-1]과 A[i]의 길이가 같은데 A[i]가 사전순으로 앞에 오면 A[i]의 뒤에 0을 붙임
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

const static int _ = []() {
    cin.tie(0); cout.tie(0); ios_base::sync_with_stdio(false);
    return 0;
}();

void solve() {
    int N;
    cin >> N;
    string A[N];
    for (int i = 0; i < N; i++) cin >> A[i];
    int ans = 0;
    for (int i = 1; i < N; i++) {
        int prev_len = A[i-1].size();
        int curr_len = A[i].size();
        if ((prev_len < curr_len) or (prev_len == curr_len and A[i-1] < A[i])) continue;
        // A[i-1] >= A[i]
        string prefix = A[i-1].substr(0, curr_len);
        if (prefix > A[i]) {
            int num_zeors = prev_len - curr_len + 1;
            A[i] += string(num_zeors, '0');
            ans += num_zeors;
        } else if (prefix < A[i]) {
            int num_zeors = prev_len - curr_len;
            A[i] += string(num_zeors, '0');
            ans += num_zeors;
        } else {
            int sz = prev_len - curr_len + 1;
            vector<int> v(sz, 0);
            v[0] = 1;
            // 계산 편의를 위해 맨 뒤의 숫자부터 계산
            for (int j = prev_len-1; j >= prefix.size(); j--) {
                int v_pos = prev_len - 1 - j;
                int x = A[i-1][j] - '0';
                if (x + v[v_pos] > 9) v[v_pos+1] = 1; // carry
                v[v_pos] = (x + v[v_pos]) % 10;
            }
            if (v.back() == 1) { // prefix 부분에서 carry가 발생한 경우
                A[i] += string(sz, '0');
                ans += sz;
            } else {
                v.pop_back(); // except carry
                while (!v.empty()) {
                    A[i].push_back(v.back() + '0');
                    v.pop_back();
                }
                ans += sz - 1;
            }
        }
    }
    cout << ans << endl;
}

int main() {
    int T;
    cin >> T;
    for (int tc = 1; tc <= T; tc++) {
        cout << "Case #" << tc << ": ";
        solve();
    }
    return 0;
}

 

Prime Time

  • 문제 설명: 카드를 두 그룹으로 나누는데, 한 그룹의 카드에 적힌 숫자의 합은 다른 그룹의 카드에 적힌 숫자의 곱과 같아야 한다. 카드에 적힌 숫자는 모두 소수이며, 같은 숫자를 가진 카드가 존재할 수 있다. 각 카드는 반드시 하나의 그룹에만 속해야 하며, 각 그룹은 적어도 하나의 카드를 포함해야 한다. 카드가 하나인 경우 그룹의 합이나 곱은 그 카드에 적힌 숫자이다. 두 그룹 중 카드에 적힌 숫자들의 합을 점수로 할 때 최대 점수를 구하라.
    • 입력으로 M 이 주어지고, 그 다음 M 줄에는 각각 Pi, Ni 가 주어진다. Pi는 카드에 적힌 숫자이며, Ni는 Pi가 적힌 카드의 개수이다.
    • Test Set 1: 카드의 총 개수는 2 ~ 10 사이이다.
    • Test Set 2: 카드의 총 개수는 2 ~ 100 사이이다.
    • Test Set 3: 카드의 총 개수는 2 ~10^15 사이이다.
    • Pi는 2~499 사이의 소수이다.
  • 첫번째 접근법: naive하게 각 카드를 하나씩 나누어서 두 그룹의 합과 곱을 계속 비교해서 최댓값을 탐색한다. Test Set 1에서 카드의 총 개수는 최대 10으로 두 그룹으로 나누는 경우의 수는 10C1 + 10C2 + ... + 10C9 = 914 이고 나눌 때마다 합과 곱을 비교해야 하므로 10회 반복문이 필요하게 된다. 시간 복잡도는 최대 9140 으로 충분히 해결할 수 있다.
#include <iostream>
#include <vector>
#include <algorithm>
#define sz(c) (int)(c).size()
using namespace std;
typedef long long ll;

const static int _ = []() {
    cin.tie(0); cout.tie(0); ios_base::sync_with_stdio(false);
    return 0;
}();

ll best;
void dfs(int pos, int checked, ll sum, vector<int>& deck) {
    ll product = 1;
    for (int i = 0; i < sz(deck); i++) {
        if (checked & (1 << i)) continue;
        product *= deck[i];
    }
    if (product == 1) return;
    if (sum == product) best = max(sum, best);
    for (int i = pos; i < sz(deck); i++)
        dfs(i+1, checked | (1 << i), sum + deck[i], deck);
}

void solve() {
    int M;
    cin >> M;
    vector<int> v;
    for (int i = 0; i < M; i++) {
        int p, n;
        cin >> p >> n;
        while (n--) v.push_back(p);
    }
    best = 0;
    dfs(0, 0, 0, v);
    cout << best << endl;
}

int main() {
    int T;
    cin >> T;
    for (int tc = 1; tc <= T; tc++) {
        cout << "Case #" << tc << ": ";
        solve();
    }
    return 0;
}
  • 두번째 접근법: Test Set 2 는 N <= 100 이기 때문에 위의 풀이대로 할 경우 시간 초과로 해결할 수 없다. 또한 2가 100번 곱해진다면 곱의 최댓값이 long long의 범위를 벗어날 수 있다. 그러나 합은 카드의 최대 소수인 499가 100번 더해지면 49900 (합의 최댓값) 이기 때문에 어차피 49900 보다 큰 곱은 볼 필요가 없음을 알 수 있다. 위의 풀이에서 주어진 카드의 모든 숫자를 더한 합보다 지금 곱이 크거나 같을 경우 가지치기를 함으로써 시간 복잡도를 줄일 수 있다.
    • 이전과 다르게 최대 합에서 하나씩 원소를 빼서 다른 그룹으로 옮기는 경우를 탐색한다. (계산의 편의를 위함)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;

const static int _ = []() {
    cin.tie(0); cout.tie(0); ios_base::sync_with_stdio(false);
    return 0;
}();

ll dfs(int pos, ll sum, ll product, vector<int>& deck) {
    if (sum < product) return 0;
    if (sum == product) return sum;
    ll ret = 0;
    for (int i = pos; i < deck.size(); i++)
        ret = max(ret, dfs(i+1, sum - deck[i], product * deck[i], deck));
    return ret;
}

void solve() {
    int M;
    cin >> M;
    vector<int> deck;
    ll upper_bound = 0;
    for (int i = 0, p, n; i < M; i++) {
        cin >> p >> n;
        upper_bound += p*n;
        while (n--) deck.push_back(p);
    }
    cout << dfs(0, upper_bound, 1, deck) << endl;
}

int main() {
    int T;
    cin >> T;
    for (int tc = 1; tc <= T; tc++) {
        cout << "Case #" << tc << ": ";
        solve();
    }
    return 0;
}
  • 세번째 접근법: Test Set 3 에서 N은 최대 10^15 이기 때문에 위의 방법은 메모리 초과 및 시간 초과로 문제를 해결할 수 없다. 약간 수학적인 추리가 필요한데, 첫번째 그룹의 카드 합과 두번째 그룹의 카드 곱이 같다는 점과 카드에 적힌 숫자의 범위 2~499 사이의 소수라는 점에서 다음과 같은 정보를 알 수 있다.
    • N = 10^15 일 때, 첫번째 그룹이 최대로 가질 수 있는 숫자 합은 499 * 10^15 이며, 이는 2^60 보다 작은 수이다.
    • 따라서 두번째 그룹의 카드를 곱해서 만들어질 수 있는 숫자도 2^60 보다 작다. 물론 모든 카드가 2일 때는 모든 카드가 499일 수 없기 때문에 그룹 곱의 상한으로 생각하면 될 것이다. 아무튼 2^60 보다 작다는 의미는 모든 숫자가 2일 때 카드가 60장 쓰일 때보다 적은 개수의 카드가 사용될 수 밖에 없다. 즉, 두번째 그룹의 카드는 최대 60장이다.
    • 두번째 그룹의 카드에 적힌 숫자의 합은 최대 499 * 60 = 29940 이다. 여기서 60은 소수 2만 가정한 것이므로 실제로는 훨씬 적은 값일 것이다.
    • 첫번째 그룹의 카드에 적힌 숫자 합은 모든 카드의 숫자 합에서 두번째 그룹의 카드에 적힌 숫자 합을 뺀 것과 같다. 따라서 첫번째 그룹의 카드에 적힌 숫자 합은 최소 "모든 카드의 숫자 합 - 29940"이 된다. 물론 주어진 카드로 29940을 만들 수 없으면 최대 만들 수 있는 값이 될 것.
    • 정리하자면, 2~29940 사이의 숫자를 X라고 할 때 "모든 카드의 숫자 합 - X"가 주어진 카드로 나누어 떨어지는 경우(=두번째 그룹의 카드들을 곱해서 만들 수 있는 경우)가 있다면 가장 작은 X에 대해 "모든 카드의 숫자 합 - X"가 첫번째 그룹의 카드로부터 나온 최대합이 된다.
      • 모든 카드에 적힌 숫자의 합이 29940 보다 작으면 그 합을 X의 상한으로 두면 된다.
      • 29940번 반복문을 돌리면서 모든 카드마다 나누어 떨어지는지 시도하면 되는데, 카드의 종류는 최대 95가지이므로 충분히 시간 안에 해결 할 수 있다. 2~499 사이의 소수는 95개이다.
      • "모든 카드의 숫자 합 - X" 에서 나누어 떨어지는 카드를 모두 합한 값이 X가 된다면, 나누어 떨어지지 않은 카드의 합은 반드시 "모든 카드의 숫자 합 - X"가 되기 때문에 두번째 그룹에 포함될 카드만 찾아내도 문제를 해결할 수 있다.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;

const static int _ = []() {
    cin.tie(0); cout.tie(0); ios_base::sync_with_stdio(false);
    return 0;
}();

void solve() {
    int M;
    cin >> M;
    ll deck[M][2];
    ll ans = 0, upper_bound = 29940, sum = 0;
    for (int i = 0; i < M; i++) {
        cin >> deck[i][0] >> deck[i][1];
        sum += deck[i][0] * deck[i][1];
    }

    if (sum < upper_bound)
        upper_bound = sum;

    for (ll x = 2; x < upper_bound; x++) {
        ll product = sum - x;
        ll card_sum = 0; // the sum of cards in product group
        bool matched = true;
        for (int i = 0; i < M and matched; i++) {
            int num_used_cards = 0;
            while (product % deck[i][0] == 0) {
                product /= deck[i][0];
                card_sum += deck[i][0];
                num_used_cards++;
            }
            if (num_used_cards > deck[i][1])
                matched = false;
        }
        if (matched and product == 1 and card_sum == x) {
            ans = sum - x;
            break;
        }
    }

    cout << ans << endl;
}

int main() {
    int T;
    cin >> T;
    for (int tc = 1; tc <= T; tc++) {
        cout << "Case #" << tc << ": ";
        solve();
    }
    return 0;
}

 

Hacked Exam

  • 문제 설명: N명의 학생이 Q개의 질문에 대답한 결과와 점수가 주어질 때, 학생들의 답변을 참고하여 점수의 기댓값이 최대가 되도록 답변을 구성하여 출력. 점수는 z/w 형태로 기약분수로 표현해야 함.
    • Test Set 1: 1 <= N <= 2, 1 <= Q <= 10
    • Test Set 2: 1 <= N <= 2, 1 <= Q <= 40
    • Test Set 3: 1 <= N <= 3, 1 <= Q <= 120
    • 질문의 답변은 T와 F로 2가지 이며, 각 학생들의 시험 결과는 T와 F로 구성된 Q 글자의 문장이다.
    • 점수는 각 문제당 1점이며, 각 학생의 점수 Si 는 0~Q 사이의 숫자이다.
  • 접근법: (참.. 문제 이해하는 것도 오래 걸리고, 해석을 보고 이해하는 것도 오래 걸린 문제)
    • 기댓값의 정의를 살펴보면, 각 사건이 발생했을 때 얻는 이득과 그 사건이 발생할 확률을 곱한 것의 합이라고 한다. 여기서 사건은 어떤 질문 q에 대한 답변이 참(true)인 경우를 의미하며, 그 사건이 발생할 확률이란 어떤 질문 q에 대한 답변이 참(true)일 확률을 뜻한다. 사건이 발생했을 얻는 이득은 1점이기 때문에 기댓값을 구할 때 확률의 합만 계산하면 된다.
    • 질문 q에 대한 답변이 참일 확률을 Pq라고 할 때, 최대 기댓값 := max(P1 + P2 + ... + Pq)
    • 단, 답변이 T와 F로 2가지이기 때문에 각 질문에 대한 확률은 답변이 T일 때 참일 확률과 답변이 F일 때 참일 확률 중 큰 값으로 해야 기댓값이 최대가 된다. 즉, Pi = max(Pi(ans = T), Pi(ans = F)) 이고 E(Q) = P1 + P2 + ... + Pq
    • 그러면 답변이 참일 확률은 어떻게 구할 수 있는가? 학생의 점수를 이용
      • 어떤 학생의 점수가 S일 때, 각 질문에 대한 그 학생의 답변은 S / Q 의 확률을 가진다. 질문 q에 대한 답이 T일 때 참일 확률을 Pt 라고 하고 답이 F일 때 확률을 Pf 라고 하자. 만약 이 학생이 어떤 질문에 T 라고 답했다면 Pt = S / Q 가 되고, 반대로 Pf = 1 - Pt = 1 - S / Q 가 된다.
    • e.g. 1명의 학생의 답변이 FFT 이고 점수가 2점일 때, 3개의 질문 중 2개가 정답이니 학생이 답변한 F, F, T 각각이 참이 될 확률은 2/3이다.
      • 1번 질문에 답변 F가 참일 확률: Pf = 2/3  <=> 반대로 답변 T가 참일 확률: Pt = 1/3
      • 2번 질문에 답변 F가 참일 확률: Pf = 2/3  <=> 반대로 답변 T가 참일 확률: Pt = 1/3
      • 3번 질문에 답변 F가 참일 확률: Pf = 1/3  <=> 반대로 답변 T가 참일 확률: Pt = 2/3
      • 매 질문마다 최대가 되는 확률값을 골라서 더할 경우, E(Q) = Pf + Pf + Pt = 2/3 + 2/3 + 2/3 = 6/3 = 2/1 = 2 가 된다.
      • 이 예시에서 기댓값이 최대가 되는 답변 케이스가 FFT 인 것은 자명하다.
    • 그러나 반드시 확률을 구할 필요가 없다. 단순히 생각해보면 1명의 학생이 절반이상 맞췄다면 그 학생의 답변을 그대로 제출하는게 할 수 있는 최대의 점수를 내는 것이다. 반대로 절반도 못 맞췄다면 그 학생의 답변과는 정반대로 답하면 "Q - 학생의 점수"를 얻을 수 있을 것이고 이 점수가 최대의 예상 점수가 된다.
      • 어차피 확률을 계산해도 점수가 낮다면 학생의 반대 답변을 정답으로 하게 된다.
  • 만약 2명 학생의 답변을 가지고 있다면?
    • 여기서부터는 크게 2가지로 나뉘어서 생각해야 한다. 두 학생의 답변이 같은 경우두 학생의 답변이 다른 경우이다.
    • 두 학생의 답변이 같은 경우 N = 1 일 때와 비슷하게 동작하면 되는데, 두 학생의 점수를 합한 것이 절반보다 크다면 같은 답변이 많을 수 밖에 없고 이 답변을 그대로 제출해야 예상 점수를 최대화할 수 있을 것이다. 반대로 두 학생의 점수를 합한 것이 절반도 못된다면 당연히 반대로 답변을 제출해야 한다. (둘이 합쳐서 절반도 못맞췄다면 반대 답변을 내야 한다.)
    • 두 학생의 답변이 다른 경우는 오히려 더 쉽다. 점수가 높은 학생의 답변을 그대로 제출하면 된다.
  • 위의 해설이 수학적으로 타당하다는 것은 다음과 같이 증명된다. (원문 링크)
    • 두 학생이 같은 대답을 한 질문의 집합을 A라고 할 때, A 중 x개의 질문의 답변이 참이라면 (여기서 두 학생 모두 x점을 획득) A에 속한 질문에 올바르게 답할 확률 Pq = x / |A| 이다. 반대로 올바르지 않게 답할 확률은 1 - Pq 이다. 여기서 우리는 두 확률 중 더 큰 것을 골라야 한다. 따라서 올바른 답변의 기댓값은 매 질문마다 큰 값을 더한 결과이다.
      • 그리고 더 큰 값이 결국에는 두 학생의 점수를 합했을 때 Q보다 크면 학생들의 답변과 같은 것이 되고, Q보다 작으면 학생들의 답변의 반대가 된다.
      • 각 질문의 점수가 1점이기 때문에 실제로 확률을 합한 것은 얻은 점수이므로 확률을 계산해줄 필요는 없다.
      • sum(max(Pq, 1-Pq)) for q in A --> max(x, |A| - x)
    • A 에 속하지 않은 질문들에 대해 (두 학생이 답변을 다르게 한 질문들) 두 학생은 반드시 Si - x 점수를 가져야 한다. 그리고 각 학생이 남은 질문에 대해 올바르게 답할 확률은 Pi,q = (Si - x) / (Q - |A|) 이다. 반대로 올바르게 답하지 않을 확률은 1 - Pi,q 이다.
    • 따라서 나머지 질문에 대해 올바른 답변을 할 기댓값은 sum(max(P1q, 1 - P1q, P2q, 1 - P2q)) for q not in A 이고 위와 마찬가지로 확률을 실제로 계산해줄 필요 없이 점수로 나타낼 수 있다. --> max(S1 - x, S2 - x, Q - |A| - (S1 - x), Q - |A| - (S2 - x))
    • 이를 그림으로 표현해보자. 편의상 동일한 답변을 한 질문들이 왼쪽에 있고, 다르게 답변한 질문이 오른쪽에 있다고 가정한다.
      • 먼저 왼쪽 그림에서 두 학생이 같은 답변을 한 경우 실제로 비교해야할 확률은 2가지이다. X개의 질문들을 맞췄을 때 참일 확률과 A-X개의 질문들을 맞췄을 때 참일 확률이 비교 대상이다. 언급했다시피 실제 확률의 합은 결국 점수 합과 같으므로 X와 A-X 를 비교하면 된다.
      • 다음 오른쪽 그림에서 두 학생이 다른 답변을 한 경우 비교해야할 확률은 4가지이다. X개의 질문을 이미 맞추었다고 가정하면 다르게 답변한 질문들에서 두 학생이 얻은 점수는 S1-X 와 S2-X가 될 것이고 이 점수를 얻은 질문들은 서로 겹치지 않는다. 즉, 상호 배타적이다. 그리고 점수를 얻지 못한 질문에 대해 참일 확률은 1 - P(S1-X) 로 쉽게 계산할 수 있고 아래 그림에서 나타낸 4개의 박스에 대해 확률 중 큰 값을 선택하면 된다. 마찬가지로 확률의 합은 점수 합과 같으므로 정리해보면 Q-A 영역에 속한 질문에 대한 점수의 최대 기댓값은 max(max(S1-X, Q-A-(S1-X)), max(S2-X, Q-A-(S2-X))) 가 된다.

  • 그렇다면, X는 어떨 때 가장 크고 작은지 알아낼 필요가 있다. 물론 재귀 함수로 하나씩 다 시도해볼 수 있겠지만 다음의 수식에서 쉽게 상한과 하한을 찾을 수 있다.
    • S1-X = max(max(S1-X, Q-A-(S1-X)), max(S2-X, Q-A-(S2-X)))
      • S1 - x >= Q - |A| - (S2 - x)
      • S1 + S2 - (Q - |A|) >= 2x
      • (S1 + S2 - (Q - |A|)) / 2 >= x
      • x의 상한 = (S1 + S2 - Q - |A|) / 2
    • S2-X = max(max(S1-X, Q-A-(S1-X)), max(S2-X, Q-A-(S2-X)))
      • S2 - x >= Q - |A| - (S1 - x)
      • S2 + S1 - (Q - |A|) >= 2x
      • (S1 + S2 - (Q - |A|)) / 2 >= x
      • x의 상한 = (S1 + S2 - Q - |A|) / 2
    • Q - |A| - (S1 - x) = max(max(S1-X, Q-A-(S1-X)), max(S2-X, Q-A-(S2-X)))
      • Q - |A| - (S1 - x) >= S2 - x
      • S1 + S2 - (Q - |A|) <= 2x
      • (S1 + S2 - (Q - |A|)) / 2 <= x
      • x의 하한 = (S1 + S2 - (Q - |A|)) / 2
    • Q - |A| - (S2 - x) = max(max(S1-X, Q-A-(S1-X)), max(S2-X, Q-A-(S2-X)))
      • Q - |A| - (S2 - x) >= S1 - x
      • S1 + S2 - (Q - |A|) <= 2x
      • (S1 + S2 - (Q - |A|)) / 2 <= x
      • x의 하한 = (S1 + S2 - (Q - |A|)) / 2

 

  • 다행스럽게 수식이 공통되기 때문에 x를 쉽게 구할 수 있고 이로써 A에 속한 질문에 대해 답변이 참일 확률과 Q-A 에 속한 질문에 대해 답변이 참일 확률도 쉽게 구할 수 있게 된다.
  • 기댓값의 선형성에 따르면 E(X+Y) = E(X) + E(Y) 이기 때문에 두 확률을 구해서 더하면 원하는 최대 기댓값이 된다.
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#define pb push_back
using namespace std;
typedef long long ll;

const static int _ = []() {
    cin.tie(0); cout.tie(0); ios_base::sync_with_stdio(false);
    return 0;
}();

struct Student {
    vector<bool> a;
    int score;
    void read(int q) {
        for (int i = 0; i < q; i++) {
            char ch; cin >> ch;
            a.pb(ch == 'T');
        }
        cin >> score;
    }
};

void solve() {
    int N, Q;
    cin >> N >> Q;
    vector<Student> students(N);
    for (Student& s : students) s.read(Q);
    string ans;
    ll z;
    if (N == 1) {
        int s = students[0].score;
        for (bool b : students[0].a)
            if (s * 2 >= Q) ans.pb(b ? 'T' : 'F');
            else ans.pb(b ? 'F' : 'T');
        z = s * 2 >= Q ? s : Q - s;
    } else if (N == 2) {
        int c0 = 0, c1 = 0;
        int s0 = students[0].score, s1 = students[1].score;
        for (int i = 0; i < Q; i++) {
            bool b;
            if (students[0].a[i] == students[1].a[i]) {
                b = s0 + s1 >= Q ? students[0].a[i] : !students[0].a[i];
                c0++;
            } else {
                b = s0 > s1 ? students[0].a[i] : students[1].a[i];
                c1++;
            }
            ans.pb(b ? 'T' : 'F');
        }
        int x = (s0 + s1 - c1) >> 1;
        z = max(x, c0-x) + max(max(s0-x, c1-(s0-x)), max(s1-x, c1-(s1-x)));
    }
    cout << ans << " " << z << "/1" << endl;
}

int main() {
    int T;
    cin >> T;
    for (int tc = 1; tc <= T; tc++) {
        cout << "Case #" << tc << ": ";
        solve();
    }
    return 0;
}
  • 세번째 테스트셋은 3명의 학생에 대한 것인데, 이는 문제 해설을 봐도 이해가 되지 않는다. (누군가 알고 있다면 지식을 공유해주었으면 좋겠다.)

 

 

문제

  • 문제 링크
  • 문제 설명: 직원들의 하루평균 매출액 값을 담은 배열 sales, 직원들의 팀장-팀원의 관계를 나타내는 2차원 배열 links가 매개변수로 주어집니다. 이때, 모든 팀에서 최소 한 명 이상 워크숍에 참석하면서, 참석하는 직원들의 하루평균 매출액의 합을 최소로 하려고 합니다. 그렇게 최소화된 매출액의 합을 구해서 반환하는 함수를 작성
    • 두 팀에 속한 1명은 팀장 이면서 팀원 이어야 함 -> 참석하면 두 팀이 참석한 것
    • CEO는 항상 1번
  • 입력
    • sales 배열의 크기는 2 이상 300,000 이하
    • sales 배열의 크기는 CEO를 포함한 전체 직원 수
    • sales 배열의 각 원소의 값은 0 이상 10,000 이하인 정수
    • links 배열의 크기는 sales 배열의 크기 - 1
    • links 배열의 각 원소는 [a, b] 형식, a는 팀장의 직원번호, b는 a팀장이 관리하는 팀원의 직원번호
    • links 배열로 만들어지는 조직도는 하나의 트리 구조 형태
  • 출력: 모든 팀이 워크숍에 참석하도록 할 때 매출액의 최소합

sales links result
[14, 17, 15, 18, 19, 14, 13, 16, 28, 17] [[10, 8], [1, 9], [9, 7], [5, 4], [1, 5], [5, 10], [10, 6], [1, 3], [10, 2]] 44
[5, 6, 5, 3, 4] [[2,3], [1,4], [2,5], [1,2]] 6
[5, 6, 5, 1, 4] [[2,3], [1,4], [2,5], [1,2]] 5
[10, 10, 1, 1] [[3,2], [4,3], [1,4]] 2

풀이

  • sales 배열이 크기 때문에 노드를 최소 1번 또는 상수 횟수로 탐색해야 함.
  • 부모-자식 관계 == 팀 (루트: 팀장[ceo], 비단말: 팀장/팀원, 단말: 팀원)
  • 모든 팀을 포함하도록 노드를 선택할 때 최소합 = min(자신이 참석한 경우 현재 트리의 최소합, 참석안할 때 현재 트리의 최소합)
  • DFS의 반환값: (자신이 참석한 경우 현재 트리의 최소합, 참석안할 때 현재 트리의 최소합) = (x,y)
    • 단말 노드: (자신의 노드값, 0) = (x,y)
    • 비단말, 루트 노드
      • x = 자신의 노드값 + sum(min(서브트리에서 자식이 참석o 최소합, 자식이 참석x 최소합))
      • y = min(i번째 자식이 참석o 최소합 + sum(min(j번째 자식이 참석o 최소합, j번째 자식이 참석x 최소합)))
        • for 모든 자식 노드, i != j
  • 비단말 노드는 자식 노드를 1번씩 모두 순회해야 되므로 DFS로 트리의 노드를 2번씩만 탐색하게 된다. → O(N)
  • 주의할 점: 각 원소가 최대 10000을 가지고, sales의 전체 크기가 300,000 이므로 중간 결과가 정수 자료형의 범위를 벗어날 수 있음
  • 정답 코드
#include <string>
#include <vector>
#define min(x,y) x < y ? x : y
using namespace std;
typedef long long ll;
typedef pair<ll,ll> pll;
vector<vector<int>> adj;

pll dfs(int root, vector<int>& sales) {
    pll ret = {sales[root], 0};
    if (adj[root].size() == 0) return ret;
    vector<pll> subtrees;
    for (auto& child : adj[root]) {
        pll subtree = dfs(child, sales);
        // 자신이 참석한 경우 서브 트리의 두 경우 중 작은 것을 포함
        ret.first += min(subtree.first, subtree.second);
        subtrees.push_back(subtree);
    }
    ret.second = 1e9;
    for (int i = 0; i < subtrees.size(); i++) {
        int minSum = subtrees[i].first; // i번째 자식이 반드시 참석한 경우
        // 그 외 다른 서브 트리의 최소합의 합을 구함
        for (int j = 0; j < adj[root].size(); j++) {
            if (i == j) continue;
            minSum += min(subtrees[j].first, subtrees[j].second);
        }
        // 모든 자식들 중 서브트리의 최소합이 가장 작은 것 계산
        ret.second = min(ret.second, minSum);
    }
    return ret;
}

int solution(vector<int> sales, vector<vector<int>> links) {
    adj = vector<vector<int>> (sales.size(), vector<int>());
    for (auto& v : links) {
        adj[v[0]-1].push_back(v[1]-1);
    }
    pll ret = dfs(0, sales);
    return min(ret.first, ret.second);
}

 

 

+ Recent posts