LeetCode/주제별 보충

Tree: 124. Binary Tree Maximum Path Sum

hyunkookim 2024. 12. 14. 17:40

124. Binary Tree Maximum Path Sum

 

문제가 요구하는 것

  • Binary Tree Maximum Path Sum:
    • 트리에서 경로의 최대 합을 구하는 문제입니다.
    • "경로"는 트리의 어떤 노드에서 시작하여 하위 노드를 따라가거나, 중간에서 멈출 수 있으며, 반드시 루트를 거칠 필요는 없습니다.
    • 경로의 합은 경로에 있는 노드 값들의 합으로 계산됩니다.
    • "최대 합"은 가능한 경로들 중 가장 큰 합입니다.

Mutable(변경 가능) 객체 와 Immutable(변경 불가능) 객체

파이썬에서 mutable(변경 가능) 객체와 immutable(변경 불가능) 객체는 객체의 성질에 따라 구분됩니다. 아래는 각각의 주요 예시를 정리한 표입니다.


1. Mutable(변경 가능) 객체

  • 특징:
    • 객체를 생성한 후 내부 데이터를 변경할 수 있음.
    • 새로운 객체를 생성하지 않고도 값을 수정 가능.
  • 예시:

Mutable 객체설명

list 리스트: 값을 추가/삭제/변경 가능. my_list[0] = 42
dict 딕셔너리: 키-값 쌍을 추가/삭제/변경 가능. my_dict['key'] = 'new_value'
set 집합: 요소를 추가/삭제 가능. my_set.add(42)
bytearray 바이트 배열: 데이터를 바이트 단위로 변경 가능. my_bytearray[0] = 65
user-defined classes 사용자가 정의한 클래스의 객체에서 속성 값을 변경 가능 (속성을 mutable로 선언한 경우).

Mutable 객체 예시

# list
my_list = [1, 2, 3]
my_list[0] = 42  # 변경 가능
print(my_list)  # 출력: [42, 2, 3]

# dict
my_dict = {'a': 1, 'b': 2}
my_dict['a'] = 42  # 변경 가능
print(my_dict)  # 출력: {'a': 42, 'b': 2}

# set
my_set = {1, 2, 3}
my_set.add(4)  # 변경 가능
print(my_set)  # 출력: {1, 2, 3, 4}

2. Immutable(변경 불가능) 객체

  • 특징:
    • 객체를 생성한 후 내부 데이터를 변경할 수 없음.
    • 데이터를 수정하려고 하면, 새로운 객체가 생성됨.
  • 예시:

Immutable 객체설명

int 정수형: 값을 변경하려면 새로운 객체가 생성됨. a = 1; a = 2
float 부동소수점: 값을 변경하려면 새로운 객체가 생성됨. b = 1.1; b = 2.2
str 문자열: 값을 변경하려면 새로운 객체가 생성됨. s = 'hello'; s = 'world'
tuple 튜플: 값을 변경할 수 없음.
frozenset 고정된 집합: 요소를 추가/삭제할 수 없음.
bytes 바이트 시퀀스: 불변형 데이터.

Immutable 객체 예시

# int
a = 1
print(id(a))  # 객체 ID 출력
a += 1  # 새로운 객체 생성
print(id(a))  # 다른 객체 ID

# str
s = "hello"
print(id(s))  # 객체 ID 출력
s += " world"  # 새로운 객체 생성
print(id(s))  # 다른 객체 ID

# tuple
my_tuple = (1, 2, 3)
# my_tuple[0] = 42  # 오류 발생: TypeError: 'tuple' object does not support item assignment

Mutable vs Immutable의 차이

특성 Mutable 객체 Immutable 객체
데이터 수정 가능 여부 가능 불가능
새 객체 생성 여부 수정 시 새 객체 생성 없음 수정 시 새로운 객체 생성
예제 list, dict, set int, float, str, tuple, frozenset

주의할 점

  • Mutable 객체를 함수에 전달하면 함수 내부에서 수정한 값이 원본에 영향을 미칩니다.
  • Immutable 객체를 함수에 전달하면 함수 내부에서 수정된 값은 원본에 영향을 주지 않습니다.

Mutable 예제

def modify_list(lst):
    lst.append(4)  # 원본 리스트가 변경됨

my_list = [1, 2, 3]
modify_list(my_list)
print(my_list)  # 출력: [1, 2, 3, 4]

Immutable 예제

def modify_int(x):
    x += 1  # 새로운 객체가 생성되며, 원본에 영향 없음

a = 1
modify_int(a)
print(a)  # 출력: 1

 

Immutable 객체 + nonlocal 사용 예제: 

- 'nonlocal' 사용해서 전역 처럼 사용 가능

def counter():
    count = 0  # Immutable 객체 (int)

    def increment():
        nonlocal count  # 외부 함수의 count 변수를 수정 가능하도록 선언
        count += 1  # count 값을 수정
        return count

    return increment

# counter 함수를 호출해 increment 함수 반환
increment_func = counter()

# increment 함수 호출로 count 값을 증가
print(increment_func())  # 출력: 1
print(increment_func())  # 출력: 2
print(increment_func())  # 출력: 3

 

중요 포인트

  1. 왜 nonlocal이 필요한가?
    • 기본적으로 내부 함수는 외부 함수의 변수를 수정할 수 없습니다.
    • nonlocal을 사용하면 내부 함수가 외부 함수의 변수를 수정 가능하게 만듭니다.
  2. nonlocal vs global
    • nonlocal: 가장 가까운 외부 함수의 변수에 접근.
    • global: 전역 변수에 접근.

 

global과의 차이

count = 0  # 전역 변수

def increment():
    global count  # 전역 변수 count를 사용
    count += 1
    return count

print(increment())  # 출력: 1
print(increment())  # 출력: 2

 

nonlocal은 지역 스코프에서 변수를 관리할 때, global은 전역 스코프에서 변수를 관리할 때 사용됩니다.


결론

  • Mutable 객체는 데이터 수정이 필요한 경우 유용하지만, 주의하지 않으면 원본 데이터가 의도치 않게 변경될 수 있습니다.
  • Immutable 객체는 안전하게 데이터를 유지할 수 있지만, 데이터를 수정하려면 새 객체를 생성해야 합니다.

필요에 따라 mutable과 immutable 객체를 적절히 활용하면 됩니다! 😊

 

최대 경로 합을 구하는 문제의 본질

  1. 경로 정의:
    • 경로는 트리의 어느 노드에서 시작해서 하위 노드를 따라가며, 어느 노드에서든 끝날 수 있습니다.
    • 루트를 반드시 포함할 필요는 없습니다.
    • 경로는 **"연결된 노드의 집합"**을 의미하며, "분할"되는 경로도 고려해야 합니다.
  2. 경로 분할:
    • "현재 노드에서의 경로 합"은 왼쪽 서브트리의 최대 경로 합 + 오른쪽 서브트리의 최대 경로 합 + 현재 노드 값입니다.
    • 이 값은 "최대 경로 합"을 갱신할 때 사용됩니다.
    • 하지만 상위 노드로 반환하는 값한쪽 경로만 포함해야 합니다.

왜 한쪽 경로만 반환하는가?

1. 트리 구조의 특성

  • 트리의 부모-자식 관계에서는 "부모 노드 → 자식 노드"로만 이동합니다.
  • 상위 노드로 값을 반환할 때는 **"연결된 경로"**를 유지해야 하므로, 현재 노드에서 왼쪽 또는 오른쪽 하위 경로 중 하나만 선택해야 합니다.
  • 두 개의 하위 경로를 모두 포함하면 상위 노드에서 트리가 분리된 경로처럼 보이게 됩니다.

2. 분할 경로 vs. 연결 경로

  • 분할 경로 (현재 노드 + 왼쪽 최대 경로 + 오른쪽 최대 경로):
    • 이는 **"완전한 경로"**로서, 상위 노드와는 연결되지 않은 독립적인 경로를 의미합니다.
    • 이 값을 사용해 res[0]을 갱신하지만, 상위 노드로 반환할 필요는 없습니다.
  • 연결 경로 (현재 노드 + 한쪽 하위 경로):
    • 상위 노드와 연결된 경로만 상위로 반환합니다.
    • 따라서 root.val + max(leftMax, rightMax)를 반환합니다.

 

https://youtu.be/Hr5cWUld4vU?si=4Db1NiEYKr0Fygx2

 

 

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right

class Solution:
    def maxPathSum(self, root: Optional[TreeNode]) -> int:
        # 전역 변수 res를 리스트로 선언하여 최대 경로 합을 저장
        res = [root.val]

        # DFS를 사용하여 트리의 최대 경로 합을 계산하는 헬퍼 함수
        def dfs(root):
            if not root:  # 현재 노드가 None이면 0을 반환
                return 0

            # 왼쪽 하위 트리의 최대 경로 합 계산
            leftMax = dfs(root.left)
            leftMax = max(0, leftMax)  # 음수 경로는 제외하고 0 또는 최대값 선택

            # 오른쪽 하위 트리의 최대 경로 합 계산
            rightMax = dfs(root.right)
            rightMax = max(0, rightMax)  # 음수 경로는 제외하고 0 또는 최대값 선택

            # 현재 노드에서의 최대 경로 합 계산 (분할 경로 포함)
            # 왼쪽 경로 + 현재 노드 값 + 오른쪽 경로의 합을 계산
            res[0] = max(res[0], root.val + leftMax + rightMax)

            # 상위 노드로 전달할 값 반환 (현재 노드 값 + 왼쪽/오른쪽 중 최대값)
            # 상위 노드는 하나의 경로만 연결할 수 있으므로 한쪽만 선택
            return root.val + max(leftMax, rightMax)

        # DFS 시작
        dfs(root)

        # 전역 변수 res에 저장된 최대 경로 합 반환
        return res[0]

        # 입력 트리 예시:
        #        1
        #       / \
        #      2   3
        #     / \
        #    4   5
        #
        # 동작 과정:
        # 1. 각 노드에서 왼쪽, 오른쪽 하위 트리의 최대 경로 합 계산
        # 2. 분할 경로의 최대 합을 계산하여 res[0]을 갱신
        # 3. 상위 노드로 전달할 경로 합은 한쪽만 선택 (left 또는 right)
        #
        # 최종 결과: 최대 경로 합은 4 -> 2 -> 1 -> 3 으로 11

 

nonlocal 사용해서..

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def maxPathSum(self, root: Optional[TreeNode]) -> int:
        # res = [root.val]
        res = root.val  # 초기 최대값을 root의 값으로 설정


        # return max path sum without split
        def dfs(root):  
            nonlocal res # res 를 전역 변수로..

            if not root:
                return 0

            leftMax = dfs(root.left)
            leftMax = max(0, leftMax)

            rightMax = dfs(root.right)
            rightMax = max(0, rightMax)
            
            # compute max path sum with split
            #res[0] = max(res[0], root.val+leftMax+rightMax)
            res = max(res, root.val+leftMax+rightMax)
        
            return root.val + max(leftMax, rightMax)
            
        dfs(root)
        #return res[0]
        return res

        # def dfs(node, currSum):
        #     if not node:
        #         return 0
            
        #     currSum += node.val

        #     return max(dfs(node.left, currSum), dfs(node.right, currSum), dfs(node.left, currSum)+dfs(node.right, currSum))

        # return dfs(root, 0)

대안 1: 전역 변수 없이 DFS로 구현

전역 변수를 사용하지 않고 반환값으로 최대 경로 합을 추적하는 방법입니다. 이 방법에서는 추가 변수를 사용하지 않고 함수 반환값을 통해 결과를 전달합니다.

 

 

class Solution:
    def maxPathSum(self, root: Optional[TreeNode]) -> int:
        def dfs(root):
            if not root:
                return (float('-inf'), 0)  # (최대 경로 합, 상위로 전달할 최대값)

            # 왼쪽과 오른쪽 하위 트리에서 최대값 계산
            leftMaxPath, leftSinglePath = dfs(root.left)
            rightMaxPath, rightSinglePath = dfs(root.right)

            # 현재 노드에서 가능한 최대 경로 합
            maxAtRoot = root.val + max(0, leftSinglePath) + max(0, rightSinglePath)

            # 현재까지의 최대 경로 합 계산
            maxPath = max(leftMaxPath, rightMaxPath, maxAtRoot)

            # 상위 노드로 전달할 값 (한쪽 경로만 선택)
            singlePath = root.val + max(0, leftSinglePath, rightSinglePath)

            return (maxPath, singlePath)

        # dfs 호출로 최대 경로 합 계산
        maxPath, _ = dfs(root)
        return maxPath

 

 

대안 2: BFS(너비 우선 탐색)로 트리의 경로 탐색

DFS가 아닌 BFS를 사용해 각 노드에서 최대 경로 합을 계산하는 방법입니다. 이 방식은 스택 대신 큐를 사용하며, 트리의 각 레벨을 순차적으로 탐색합니다.

 

class Solution:
    def maxPathSum(self, root: Optional[TreeNode]) -> int:
        def dfs(root):
            if not root:
                return (float('-inf'), 0)  # (최대 경로 합, 상위로 전달할 최대값)

            # 왼쪽과 오른쪽 하위 트리에서 최대값 계산
            leftMaxPath, leftSinglePath = dfs(root.left)
            rightMaxPath, rightSinglePath = dfs(root.right)

            # 현재 노드에서 가능한 최대 경로 합
            maxAtRoot = root.val + max(0, leftSinglePath) + max(0, rightSinglePath)

            # 현재까지의 최대 경로 합 계산
            maxPath = max(leftMaxPath, rightMaxPath, maxAtRoot)

            # 상위 노드로 전달할 값 (한쪽 경로만 선택)
            singlePath = root.val + max(0, leftSinglePath, rightSinglePath)

            return (maxPath, singlePath)

        # dfs 호출로 최대 경로 합 계산
        maxPath, _ = dfs(root)
        return maxPath

 

 

결론

  • 전역 변수 없는 DFS: 함수 반환값을 통해 최대 경로 합을 추적할 수 있어 일반적으로 더 깔끔한 구현입니다.
  • BFS: 트리의 레벨별 탐색을 원하거나 재귀를 피하고 싶을 때 적합합니다.
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:    
    def maxPathSum(self, root: Optional[TreeNode]) -> int:
        # 루트 마다 -> 왼쪽합 최대값, 오른쪽합 최대값, 나포함 최대값 -> 최대값 갱신
        # 깊이 우선 탐색: dfs

        """
        maxSum = root.val 으로 선언해서 사용하고 싶으나, 일반 변수면.
        dfs() 함수 내에서 사용하려면, nonlocal 등의 파라미터를 사용해야되서
        다른 키워드 처리없이, 전역 변수로 사용하기위해, 리스트 변수로 만들어서 사용함
        """

        maxSum = [root.val] 

        def dfs(node):
            if not node:
                return 0

            leftMax = max(0, dfs(node.left))
            rightMax = max(0, dfs(node.right))

            maxSum[0] = max(maxSum[0], node.val + leftMax + rightMax)

            return node.val + max(leftMax, rightMax)

        dfs(root)
        return maxSum[0]

 

최종

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def maxPathSum(self, root: Optional[TreeNode]) -> int:        
        self.max_sum = -float("inf")
        
        def dfs_sum(node):
            if not node:
                return 0

            # 음수 노드로 인해 최대값이 왜곡되지 않고, 최대 경로 합이 정확히 계산하기 위해, 
            # max(0, ) 취함
            left_max = max(dfs_sum(node.left), 0) 
            right_max = max(dfs_sum(node.right), 0)
            self.max_sum = max(self.max_sum, 
                                left_max + right_max+ node.val)
            return max(left_max, right_max) + node.val
        
        dfs_sum(root)
        return self.max_sum