문제
- 문제 링크
BOJ 25973 - 어지러운 트리
$N$개의 정점으로 이루어진 트리와 $Q$개의 쿼리가 주어진다.
각 쿼리마다 그에 맞는 처리를 진행하자.
TL : $1$ sec, ML : $1024$ MB
$1 ≤ N, Q ≤ 200,000$ $2$번 쿼리는 적어도 하나 이상 주어진다.
알고리즘 분류
- 트리(trees)
- 최소 공통 조상(lowest common ancestor)
- 다이나믹 프로그래밍(dp)
- 트리에서의 다이나믹 프로그래밍(dp_tree)
풀이
매 쿼리마다 $dfs$ 돌려볼 순 없는 노릇이다.
루트가 $1$인 최초 시점을 기준으로 모든 경우의 수를 전처리 후,
$x$와 현재 루트 정점을 가지고 뭐 어떻게 잘 요리해서 쿼리당 $O(logN)$ 으로 응답할 것을 생각하자.
경우의 수는 서브 트리의 사이즈 $(S[])$ 로써 계산할 수 있으니 $LCA$를 위한 전처리를 하며 임의의 정점에 대한 경우의 수 전처리도 동시에 진행할 수 있다.
즉, 다음의 추가적인 정보를 전처리 하자는 것이다.
$Sdp[i] :$ 루트가 $1$인 시점을 기준으로 $x == i$ 일 때 $(a, b)$의 쌍으로 가능한 경우의 수.
$1$번 쿼리는 간단하다. 말그대로 현재 루트를 $x$로 변경해 준다.
$2$번 쿼리는 $3$가지로 분류하여 생각해보자.
- $root == x$ 일 때.
- $LCA(root, x) == x$ 일 때.
- $root$와 $x$가 아무런 연고가 없을 때.
$1$의 경우.
아래 내용에서의 $S[], Sdp[]$는 루트가 $1$일 때를 기준으로 전처리 한 것이다. 이를 항상 명심하여 식을 세워보자.
기본적으로 포함 되는 값은, $Sdp[x]$와 $S[x]$ $-$ $1$ 이다. $($후자는 $x$와 그 서브 트리 내 노드를 고르는 경우의 수$)$
그리고 $x$가 루트가 되면서 반전 된 경우의 수를 더해줘야 한다.
이는 위에서의 $S[x]$ $-$ $1$ 과 나머지 노드들인 $N$ $-$ $S[x]$ 를 곱한 값, $N$ $-$ $S[x]$ 를 더해주면 된다.
소거할 항을 소거하고 식을 정리해 보면
$r = Sdp[x] + S[x] - 1 + (S[x] - 1) * (N - S[x]) + N - S[x]$
$r = Sdp[x] + S[x] - 1 + NS[x] - S[x]^2 - N + S[x] + N - S[x]$
$r = Sdp[x] - 1 - S[x] * (S[x] - N - 1)$
$2$의 경우.
가장 까다로운 경우이다. 포함 및 배제할 값들을 정확히 추리기 위해 $o$를 $x$ 바로 밑까지 끌어올려 주고, 이 노드를 $y$라 하자.
기본적으로 포함 되는 값은, $Sdp[x]$와 $N - S[y] - 1$ 가 된다. $($후자는 $x$와 바뀐 서브 트리 내 노드를 고르는 경우의 수$)$
그리고 $x$의 $S[y]$를 제외한 기존 하위 노드들과 새로이 $x$의 하위 노드가 된 노드들간의 경우의 수를 더해줘야 한다.
$(S[x] - S[y] - 1) * (N - S[x])$
처음 포함한 $Sdp[x]$는 $S[y]$의 노드들이 고려된 값이다.
관계가 뒤집힘에 따라 발생하는 차이만큼 빼주어야 하므로 $S[y] * (S[x] - S[y] - 1)$ 를 빼주면 된다.
소거할 항을 소거하고 식을 정리해 보면
$r = Sdp[x] + N - S[y] - 1 + (S[x] - S[y] - 1) * (N - S[x]) - S[y] * (S[x] - S[y] - 1)$
$r = Sdp[x] + N - S[y] - 1 + NS[x] - S[x]^2 - NS[y] + S[y]S[x] - N + S[x] - S[y]S[x] + S[y]^2 + S[y]$
$r = Sdp[x] - 1 + S[y] * (S[y] - N) - S[x] * (S[x] - N - 1)$
$3$의 경우.
간단하다.
루트가 어떤 노드이건 상관 없이 $x$와 $x$의 서브 트리로만 구성된다.
$r = Sdp[x] + S[x] - 1$
좀 주저리주저리 한 것 같은데.. 적당한 사이즈의 트리를 만들어 놓고,
이런저런 예제를 만들어 보며 직접 식을 유도해 보는 것을 추천한다.
그 과정에서 진정 문제를 제대로 바라보게 될 테니 말이다.
기본적으로 포함 되는 값은, $Sdp[x]$와 $S[x]$ $-$ $1$ 이다. $($후자는 $x$와 그 서브 트리 내 노드를 고르는 경우의 수$)$
그리고 $x$가 루트가 되면서 반전 된 경우의 수를 더해줘야 한다.
이는 위에서의 $S[x]$ $-$ $1$ 과 나머지 노드들인 $N$ $-$ $S[x]$ 를 곱한 값, $N$ $-$ $S[x]$ 를 더해주면 된다.
소거할 항을 소거하고 식을 정리해 보면
$r = Sdp[x] + S[x] - 1 + (S[x] - 1) * (N - S[x]) + N - S[x]$
$r = Sdp[x] + S[x] - 1 + NS[x] - S[x]^2 - N + S[x] + N - S[x]$
$r = Sdp[x] - 1 - S[x] * (S[x] - N - 1)$
기본적으로 포함 되는 값은, $Sdp[x]$와 $N - S[y] - 1$ 가 된다. $($후자는 $x$와 바뀐 서브 트리 내 노드를 고르는 경우의 수$)$
그리고 $x$의 $S[y]$를 제외한 기존 하위 노드들과 새로이 $x$의 하위 노드가 된 노드들간의 경우의 수를 더해줘야 한다.
$(S[x] - S[y] - 1) * (N - S[x])$
처음 포함한 $Sdp[x]$는 $S[y]$의 노드들이 고려된 값이다.
관계가 뒤집힘에 따라 발생하는 차이만큼 빼주어야 하므로 $S[y] * (S[x] - S[y] - 1)$ 를 빼주면 된다.
소거할 항을 소거하고 식을 정리해 보면
$r = Sdp[x] + N - S[y] - 1 + (S[x] - S[y] - 1) * (N - S[x]) - S[y] * (S[x] - S[y] - 1)$
$r = Sdp[x] + N - S[y] - 1 + NS[x] - S[x]^2 - NS[y] + S[y]S[x] - N + S[x] - S[y]S[x] + S[y]^2 + S[y]$
$r = Sdp[x] - 1 + S[y] * (S[y] - N) - S[x] * (S[x] - N - 1)$
루트가 어떤 노드이건 상관 없이 $x$와 $x$의 서브 트리로만 구성된다.
$r = Sdp[x] + S[x] - 1$
전체 코드
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | #include<bits/stdc++.h> #define N 200001 using namespace std; vector <int> Gr[N]; int n, q, D[N], dp[N][19]; long long r, S[N], Sdp[N]; void Init() { ios_base::sync_with_stdio(0); cin.tie(0); cin >> n >> q; for (int i, j, o{}; ++o < n;) cin >> i >> j, Gr[i].push_back(j), Gr[j].push_back(i); } void LCAInit(int p, int q) { D[p] = q; S[p] = 1; for (int i : Gr[p]) if (!D[i]) { LCAInit(i, q + 1); Sdp[p] += S[i] * (S[p] - 1); dp[i][0] = p, S[p] += S[i]; } } void SparseTableDP() { for (int j(1); j < 19; j++) for (int i(1); i <= n; i++) dp[i][j] = dp[dp[i][j - 1]][j - 1]; } int GetLCA(int p, int q, int k) { if (D[p] < D[q]) swap(p, q); int d(D[p] - D[q] - k); for (int i{}; d; d >>= 1, i++) if (d & 1) p = dp[p][i]; if (!k && p ^ q) { for (int i(18); i >= 0; i--) if (dp[p][i] ^ dp[q][i]) p = dp[p][i], q = dp[q][i]; p = dp[p][0]; } return p; } void Solve(int rt = 1) { for (int i, x, y; q--;) if (cin >> i >> x, i & 1) rt = x; else { if (rt == x) r = -S[x] * (S[x] - n - 1); else if (x == GetLCA(x, rt, 0)) { y = GetLCA(x, rt, 1); r = S[y] * (S[y] - n) - S[x] * (S[x] - n - 1); } else r = S[x]; cout << r + Sdp[x] - 1 << '\n'; } } int main() { Init(); LCAInit(1, 1); SparseTableDP(); Solve(); } | cs |
comment
$LCA$와 좀 더 친해질 수 있는 좋은 문제라고 생각한다.
'Problem Solving > Baekjoon Online Judge (Platinum)' 카테고리의 다른 글
백준 25430 - 다이제스타 (C++) (0) | 2023.04.26 |
---|---|
백준 5467 - Type Printer (C++) (0) | 2023.04.23 |
백준 25478 - Marinada (C++) (0) | 2023.04.23 |
백준 19585 - 전설 (C++) (0) | 2023.04.23 |
백준 8571 - Apteka (C++) (0) | 2023.04.23 |