Codeforces Round #574 (Div. 2) 기술 블로그 및 문제 풀이

Problem A: Drinks Choosing

N명의 학생들이 각자 선호하는 음료 맛이 있습니다. 총 $\lceil n/2 \rceil$개의 세트가 제공되며, 각 세트에는 같은 맛의 음료 2병이 들어 있습니다. 목표는 최대한 많은 학생이 자신이 원하는 맛의 음료를 받을 수 있도록 배분하는 것입니다.

가장 효율적인 방법은 동일한 맛을 원하는 학생들을 2명씩 묶어 한 세트를 주는 것입니다. 이렇게 짝을 지어 배분한 뒤 남은 세트들은 각각 한 명씩의 요구사항만 충족시킬 수 있습니다. 즉, 각 맛의 빈도수를 계산하여 짝수 명의 그룹을 먼저 처리하고, 남은 홀수 명의 인원들은 남은 세트 수만큼 추가로 만족시킬 수 있습니다.

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int student_count, flavor_limit;
    cin >> student_count >> flavor_limit;

    vector<int> counts(flavor_limit + 1, 0);
    for (int i = 0; i < student_count; ++i) {
        int favorite;
        cin >> favorite;
        counts[favorite]++;
    }

    int satisfied = 0;
    int remaining_odds = 0;
    int total_sets = (student_count + 1) / 2;

    for (int i = 1; i <= flavor_limit; ++i) {
        satisfied += (counts[i] / 2) * 2;
        total_sets -= counts[i] / 2;
        if (counts[i] % 2 == 1) {
            remaining_odds++;
        }
    }

    satisfied += total_sets;
    cout << satisfied << endl;

    return 0;
}

Problem B: Sport Mafia

사탕 상자에 두 가지 작업을 수행할 수 있습니다. 첫 번째 작업은 사탕을 추가하는 것인데, 이전에 추가한 개수보다 1개 더 많이 넣습니다(1, 2, 3... 순서). 두 번째 작업은 사탕을 1개 꺼내는 것입니다. 총 $n$번의 작업을 수행한 후 상자에 $k$개의 사탕이 남았을 때, 사탕을 꺼낸 횟수를 구해야 합니다.

사탕을 추가한 횟수를 $x$라고 가정하면, 꺼낸 횟수는 $n - x$가 됩니다. 추가된 총 사탕의 개수는 등차수열의 합 공식에 의해 $x(x+1)/2$입니다. 따라서 최종 사탕 개수 $k$는 다음과 같은 방정식으로 나타낼 수 있습니다: $x(x+1)/2 - (n - x) = k$. 이 방정식에서 $x$를 구하기 위해 이분 탐색을 활용하거나 근의 공식을 사용할 수 있습니다.

#include <iostream>

using namespace std;

typedef long long ll;

int main() {
    ll n, k;
    cin >> n >> k;

    ll low = 0, high = n, put_count = 0;
    while (low <= high) {
        ll mid = (low + high) / 2;
        ll current_candies = mid * (mid + 1) / 2 - (n - mid);
        if (current_candies == k) {
            put_count = mid;
            break;
        } else if (current_candies < k) {
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }

    cout << n - put_count << endl;
    return 0;
}

Problem C: Basketball Exercise

2행 $n$열로 배치된 학생들 중에서 합산 키가 최대가 되도록 팀을 구성해야 합니다. 선택 시 두 가지 제약 조건이 있습니다. 첫째, 동일한 열(column)에서는 최대 한 명만 선택할 수 있습니다. 둘째, 바로 직전 열에서 선택한 행(row)과 같은 행에 있는 학생은 연속해서 선택할 수 없습니다.

이 문제는 다이나믹 프로그래밍(DP)으로 해결할 수 있습니다. 각 열 $i$에 대해 세 가지 상태를 정의합니다.

  • $dp[i][0]$: $i$번째 열에서 1행 학생을 선택한 경우의 최대 합
  • $dp[i][1]$: $i$번째 열에서 2행 학생을 선택한 경우의 최대 합
  • $dp[i][2]$: $i$번째 열에서 아무도 선택하지 않은 경우의 최대 합

점화식은 직전 열의 서로 다른 행을 선택했거나 건너뛴 경우 중 최댓값을 취하는 방식으로 구성됩니다.

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

typedef long long ll;

int main() {
    int n;
    cin >> n;

    vector<ll> row1(n), row2(n);
    for (int i = 0; i < n; ++i) cin >> row1[i];
    for (int i = 0; i < n; ++i) cin >> row2[i];

    vector<vector<ll>> dp(n, vector<ll>(3, 0));
    dp[0][0] = row1[0];
    dp[0][1] = row2[0];
    dp[0][2] = 0;

    for (int i = 1; i < n; ++i) {
        dp[i][0] = max(dp[i-1][1], dp[i-1][2]) + row1[i];
        dp[i][1] = max(dp[i-1][0], dp[i-1][2]) + row2[i];
        dp[i][2] = max({dp[i-1][0], dp[i-1][1], dp[i-1][2]});
    }

    cout << max({dp[n-1][0], dp[n-1][1], dp[n-1][2]}) << endl;
    return 0;
}

Problem D: Submarine in the Rybinsk Sea

두 숫자 $a$와 $b$를 섞는 함수 $f(a, b)$는 $b$의 마지막 자리, $a$의 마지막 자리 순으로 번갈아 가며 배치하여 새로운 숫자를 만듭니다. 배열 내의 모든 쌍 $(i, j)$에 대해 $f(a_i, a_j)$의 합을 998,244,353으로 나눈 나머지를 구하는 문제입니다.

핵심은 각 숫자의 각 자릿수가 결과값에 기여하는 정도를 개별적으로 계산하는 것입니다. 어떤 숫자 $a_i$의 $k$번째 자릿수가 $f(a_i, a_j)$에서 어느 위치에 오게 될지는 $a_j$의 길이에 따라 결정됩니다. 배열 내 숫자들의 길이 분포를 미리 파악한 뒤, 각 자릿수마다 가능한 모든 길이에 대한 기여도를 합산하면 $O(N \cdot \text{max\_digit})$의 시간 복잡도로 해결 가능합니다.

#include <iostream>
#include <vector>

using namespace std;

typedef long long ll;
const int MOD = 998244353;

ll powers[25];

void precompute_powers() {
    powers[0] = 1;
    for (int i = 1; i < 25; ++i) powers[i] = (powers[i - 1] * 10) % MOD;
}

int main() {
    int n;
    cin >> n;
    vector<ll> nums(n);
    vector<int> len_counts(11, 0);

    for (int i = 0; i < n; ++i) {
        cin >> nums[i];
        int temp = nums[i], cur_len = 0;
        while (temp) { cur_len++; temp /= 10; }
        len_counts[cur_len]++;
    }

    precompute_powers();
    ll total_sum = 0;

    for (int i = 0; i < n; ++i) {
        for (int L = 1; L <= 10; ++L) {
            if (len_counts[L] == 0) continue;
            
            ll val = nums[i];
            ll contrib_a = 0, contrib_b = 0;
            int pos = 0;
            
            while (val > 0) {
                int digit = val % 10;
                val /= 10;
                
                if (pos < L) {
                    contrib_a = (contrib_a + (ll)digit * powers[2 * pos + 1]) % MOD;
                    contrib_b = (contrib_b + (ll)digit * powers[2 * pos]) % MOD;
                } else {
                    contrib_a = (contrib_a + (ll)digit * powers[pos + L]) % MOD;
                    contrib_b = (contrib_b + (ll)digit * powers[pos + L]) % MOD;
                }
                pos++;
            }
            total_sum = (total_sum + (contrib_a + contrib_b) * len_counts[L]) % MOD;
        }
    }

    cout << total_sum << endl;
    return 0;
}

Problem E: OpenStreetMap

주어진 점화식으로 $n \times m$ 크기의 행렬을 생성하고, 모든 $a \times b$ 크기의 부분 행렬 내 최솟값들의 총합을 구해야 합니다. 행렬의 크기가 최대 $3000 \times 3000$이므로 효율적인 2차원 범위 최솟값 쿼리(Range Minimum Query) 알고리즘이 필요합니다.

단조 큐(Monotonic Queue)를 사용하여 슬라이딩 윈도우 최솟값을 구하는 방식을 적용합니다. 먼저 각 행에 대해 길이가 $b$인 윈도우의 최솟값을 모두 구하여 새로운 행렬을 만듭니다. 그 다음, 생성된 행렬의 각 열에 대해 길이가 $a$인 윈도우의 최솟값을 구하면 각 $a \times b$ 부분 행렬의 최솟값을 $O(NM)$ 시간에 모두 찾을 수 있습니다.

#include <iostream>
#include <vector>
#include <deque>

using namespace std;

typedef long long ll;

int main() {
    int n, m, a, b;
    cin >> n >> m >> a >> b;
    ll g, x, y, z;
    cin >> g >> x >> y >> z;

    vector<vector<int>> grid(n, vector<int>(m));
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            grid[i][j] = g;
            g = (g * x + y) % z;
        }
    }

    vector<vector<int>> row_min(n, vector<int>(m - b + 1));
    for (int i = 0; i < n; ++i) {
        deque<int> dq;
        for (int j = 0; j < m; ++j) {
            while (!dq.empty() && grid[i][dq.back()] >= grid[i][j]) dq.pop_back();
            dq.push_back(j);
            if (dq.front() <= j - b) dq.pop_front();
            if (j >= b - 1) row_min[i][j - b + 1] = grid[i][dq.front()];
        }
    }

    ll ans = 0;
    int new_m = m - b + 1;
    for (int j = 0; j < new_m; ++j) {
        deque<int> dq;
        for (int i = 0; i < n; ++i) {
            while (!dq.empty() && row_min[dq.back()][j] >= row_min[i][j]) dq.pop_back();
            dq.push_back(i);
            if (dq.front() <= i - a) dq.pop_front();
            if (i >= a - 1) ans += row_min[dq.front()][j];
        }
    }

    cout << ans << endl;
    return 0;
}

태그: Codeforces competitive-programming DP Monotonic-Queue binary-search

6월 14일 17:30에 게시됨