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

파이썬으로 이진 트리에서 합이 k인 경로 개수 구하기 (접두사 합 + DFS)


문제 소개

이진 트리(binary tree)와 목표값 k가 주어졌을 때, 어떤 노드에서 시작해 그 아래 자손 노드로 내려가는 경로 중에서 노드 값들의 합이 정확히 k가 되는 고유한 경로의 개수를 세는 프로그램을 작성해 보겠습니다.

예를 들어 다음과 같은 이진 트리가 있다고 가정해 봅시다.

파이썬으로 이진 트리에서 합이 k인 경로 개수 구하기 (접두사 합 + DFS)

이때 k = 5라면 출력 결과는 2가 됩니다. 합이 5가 되는 경로가 [2, 3]과 [1, 4], 총 두 가지이기 때문입니다.

접근 방법: 접두사 합(Prefix Sum) 활용

이 문제는 접두사 합(prefix sum) 기법과 DFS(깊이 우선 탐색)를 조합하면 효율적으로 해결할 수 있습니다. 루트에서 현재 노드까지의 누적 합을 추적하고, 해시 맵에 각 접두사 합이 등장한 횟수를 저장합니다. 현재 접두사 합에서 target을 뺀 값이 맵에 존재한다면, 그 등장 횟수만큼 합이 k가 되는 경로가 존재한다는 의미입니다.

구체적인 알고리즘 단계는 다음과 같습니다.

  • count := 키 0에 값 1을 미리 저장한 맵(Counter)으로 초기화
  • ans := 0, prefix := 0으로 초기화
  • dfs() 함수를 정의합니다. 이 함수는 노드를 인자로 받습니다.
  • 노드가 null이 아니라면:
    • prefix := prefix + 현재 노드의 값
    • ans := ans + count[prefix - target] (키가 없으면 0으로 처리)
    • count[prefix] := count[prefix] + 1
    • dfs(노드의 왼쪽 자식) 호출
    • dfs(노드의 오른쪽 자식) 호출
    • count[prefix] := count[prefix] - 1 (백트래킹으로 상태 복원)
    • prefix := prefix - 현재 노드의 값 (백트래킹으로 상태 복원)
  • 메인 메서드에서는 dfs(root)를 호출한 뒤 ans를 반환합니다.

여기서 백트래킹(backtracking)이 중요한 이유는, 한쪽 서브트리 탐색이 끝난 후 다른 분기를 탐색할 때 이전 경로의 접두사 정보가 남아 있으면 잘못된 경로까지 세어질 수 있기 때문입니다. 재귀 호출이 끝나면 반드시 count와 prefix를 원래 상태로 되돌려 주어야 합니다.

파이썬 구현 예제

from collections import Counter

class TreeNode:
    def __init__(self, data, left=None, right=None):
        self.val = data
        self.left = left
        self.right = right

class Solution:
    def solve(self, root, target):
        count = Counter([0])
        ans = prefix = 0

        def dfs(node):
            nonlocal ans, prefix
            if node:
                prefix += node.val
                ans += count[prefix - target]
                count[prefix] += 1
                dfs(node.left)
                dfs(node.right)
                # 백트래킹: 다른 분기 탐색 시 영향을 주지 않도록 상태 복원
                count[prefix] -= 1
                prefix -= node.val

        dfs(root)
        return ans

ob = Solution()
root = TreeNode(3)
root.left = TreeNode(2)
root.right = TreeNode(4)
root.right.left = TreeNode(1)
root.right.left.right = TreeNode(2)
k = 5
print(ob.solve(root, k))

입력

root = TreeNode(3)
root.left = TreeNode(2)
root.right = TreeNode(4)
root.right.left = TreeNode(1)
root.right.left.right = TreeNode(2)
5

출력

2

복잡도 분석

이 알고리즘은 트리의 모든 노드를 정확히 한 번씩 방문하므로 시간 복잡도는 O(n)입니다(n은 노드의 개수). 또한 해시 맵과 재귀 호출 스택에 공간을 사용하므로 공간 복잡도 역시 최악의 경우 O(n)입니다. 모든 가능한 경로를 일일이 확인하는 브루트포스 방식(O(n²))보다 훨씬 효율적이라는 점이 이 접근법의 가장 큰 장점입니다.