본문 바로가기

Algorithms and Languages/파이썬 알고리즘 문제풀이

[Python/백준] 1068. 트리

문제 접근 및 공부 내용, 풀이는 모두 하단 코드에 "주석"으로 포함되어 있으니 참고해주세요.

문제 유형 보기

더보기
더보기
트리 + dfs or 트리 + 순회

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


풀이

1차 시도) 실패

테케는 다 맞았는데… 더 고민해보자.

import sys
input = sys.stdin.readline

# 노드 하나 지운 이후의 트리의 리프 노드 개수를 구하라.
# 노드를 지우면 그 노드에 딸린 서브트리가 통째로 지워짐.
# 입력 : 노드 개수, 0~n-1번 노드까지의 각 노드의 부모, 지울 노드의 번호
# 출력 : 처리 이후 리프 노드의 개수

# 흠... 이거 오름차순으로 주나?
# 일단, 결국 그 노드를 기준으로 후위 순회 돌리면 되겠는데?
# 후위 순회의 action은, 자신의 자식 노드를 전부 -1로 하는 것.
# 그리고 마지막에 루트 노드를 -1로.

n = int(input())

tree = [[-1, -1] for _ in range(n)]

parents = list(map(int, input().split()))
for i in range(1, n):
    if tree[parents[i]][0] == -1:
        tree[parents[i]][0] = i
        continue
    tree[parents[i]][1] = i

# print(tree)
# [[1, 2], [3, 4], [-1, -1], [-1, -1], [-1, -1]]

del_node = int(input())

def postorder(node):
    if node == -1:
        return
    postorder(tree[node][0])
    postorder(tree[node][1])
    tree[node] = [-2, -2]


postorder(del_node)
counts = 0
for children in tree:
    for i in range(2):
        if children[i] == del_node:
            children[i] = -1
    if children.count(-2) != 2:
        if children.count(-1) + children.count(-2) == 2:
            counts += 1

# del_node는 리프 노드로 간주되어 세어졌을 것이니
print(counts)

2차 시도) 실패

이거… 트리 정렬해버리면 index를 잃어버림. 어카지…

게다가, 이진 트리가 아닐 수도 있다!

import sys
input = sys.stdin.readline

# 노드 하나 지운 이후의 트리의 리프 노드 개수를 구하라.
# 노드를 지우면 그 노드에 딸린 서브트리가 통째로 지워짐.
# 입력 : 노드 개수, 0~n-1번 노드까지의 각 노드의 부모, 지울 노드의 번호
# 출력 : 처리 이후 리프 노드의 개수

# 흠... 이거 오름차순으로 주나?
# 일단, 결국 그 노드를 기준으로 후위 순회 돌리면 되겠는데?
# 후위 순회의 action은, 자신의 자식 노드를 전부 -1로 하는 것.
# 그리고 마지막에 루트 노드를 -1로.

n = int(input())

tree = [[-1, -1] for _ in range(n)]

parents = list(map(int, input().split()))



# print(tree)
# [[1, 2], [3, 4], [-1, -1], [-1, -1], [-1, -1]]

del_node = int(input())

for i in range(1, n):
    if tree[parents[i]][0] == -1:
        tree[parents[i]][0] = i
        continue
    tree[parents[i]][1] = i

is_del_node = [[0] for _ in range(n)]
is_del_node[del_node] = 1
parents = list(zip(parents, is_del_node))
parents.sort(key = lambda x: x[0])





def postorder(node):
    if node == -1:
        return
    postorder(tree[node][0])
    postorder(tree[node][1])
    tree[node] = [-1, -1]
    if tree[parents[node]][0] == node:
        tree[parents[node]][0] = -1
    else:
        tree[parents[node]][1] = -1
    parents[node] = -2


postorder(del_node)
counts = 0
for index in range(n):
    if parents[index] != -2:
        if tree[index].count(-1) == 2:
            counts += 1

# del_node는 리프 노드로 간주되어 세어졌을 것이니
print(counts)

250225 재시도

3차 시도, 시도 중 모르겠어서 질문게시판 보고 수정해 성공

질문게시판에서 2가지 유형의 엣지 케이스 확인

https://www.acmicpc.net/board/view/132447

 

내 풀이는, 지워야 할 노드를 루트 노드로 같는 서브 트리를 dfs(와 유사한 방식으)로 탐색하며, 지워가고 있음.

여기서 dfs는 그냥 ‘그 서브트리를 통째로 지워버리는’ 역할만 함.

여기서 지움당하는 대상은 parents로 구현된 트리 그 자체. parents를 바꿔나가는 거임.

 

잊지말자!! 트리의 구현 방식은 여러가지가 있다구!!

‘parents[자식트리번호] = 부모트리번호’로 트리를 표현하는 테크닉!! 기억하자.

 

이거 근데 dfs라고 쓰긴 썼는데, 사실 트리 (후위) 순회에 더 가깝긴 한듯.ㅋㅋ

그리고 전위를 하냐 후위를 하냐 중위를 하냐는 여기선 아무 상관이 x

import sys
input = sys.stdin.readline

# 특정 노드를 제거한 후의 리프 노드의 개수를 세자
# 입력 : 트리의 노드 개수 n, 둘째 줄부턴 0 - n-1 번 노드까지 각 노드의 부모 주어짐
# 셋째 줄에는 지울 노드 번호

n = int(input())
parent = list(map(int, input().split()))
t = int(input())
root = parent.index(-1)

# 지우는 건 둘째치고, 리프 노드 개수를 어떻게 셀 수 있지?
# 지우는 거는 뭐 그 노드를 부모로 가지는 노드들을 -1로 값 바꿔주면 되지 않을까?
# 탐색하면서, 그 노드의 값이 parent에 없으면 리프 노드다? 이거 괜찮아보이는데?

# 일단 n이 50 이하인데, 좀 비효율적이어도 될 듯함.
# while 돌면서 flag 설정해두고, 순차적으로 다 지우면 될 듯한데?
# 어? dfs? bfs?

def dfs(node):
    for i in range(n):
        if parent[i] == node: # 만약 i가 node를 부모로 가지면
            dfs(i) # i 기준으로 다시 dfs 돌림
    parent[node] = -1

dfs(t)

counts = 0

# 루트 노드 혼자 남았거나, 아님 루트 노드를 지워버렸을 경우
if parent.count(-1) == n:
    if t != root: # 루트 노드를 지웠을 경우
        counts = 1
    n = 0 # 뒤에 오는 반복문 실행 안되게 처리
    
for i in range(n):
    if parent[i] != -1 and i not in parent:
        counts += 1
print(counts)

# (모르겠음 -> 질문 게시판 확인)
# 루트 노드 하나만 남아도 리프 노드가 됨을 간과.
'''
2
-1 0
1
output: 0
answer: 1
'''
# 루트 노드 하나만 남았다는건, 모든 노드가 다 -1이라는 거임.

# 위 문제 해결해도 99%에서 오류 나옴...
# 루트 노드를 삭제하면 0이 나와야 함을 간과.
'''
2
1 -1
1
output: 1
answer: 0
'''

강사님 정답 코드 확인

일단 우리가 알던 대로 트리를 만드는데, 거기에 ‘지울 노드’는 존재하지 않는, 즉 끊겨있는 트리를 넣음!!

정확히는, 그 지울 노드의 부모 노드에다가 그 지울 노드를 넣는 행위를 생략해버린 것!!

그렇게 한 후에 루트 노드에서 dfs 돌리면, 끊긴 노드는 제외하고 탐색하니까, 노드를 버린 것과 동일한 효과!!

 

와… 이 아이디어를 어떻게 떠올리지?

=> 일단 경험치로 가지고 가자!! 

근데, 솔직히 이 문제는 내 풀이(위에 3차 시도)로 순회로 푸는 게 더 나아보이기도 함.

 

enumerate 사용한 건 굳이같기도 함. 그냥 parents[index] ← parent 로 대체 가능.(enumerate 사용이 더 직관적이긴 함)

 

이건 순수 dfs가 되므로, 내부에서 leafs를 바로바로 변동시킬 수 있음. 물론 leafs를 직접 세는 코드를 외부에 구현할 수도 있지만(아마 이렇게 하면, [-1, -1]인 것중에, 그 놈을 자식으로 가지는 놈이 있으면 → leafs += 1 하면 될듯), 바로바로 업데이트가 되는데 굳이?

import sys

input = sys.stdin.readline


def dfs(node):
    # 리프 노드인 경우
    if not tree[node]:
        return 1 

    leafs = 0

    for next_node in tree[node]:
        leafs += dfs(next_node)  # 자식 노드들을 탐색하며 리프 노드의 개수 더하기

    return leafs
    # return sum(dfs(next_node) for next_node in tree[node])


n = int(input())
parents = list(map(int, input().split()))
target = int(input())  # 삭제할 노드

# 트리 만들기 (트리가 이진트리가 아닐 수 있음에 주의!)
tree = [[] for _ in range(n)]

for child, parent in enumerate(parents):
    if parent == -1:
        root = child  # 루트 노드 찾기
        continue

    if child == target:
        continue  # 삭제할 노드는 트리에 포함하지 않음

    tree[parent].append(child)

if target == root:
    print(0)  # 루트 노드를 삭제하는 경우 0 출력
else:
    print(dfs(root))

추가로, 이 문제는 궁금해서 이후 다른분들 풀이도 많이 찾아봤는데,

내 3차 시도처럼 푸신 분들도 계시고, 강사님 풀이처럼 우리가 해오던 트리 표현 방식으로 푸신 분들도 계셨다.

 

내 풀이처럼 푼 분은 이 분이 비슷했다.

https://hstory0208.tistory.com/entry/Python%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EB%B0%B1%EC%A4%80-1068-%ED%8A%B8%EB%A6%AC

 

아래 분은 내 풀이랑 방식은 같은데, dfs(겸 순회)가 아닌 bfs를 하셨다. bfs도 충분히 가능!

https://www.acmicpc.net/source/90242839

 

그리고, 강사님 정답 풀이와는 아래 분 풀이가 비슷했다.

https://velog.io/@ice-prince/백준-1068-트리-Python


 

5차 시도) 위 블로그 글 보고, 3차 시도에서 추가 수정

그냥 ‘지웠음’ 표시를 -1이 아닌, -2, -3 따위의 magic number로 처리하면 되는 문제였음!!!!!

아!!!!!!!!!!!!!!!!

이제 루트 노드 관련 엣지 케이스 다 대응가능. ㅎㅎ

import sys

input = sys.stdin.readline

# 특정 노드를 제거한 후의 리프 노드의 개수를 세자
# 입력 : 트리의 노드 개수 n, 둘째 줄부턴 0 - n-1 번 노드까지 각 노드의 부모 주어짐
# 셋째 줄에는 지울 노드 번호

n = int(input())
parent = list(map(int, input().split()))
t = int(input())


# 지우는 건 둘째치고, 리프 노드 개수를 어떻게 셀 수 있지?
# 지우는 거는 뭐 그 노드를 부모로 가지는 노드들을 -1로 값 바꿔주면 되지 않을까?
# 탐색하면서, 그 노드의 값이 parent에 없으면 리프 노드다? 이거 괜찮아보이는데?

# 일단 n이 50 이하인데, 좀 비효율적이어도 될 듯함.
# while 돌면서 flag 설정해두고, 순차적으로 다 지우면 될 듯한데?
# 어? dfs? bfs?

def dfs(node):
    for i in range(n):
        if parent[i] == node:  # 만약 i가 node를 부모로 가지면
            dfs(i)  # i 기준으로 다시 dfs 돌림
    parent[node] = -2


dfs(t)

counts = 0

for i in range(n):
    if parent[i] != -10 and i not in parent:
        counts += 1
print(counts)