NOI2025 예선 대비 문제 풀이 정리

[NOI2025 예선 R1] A - 기본 사이클 구조

다음의 수학적 원리를 활용한다:

Cayley 정리
n개의 노드가 k개의 연결 성분으로 구성되어 있을 때, 이들을 연결하기 위해 k-1개의 간선을 추가하는 방법의 수는 n^(k−2) × ∏(i=1 to k) size_i이다.

이 정리를 바탕으로, 입력 그래프에 이미 사이클이 존재하는 경우 답을 직접 계산할 수 있다.

반면, 초기 상태에서 사이클이 없는 경우, 가능한 사이클 조합을 모두 탐색해야 한다.

각 연결 성분의 크기만 기억한 후, 일부 성분들을 선택하여 사이클을 형성하는 경우를 고려하자.

  • c = 1인 경우: 해당 성분 내부에 간선 하나를 추가하면 된다. 가능한 방법은 sz*(sz-1)/2 - sz + 1.
  • c = 2인 경우: 두 성분 각각에서 한 개씩 간선을 추가하여 연결. 가능한 경우의 수는 (sz₁*sz₂)*(sz₁*sz₂ - 1)/2.
  • c ≥ 3인 경우: 순환 구조를 만들기 위해 (c-1)!/2 가지 회전 및 반전 대칭을 고려하고, 인접한 성분 간 연결 수를 곱해준다.

DP를 통해 c, ∑size를 상태로 하고 ∏size를 값으로 관리하면 시간 복잡도 O(n²)까지 최적화 가능하다.

const int MAXN = 5005;
const int MOD = 1e9 + 7;

int mod_add(int a, int b) { return (a + b) % MOD; }
int mod_sub(int a, int b) { return (a - b + MOD) % MOD; }
int mod_mul(long long a, long long b) { return (a * b) % MOD; }

int power(int base, int exp) {
    int result = 1;
    while(exp) {
        if(exp & 1) result = mod_mul(result, base);
        base = mod_mul(base, base);
        exp >>= 1;
    }
    return result;
}

int n, m;
int parent[MAXN], sz[MAXN];

int root(int x) {
    return parent[x] == x ? x : parent[x] = root(parent[x]);
}

void unite(int a, int b) {
    a = root(a), b = root(b);
    if(a != b) {
        sz[a] += sz[b];
        parent[b] = a;
    }
}

int fact[MAXN], inv_fact[MAXN], powers[MAXN];
int component_sizes[MAXN], total_components;

void preprocess() {
    fact[0] = 1;
    for(int i = 1; i <= n; ++i)
        fact[i] = mod_mul(fact[i - 1], i);

    inv_fact[n] = power(fact[n], MOD - 2);
    for(int i = n - 1; i >= 0; --i)
        inv_fact[i] = mod_mul(inv_fact[i + 1], i + 1);

    powers[0] = 1;
    for(int i = 1; i <= n; ++i)
        powers[i] = mod_mul(powers[i - 1], n);
}

int comb2(int x) {
    return mod_mul(mod_mul(x, x - 1), inv_fact[2]);
}

int dp_table[MAXN][2];

void compute() {
    cin >> n >> m;

    for(int i = 1; i <= n; ++i) {
        parent[i] = i;
        sz[i] = 1;
    }

    bool has_cycle = false;
    for(int i = 0; i < m; ++i) {
        int u, v;
        cin >> u >> v;
        if(root(u) == root(v))
            has_cycle = true;
        else
            unite(u, v);
    }

    preprocess();

    if(has_cycle) {
        int components = 0, product = 1;
        for(int i = 1; i <= n; ++i) {
            if(root(i) == i) {
                components++;
                product = mod_mul(product, sz[i]);
            }
        }
        int answer = mod_mul(product, powers[components - 2]);
        cout << answer << "\n";
        return;
    }

    vector<int> sizes;
    for(int i = 1; i <= n; ++i) {
        if(root(i) == i)
            sizes.push_back(sz[i]);
    }

    sort(sizes.begin(), sizes.end());
    int total_size_product = 1;
    for(auto s : sizes)
        total_size_product = mod_mul(total_size_product, s);

    int answer = 0;

    // c = 1
    for(auto s : sizes) {
        if(s > 2) {
            int ways = mod_sub(comb2(s), s - 1);
            answer = mod_add(answer, mod_mul(ways, mod_mul(total_size_product, powers[sizes.size() - 2])));
        }
    }

    // c = 2
    if(sizes.size() == 2) {
        int prod = mod_mul(sizes[0], sizes[1]);
        answer = mod_add(answer, comb2(prod));
    }

    // c >= 3
    if(sizes.size() >= 3) {
        memset(dp_table, 0, sizeof dp_table);
        dp_table[0][0] = 1;

        for(int i = 0; i < sizes.size(); ++i) {
            for(int j = i + 1; j >= 1; --j) {
                dp_table[j][0] = mod_add(dp_table[j][0], mod_mul(dp_table[j - 1][0], sizes[i]));
                dp_table[j][1] = mod_add(dp_table[j][1], mod_add(
                    mod_mul(dp_table[j - 1][1], sizes[i]),
                    mod_mul(dp_table[j - 1][0], mod_mul(sizes[i], sizes[i]))
                ));
            }
        }

        for(int c = 3; c <= sizes.size(); ++c) {
            int term = mod_mul(dp_table[c][1], mod_mul(powers[sizes.size() - c - 1], total_size_product));
            term = mod_mul(term, mod_mul(fact[c - 1], (MOD + 1) / 2));
            answer = mod_add(answer, term);
        }
    }

    cout << answer << "\n";
}

[NOI2025 예선 R1] B - 작업 롤백

세그먼트 트리를 사용하여 실시간으로 쿼리에 응답하면서도 과거 상태로 되돌릴 수 있는 구조를 설계한다.

const int MAXQ = 300005;
int n, q;
vector<array<int, 3>> updates[MAXQ];
vector<int> queries[MAXQ];
long long answers[MAXQ];

struct SegmentNode {
    int left_len, right_len;
    long long value;
};

class PersistentSegmentTree {
private:
    int left_idx[4 * MAXQ], right_idx[4 * MAXQ];
    SegmentNode tree[4 * MAXQ];
    int node_count = 0;

    long long query_internal(int node, int k) {
        if(k <= 0) return tree[node].value;
        if(k >= tree[node].right_len) return 0;
        int lc = node * 2, rc = node * 2 + 1;
        if(k <= tree[rc].right_len)
            return tree[node].value - tree[rc].value + query_internal(rc, k);
        else
            return query_internal(lc, k - tree[rc].right_len + tree[rc].left_len);
    }

    SegmentNode merge_nodes(int l_node, SegmentNode r_node) {
        if(tree[l_node].right_len <= r_node.left_len) {
            return {tree[l_node].left_len + r_node.left_len - tree[l_node].right_len, r_node.right_len, r_node.value};
        } else {
            long long merged_value = query_internal(l_node, r_node.left_len) + r_node.value;
            return {tree[l_node].left_len, tree[l_node].right_len - r_node.left_len + r_node.right_len, merged_value};
        }
    }

public:
    void build(int node, int l, int r) {
        left_idx[node] = l;
        right_idx[node] = r;
        if(l == r) return;
        int mid = (l + r) / 2;
        build(node * 2, l, mid);
        build(node * 2 + 1, mid + 1, r);
    }

    void update(int node, int pos, int type, int val) {
        if(left_idx[node] == right_idx[node]) {
            if(type == 0) tree[node] = {0, 0, 0};
            if(type == 1) tree[node] = {0, 1, val};
            if(type == 2) tree[node] = {1, 0, 0};
            return;
        }
        int mid = (left_idx[node] + right_idx[node]) / 2;
        if(pos <= mid)
            update(node * 2, pos, type, val);
        else
            update(node * 2 + 1, pos, type, val);
        tree[node] = merge_nodes(node * 2, tree[node * 2 + 1]);
    }

    SegmentNode query_range(int node, int end_pos) {
        if(right_idx[node] <= end_pos) return tree[node];
        int mid = (left_idx[node] + right_idx[node]) / 2;
        if(end_pos <= mid) return query_range(node * 2, end_pos);
        return merge_nodes(node * 2, query_range(node * 2 + 1, end_pos));
    }
} seg_tree;

void process_queries() {
    cin >> n >> q;

    for(int i = 1; i <= q; ++i) {
        int op; cin >> op;
        if(op == 1) {
            int l, r, x;
            cin >> l >> r >> x;
            updates[l].push_back({i, 1, x});
            updates[r + 1].push_back({i, 0, 0});
        } else if(op == 2) {
            int l, r;
            cin >> l >> r;
            updates[l].push_back({i, 2, 0});
            updates[r + 1].push_back({i, 0, 0});
        } else {
            int idx; cin >> idx;
            queries[idx].push_back(i);
        }
    }

    fill(answers, answers + MAXQ, -1);
    seg_tree.build(1, 1, q);

    for(int i = 1; i <= n; ++i) {
        for(auto& upd : updates[i])
            seg_tree.update(1, upd[0], upd[1], upd[2]);

        for(int query_time : queries[i]) {
            auto res = seg_tree.query_range(1, query_time);
            answers[query_time] = res.value;
        }
    }

    for(int i = 1; i <= q; ++i)
        if(answers[i] != -1)
            cout << answers[i] << "\n";
}

[NOI2025 예선 R2] A - 강수량 분석

특정 수열의 합과 팩토리얼 값을 기반으로 결과를 도출하는 문제이다.

const int LIMIT = 200000;
const int PRIME = 998244353;

int mod_add(int a, int b) { return (a + b) % PRIME; }
int mod_sub(int a, int b) { return (a - b + PRIME) % PRIME; }
int mod_mul(long long a, long long b) { return (a * b) % PRIME; }

int factorial[LIMIT + 5];
long long prefix_sums[LIMIT + 5];
int cumulative_factorial[LIMIT + 5];

void initialize(int limit) {
    factorial[0] = 1;
    for(int i = 1; i <= limit; ++i)
        factorial[i] = mod_mul(factorial[i - 1], i);

    for(int i = 1; i <= limit; ++i)
        prefix_sums[i] = prefix_sums[i - 1] + 1LL * i * i;

    for(int i = 1; i <= limit; ++i)
        cumulative_factorial[i] = mod_add(cumulative_factorial[i - 1], mod_mul(i, factorial[i]));
}

long long ceil_div(long long a, long long b) {
    return (a + b - 1) / b;
}

void calculate_rainfall() {
    long long target; cin >> target;
    target++;

    int low = 1, high = LIMIT, position = 1;
    while(low <= high) {
        int mid = (low + high) / 2;
        if(prefix_sums[mid] >= target) {
            position = mid;
            high = mid - 1;
        } else {
            low = mid + 1;
        }
    }

    int result = 0;
    long long quotient = ceil_div(target - prefix_sums[position - 1], position);
    target -= quotient * position;
    result = mod_add(result, mod_mul(quotient, factorial[position]));

    position--;
    if(target == prefix_sums[position]) {
        result = mod_add(result, cumulative_factorial[position]);
    } else {
        int diff = prefix_sums[position] - target;
        result = mod_add(result, cumulative_factorial[position]);
        result = mod_sub(result, factorial[diff]);
    }

    cout << result << "\n";
}

[NOI2025 예선 R2] B - 모래 폭풍 시뮬레이션

다이나믹 프로그래밍과 다익스트라 알고리즘을 결합하여 최단 경로를 구하는 문제이다.

const int NODE_LIMIT = 505;
int iterations, node_count, cost_coefficient;
int heights[NODE_LIMIT], delays[NODE_LIMIT];
long long weights[NODE_LIMIT][NODE_LIMIT];
long long distances[NODE_LIMIT][NODE_LIMIT];
long long start_distances[NODE_LIMIT][NODE_LIMIT];
long long end_distances[NODE_LIMIT][NODE_LIMIT];
long long final_result[NODE_LIMIT][NODE_LIMIT];

bool visited[NODE_LIMIT];

void simulate_sandstorm() {
    cin >> iterations >> node_count >> cost_coefficient;

    for(int i = 1; i <= node_count; ++i)
        cin >> heights[i];

    for(int i = 1; i <= node_count; ++i)
        cin >> delays[i];

    for(int i = 1; i <= node_count; ++i)
        for(int j = 1; j <= node_count; ++j)
            cin >> weights[i][j];

    const long long INF = 1LL << 60;
    for(int i = 1; i <= node_count; ++i)
        for(int j = 1; j <= node_count; ++j)
            final_result[i][j] = INF;

    for(int i = 1; i <= node_count; ++i) {
        for(int j = 1; j <= node_count; ++j) {
            long long edge_weight = weights[i][j] + 1LL * abs(heights[i] - heights[j]) * cost_coefficient;
            distances[i][j] = start_distances[i][j] = end_distances[i][j] = edge_weight;
        }
    }

    for(int source = 1; source <= node_count; ++source) {
        vector<long long> dist(node_count + 1, INF);
        fill(visited, visited + NODE_LIMIT, false);
        dist[source] = 0;

        while(true) {
            int current = -1;
            for(int i = 1; i <= node_count; ++i) {
                if(!visited[i] && (current == -1 || dist[current] > dist[i]))
                    current = i;
            }
            if(current == -1) break;
            visited[current] = true;

            for(int next = 1; next <= node_count; ++next) {
                long long weight = weights[current][next] + 1LL * abs(heights[source] - heights[next]) * delays[next];
                if(dist[next] > dist[current] + weight)
                    dist[next] = dist[current] + weight;
            }
        }

        for(int i = 1; i <= node_count; ++i) {
            distances[source][i] = dist[i];
            start_distances[i][source] = min(start_distances[i][source], dist[i]);
            end_distances[source][i] = min(end_distances[source][i], dist[i]);
        }
    }

    for(int l = 1; l <= node_count; ++l) {
        for(int r = 1; r <= node_count; ++r) {
            for(int mid = 1; mid <= node_count; ++mid) {
                long long combined_cost = distances[l][mid] + distances[r][mid];
                long long left_penalty = 1LL * abs(heights[mid] - heights[l]) * delays[mid];
                long long right_penalty = 1LL * abs(heights[mid] - heights[r]) * delays[mid];
                combined_cost -= max(left_penalty, right_penalty);
                combined_cost += 1LL * abs(heights[l] - heights[r]) * cost_coefficient;
                distances[l][r] = min(distances[l][r], combined_cost);
            }
        }
    }

    for(int k = 1; k <= node_count; ++k)
        for(int i = 1; i <= node_count; ++i)
            for(int j = 1; j <= node_count; ++j)
                distances[i][j] = min(distances[i][j], distances[i][k] + distances[k][j]);

    for(int k = 1; k <= node_count; ++k)
        for(int i = 1; i <= node_count; ++i)
            for(int j = 1; j <= node_count; ++j)
                start_distances[i][j] = min(start_distances[i][j], start_distances[i][k] + distances[k][j]);

    for(int k = 1; k <= node_count; ++k)
        for(int i = 1; i <= node_count; ++i)
            for(int j = 1; j <= node_count; ++j)
                final_result[i][j] = min(final_result[i][j], start_distances[i][k] + end_distances[k][j]);

    for(int i = 1; i <= node_count; ++i) {
        for(int j = 1; j <= node_count; ++j)
            cout << final_result[i][j] << " ";
        cout << "\n";
    }
}

[NOI2025 예선 R2] C - 우주 방사선

소수와 조합론을 기반으로 한 수학적 계산 문제이다.

const int MAX_NUM = 25000005;
const int MOD = 998244353;

int mod_add(int a, int b) { return (a + b) % MOD; }
int mod_sub(int a, int b) { return (a - b + MOD) % MOD; }
int mod_mul(long long a, long long b) { return (a * b) % MOD; }

int power_mod(int base, int exp) {
    int result = 1;
    while(exp) {
        if(exp & 1) result = mod_mul(result, base);
        base = mod_mul(base, base);
        exp >>= 1;
    }
    return result;
}

int num_elements, exponent_k;
int factorials[MAX_NUM], inverse_factorials[MAX_NUM];
int prime_powers[MAX_NUM], mobius_values[MAX_NUM];
int primes_list[MAX_NUM / 10], prime_count;
bool is_composite[MAX_NUM];

void sieve_and_precompute(int limit) {
    factorials[0] = 1;
    for(int i = 1; i <= limit; ++i)
        factorials[i] = mod_mul(factorials[i - 1], i);

    inverse_factorials[limit] = power_mod(factorials[limit], MOD - 2);
    for(int i = limit - 1; i >= 0; --i)
        inverse_factorials[i] = mod_mul(inverse_factorials[i + 1], i + 1);

    is_composite[1] = true;
    mobius_values[1] = 1;

    for(int i = 2; i <= limit; ++i) {
        if(!is_composite[i]) {
            primes_list[++prime_count] = i;
            mobius_values[i] = power_mod(i, exponent_k);
        }
        for(int j = 1; j <= prime_count && primes_list[j] * i <= limit; ++j) {
            is_composite[primes_list[j] * i] = true;
            mobius_values[i * primes_list[j]] = mod_mul(mobius_values[i], mobius_values[primes_list[j]]);
            if(i % primes_list[j] == 0) break;
        }
    }
}

int combination(int n, int k) {
    if(k > n || k < 0) return 0;
    return mod_mul(mod_mul(factorials[n], inverse_factorials[k]), inverse_factorials[n - k]);
}

int partial_sum_array[3 * MAX_NUM][3], derived_values[MAX_NUM];

int compute_partial(int m, int d) {
    long long part1 = 1LL * partial_sum_array[d][0] * (m + 1);
    long long part2 = partial_sum_array[d - 1][2] - partial_sum_array[d - m - 1][2];
    long long part3 = 1LL * (partial_sum_array[d - m - 1][1] - partial_sum_array[d - 1][1]) * (d - m - 1);
    long long part4 = -partial_sum_array[d + m][2] + partial_sum_array[d][2];
    long long part5 = 1LL * (partial_sum_array[d + m][1] - partial_sum_array[d][1]) * (d + m + 1);
    return ((part1 + part2 + part3 + part4 + part5) % MOD + MOD) % MOD;
}

void cosmic_radiation_analysis() {
    cin >> num_elements >> exponent_k;

    sieve_and_precompute(num_elements);

    for(int i = 0; i <= num_elements; ++i) {
        if(!((num_elements + i) & 1))
            partial_sum_array[i][0] = combination(num_elements, (num_elements + i) / 2);
    }

    for(int i = 1; i <= num_elements; ++i)
        partial_sum_array[i][1] = mod_add(partial_sum_array[i - 1][1], partial_sum_array[i][0]);

    for(int i = 1; i <= num_elements; ++i)
        partial_sum_array[i][2] = mod_add(partial_sum_array[i - 1][2], mod_mul(partial_sum_array[i][0], i));

    for(int i = num_elements + 1; i <= 3 * num_elements; ++i)
        partial_sum_array[i][1] = partial_sum_array[i - 1][1];

    for(int i = num_elements + 1; i <= 3 * num_elements; ++i)
        partial_sum_array[i][2] = partial_sum_array[i - 1][2];

    derived_values[1] = 2;

    for(int i = 2; i <= num_elements; ++i) {
        long long value = 0;
        value += 1LL * partial_sum_array[0][0] * (i + 1);
        value -= 1LL * partial_sum_array[i + 1][2] * 2;
        value += 1LL * partial_sum_array[i + 1][1] * (i * 2 + 2);

        int step = i + 2, sign = 1;
        while(step <= num_elements + i) {
            if(sign)
                value -= 2LL * compute_partial(i, step);
            else
                value += 2LL * compute_partial(i, step);
            step += i + 2;
            sign ^= 1;
        }

        derived_values[i] = ((value % MOD) + MOD) % MOD;
    }

    for(int i = num_elements; i >= 1; --i)
        derived_values[i] = mod_sub(derived_values[i], derived_values[i - 1]);

    int final_answer = 0;
    for(int i = 1; i <= num_elements; ++i) {
        int diff = mod_sub(derived_values[i], derived_values[i - 1]);
        final_answer = mod_add(final_answer, mod_mul(mobius_values[i], diff));
    }

    cout << final_answer << "\n";
}

태그: algorithm graph-theory dynamic-programming number-theory competitive-programming

7월 24일 03:13에 게시됨