알고리즘

백준 25402 트리와 쿼리 파이썬 풀이

gubshig 2023. 2. 17. 23:35

이 문제는 서브테스크를 긁고 최적화를 할 방법을 모르겠어서 풀이를 봤다. 풀이가 신기하고 좋은 문제라고 생각해서 풀이를 올려본다.

 

https://www.acmicpc.net/problem/25402

 

25402번: 트리와 쿼리

첫 번째 줄부터 $Q$개의 줄에 걸쳐, 각 질의에 대한 답을 출력한다. 이 중 $i$ ($1 ≤ i ≤ Q$)번째 줄에는 $i$번째 질의에서 주어진 $S$에 대하여, $S$의 연결 강도를 출력한다.

www.acmicpc.net

이 문제를 요악하자면 트리를 S라는 집합의 정점들로만 한정지었을때, 생기는 각 컴포넌트들의 연결 되어있는 노드의 개수를 구하면 된다. 연결 개수는 컴포넌트의 노드 개수를 n이라 했을때, nC2가 된다. 

나이브하게 매 쿼리마다 dfs를 돌리며 컴포넌트의 개수를 세어주면, $O(NQ)$가 된다. 이로 서브테스크 3까지 긁을 수 있다 (24점 풀이).

import sys
input = sys.stdin.readline

def find(x):
    if x == p[x]: return x
    p[x] = find(p[x])
    return p[x]

def union(x, y):
    x, y = find(x), find(y)
    if x == y: return
    if r[x] < r[y]: x, y = y, x
    p[y] = x
    r[x] += r[y]

def dfs(n):
    if visited[n]: return 0
    visited[n] = True
    for c in g[n]:
        if not visited[c] and c in s:
            union(n, c)
            dfs(c)
    return r[n]

n = int(input())
g = [[] for _ in range(n + 1)]
for _ in range(n-1):
    u, v = map(int, input().split())
    g[u].append(v)
    g[v].append(u)

Q = int(input())
for _ in range(Q):
    p = [i for i in range(n + 1)]
    r = [1 for i in range(n + 1)]
    k, *s = map(int, input().split())
    visited = [0] * (n + 1)
    s = set(s)
    ans = 0
    for node in s:
        tmp = dfs(node)
        ans += (tmp * (tmp - 1)) // 2
    print(ans)

여기서 최적화를 해주면 만점 풀이를 얻을 수 있다. 핵심적인 아이디어는 트리의 성질이다. 임이의 정점을 루트로 잡았을때, 각 노드의 부모는 유일하게 존재한다. 나이브하게 dfs를 돌리면 S의 각 정점마다 그 정점의 차수만큼 탐색을 해주어야하는데, 각 노드들의 부모를 저장해주면, 노드의 부모가 S에 있는지만 확인해주면 된다. 유니온파인드를 이용해서 컴포넌트의 개수를 세는것은 쉽다. 즉, 각 쿼리를 $O(K * \alpha(K))$에 해결해줄 수 있고, K의 합은 1e6 이하이기에 이는 충분한 시간복잡도이다.

import sys
input = sys.stdin.readline
print = sys.stdout.write

def find(x):
    while x != p[x]:
        p[x] = p[p[x]]
        x = p[x]
    return p[x]

def union(x, y):
    x, y = find(x), find(y)
    if x == y: return
    if r[x] < r[y]: x, y = y, x
    p[y] = x
    r[x] += r[y]

def dfs(n):
    visited[n] = 1
    s = [n]
    while s:
        v = s.pop()
        for w in g[v]:
            if not visited[w]:
                an[w] = v
                s.append(w)
                visited[w] = 1

n = int(input())
n += 1
g = [[] for _ in range(n)]
for _ in range(n-2):
    u, v = map(int, input().split())
    g[u].append(v)
    g[v].append(u)

an = [0] * n
visited = [0] * n
dfs(1)


p = [i for i in range(n)]
r = [1 for i in range(n)]
isin = [0] * n
Q = int(input())
for _ in range(Q):
    ans = 0
    k, *s = map(int, input().split())
    for i in range(k): isin[s[i]] = 1
    for node in s:
        if isin[an[node]]: union(node, an[node])
    for node in s:
        if p[node] == node: ans += (r[node] * (r[node] - 1)) // 2
        p[node] = node
        r[node] = 1
    for i in range(k): isin[s[i]] = 0
    print(str(ans) + '\n')

dfs로 각 노드의 부모를 전처리해주고, 유니온파인드를 통해 컴포넌트에 속한 노드의 개수를 세주면 된다. 파이썬 사용자는 주의해야할 점이 있는데, dfs와 유니온파인드를 재귀 구현하면 시간초과가 난다. 전부 비재귀로 바꾸고 나서야 결국 AC를 받을 수 있었다.

'알고리즘' 카테고리의 다른 글

2023 02 21 problem solving  (0) 2023.02.21
백준 1750 서로소의 개수 파이썬  (0) 2023.02.19
2023 02 17 problem solving  (0) 2023.02.17
정올 1차 1, 2번 문제 풀이  (0) 2023.02.16
FFT (Fast Fourier Transform)  (1) 2022.12.01