NOIP 2024 시뮬레이션 경연 문제 분석 및 구현 가이드

철도 2 (Railway 2)

트리 구조에서 모든 노드 쌍 사이의 거리 함수 $f(i,j)$의 총합을 구하는 문제입니다. 이 문제의 핵심은 특정 노드에서 출발할 때 트리의 지름(Diameter) 끝점 중 하나로 향하는 경로가 최적의 해를 포함한다는 점입니다.

트리의 지름을 구한 뒤, 두 끝점을 기준으로 각 노드까지의 거리를 계산하여 정렬합니다. 이를 통해 각 노드별 기여도를 계산하여 최종 합계를 도출할 수 있습니다.

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

vector<pair<int, long long>> adj[MAXN];
long long dist_to_node[MAXN], max_dist[MAXN];
int farthest_point;

void find_farthest(int u, int p, long long d) {
    if (d > dist_to_node[farthest_point]) farthest_point = u;
    dist_to_node[u] = d;
    for (auto& [v, w] : adj[u]) {
        if (v != p) find_farthest(v, u, d + w);
    }
}

long long solve_railway(int n) {
    farthest_point = 1;
    dist_to_node[1] = 0;
    find_farthest(1, 0, 0);
    
    int u = farthest_point;
    dist_to_node[u] = 0;
    find_farthest(u, 0, 0);
    int v = farthest_point;
    
    for(int i=1; i<=n; ++i) max_dist[i] = dist_to_node[i];
    
    dist_to_node[v] = 0;
    find_farthest(v, 0, 0);
    for(int i=1; i<=n; ++i) max_dist[i] = max(max_dist[i], dist_to_node[i]);
    
    sort(max_dist + 1, max_dist + n + 1);
    long long total_sum = 0;
    for (int i = 1; i <= n; ++i) {
        total_sum = (total_sum + (max_dist[i] % MOD) * (n - i)) % MOD;
    }
    return (total_sum * 2) % MOD;
}

경찰과 도둑

추격 문제로, 경찰의 속도 $V1$과 도둑의 속도 $V2$의 관계에 따라 전략이 달라집니다. $V1 \leq V2$라면 도둑은 루트 방향에서 잡히게 되며, 그렇지 않다면 도둑이 도망갈 수 있는 최대 거리를 이분 탐색(Binary Search)으로 결정해야 합니다.

LCA(Lowest Common Ancestor)와 트리의 깊이를 활용하여 각 시점의 위치를 계산하고, 리프 노드 방향으로의 도주와 경로상에서의 체포 가능성을 비교 분석합니다.

섬 (Islands)

간선의 개수와 연결 상태의 특성을 이용하는 문제입니다. 인접한 노드 $(x-1, x+1)$을 연결하는 간선을 찾고, 이를 기준으로 BFS를 수행하여 유효한 채색(Coloring) 방식을 결정합니다.

점프 게임 (Jump Game)

거리가 $K$ 이상인 가중치들의 합을 최대화하는 문제입니다. 세그먼트 트리(Segment Tree)를 사용하여 $K$개의 상태를 유지하며 동적 계획법(DP)을 최적화합니다.

struct SegTree {
    struct Node {
        int left, right;
        long long max_val, lazy;
    } tree[MAXN * 64];
    int node_count = 1;

    void apply_tag(int p, long long v) {
        tree[p].max_val += v;
        tree[p].lazy += v;
    }

    void push_down(int p) {
        if (tree[p].lazy != 0) {
            if (!tree[p].left) tree[p].left = ++node_count;
            apply_tag(tree[p].left, tree[p].lazy);
            if (!tree[p].right) tree[p].right = ++node_count;
            apply_tag(tree[p].right, tree[p].lazy);
            tree[p].lazy = 0;
        }
    }

    void update_range(int &p, int l, int r, int ql, int qr, long long v) {
        if (ql > qr) return;
        if (!p) p = ++node_count;
        if (ql <= l && r <= qr) {
            apply_tag(p, v);
            return;
        }
        push_down(p);
        int mid = (l + r) / 2;
        if (ql <= mid) update_range(tree[p].left, l, mid, ql, qr, v);
        if (qr > mid) update_range(tree[p].right, mid + 1, r, ql, qr, v);
        tree[p].max_val = max(tree[tree[p].left].max_val, tree[tree[p].right].max_val);
    }
};

바이러스 (Virus)

점 분할 트리(Centroid Decomposition)를 구축하고 각 점에서의 깊이 정보를 바탕으로 보조 그래프를 생성합니다. 이후 다익스트라(Dijkstra) 알고리즘을 적용하여 전염 시간을 계산합니다.

친구 (Friends)

물고기들이 멈추는 구간 $[l, r]$이 주어졌을 때, 구간 DP를 사용하여 답을 구합니다. $dp[l][r]$을 구간 $[l, r]$에서의 최댓값으로 정의하고, 구간 내의 최댓값 위치 $k$를 기준으로 분할하여 계산합니다.

$$dp_{l,r} = \max_{l \leq k \leq r} \{ dp_{l,k-1} + dp_{k+1,r} + \binom{s_{l,r,k}}{2} \}$$

사막 점렬 (Desert Point Sequence)

Tarjan 알고리즘을 사용하여 이중 연결 성분(Biconnected Components)을 추출합니다. 사이클에 포함되지 않은 간선들을 우선적으로 제거한 뒤, 사이클의 크기에 따라 탐욕법(Greedy)을 적용하여 정답을 구합니다. 점 이중 연결 성분과 간선 이중 연결 성분의 차이에 주의해야 합니다.

집합 분할 (Set Partition)

비트마스크 DP를 활용합니다. 특정 집합 $S$가 선택되었을 때, 그 부분집합들의 선택 조건을 만족하는지 확인하며 상태를 전이합니다.

색칠하기 (Coloring)

양 끝의 색상이 같은 겹치지 않는 구간들을 최대한 많이 선택하는 문제입니다. 다음으로 가능한 위치를 빠르게 찾기 위해 희소 테이블(Sparse Table)을 활용한 doubling(Binary Lifting) 기법으로 최적화를 수행합니다.

void preprocess_doubling(int n) {
    for (int j = 1; j < 20; ++j) {
        for (int i = 1; i <= n; ++i) {
            jump[i][j] = jump[jump[i][j - 1]][j - 1];
        }
    }
}

int query_intervals(int L, int R) {
    int count = 0;
    int current = R;
    for (int j = 19; j >= 0; --j) {
        if (jump[current][j] >= L) {
            current = jump[current][j];
            count += (1 << j);
        }
    }
    return count;
}

산요로원 (Mountain Path)

올바른 괄호 문자열 판단 로직을 그래프 확장의 관점으로 재해석합니다. 현재 위치에서 좌우로 확장 가능한 상태를 정의하고 SPFA 또는 다익스트라를 통해 최단 경로를 탐색합니다.

접두사와 접미사

$dp[i]$를 '접두사 $i$까지 고려했을 때 가능한 최대 접미사 길이'로 정의합니다. 각 쿼리마다 제한 조건에 맞는 구간을 이분 탐색으로 탐색하며 탐욕적으로 전이합니다.

커튼 치기 (Draw the Curtains)

버튼 조작과 수동 조작의 비용 관계가 단조성(Monotonicity)을 가짐을 이용합니다. 투 포인터(Two-pointers) 기법과 BIT(Binary Indexed Tree)를 결합하여 각 쿼리에 대한 최적의 분기점을 빠르게 계산합니다.

태그: tree dynamic programming segment tree Dijkstra Tarjan

7월 13일 22:29에 게시됨