Computer >> 컴퓨터 >  >> 프로그래밍 >> Python

Python으로 동일한 레이블을 가진 하위 트리의 노드 수 찾기

노드가 n개인 루트가 있는 일반 트리가 있다고 가정해 보겠습니다. 노드는 0부터 n-1까지 번호가 매겨져 있으며, 각 노드에는 소문자 영어 알파벳 하나로 이루어진 레이블이 붙어 있습니다. 레이블은 labels 배열로 입력되며, labels[i]는 i번째 노드의 레이블을 담고 있습니다. 트리는 간선 목록으로 표현되는데, 각 간선 e의 [u, v]는 u가 부모이고 v가 자식임을 의미합니다.

우리가 구해야 할 것은 크기가 n인 배열 A입니다. 여기서 A[i]에는 i번째 노드와 같은 레이블을 가진 노드 중에서 i번째 노드의 하위 트리(subtree)에 속한 노드의 개수가 저장됩니다.

예를 들어 입력이 다음과 같다고 해보겠습니다.

Python으로 동일한 레이블을 가진 하위 트리의 노드 수 찾기

이때 n = 5이고 label = "ccaca"입니다.

그러면 출력은 [3, 2, 1, 1, 1]이 됩니다. 루트(0번 노드)는 같은 레이블을 가진 세 개의 자손을 포함하고 있고, 1번 노드는 두 개의 자손을 가지며, 나머지 노드들은 자기 자신만 해당 레이블을 만족하기 때문입니다.

풀이 접근 방법

이 문제는 깊이 우선 탐색(DFS)을 후위 순회(post-order) 방식으로 수행하면 효율적으로 해결할 수 있습니다. 각 노드를 방문할 때 자식 노드들의 레이블 빈도 정보를 먼저 모은 뒤, 자신의 레이블을 추가하고 결과 배열에 기록하는 방식입니다. 구체적인 단계는 다음과 같습니다.

  • E := 주어진 간선 목록으로 그래프(인접 리스트)를 생성합니다.
  • N := 각 노드 번호와 대응하는 레이블을 담은 맵을 만듭니다.
  • R := 크기가 n이고 0으로 초기화된 결과 리스트를 준비합니다.
  • r(ni) 함수를 정의합니다.
  • C := 레이블별 빈도수를 저장할 맵(Counter)을 생성합니다.
  • E[ni]에 있는 각 이웃 e에 대해 다음을 수행합니다.
    • E[e]에서 ni를 제거하여 부모 방향으로의 재방문을 막습니다.
    • 재귀 호출한 r(e)의 결과를 C에 반영합니다.
  • 현재 노드의 레이블 N[ni]를 C에 반영합니다.
  • R[ni] := C[N[ni]]로 현재 노드의 답을 기록합니다.
  • C를 반환합니다.
  • 메인에서 r(0)을 호출하여 루트부터 탐색을 시작합니다.
  • 최종적으로 R을 반환합니다.

각 노드마다 최대 26개의 알파벳 빈도만 관리하면 되므로 전체 시간 복잡도는 O(n × 26)으로 매우 효율적입니다.

구현 예제

아래 Python 구현을 통해 더 자세히 이해해 보겠습니다.

from collections import defaultdict, Counter

def solve(n, edges, labels):
    E = defaultdict(set)
    for f, t in edges:
        E[f].add(t)
        E[t].add(f)
    N = {i: e for i, e in enumerate(labels)}
    R = [0] * n

    def r(ni):
        C = Counter()
        for e in E[ni]:
            E[e].remove(ni)
            C.update(r(e))
        C.update((N[ni]))
        R[ni] = C[N[ni]]
        return C

    r(0)
    return R

n = 5
edges = [[0,1],[0,2],[1,3],[0,4]]
labels = "ccaca"
print(solve(n, edges, labels))

입력

5, [[0,1],[0,2],[1,3],[0,4]], "ccaca"

출력

[3, 2, 1, 1, 1]