[#1043] 거짓말

RookieAND·2022년 11월 24일
0

BaekJoon

목록 보기
34/42
post-thumbnail

❓ Question

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

📖 Before Start

Python에서 제공하는 Set 자료구조를 활용하여 문제를 풀었다.
하지만 이 외에도 Union-Find 알고리즘으로도 풀이가 가능하다.

이번 문제는 Set 자료구조와 교집합, 합집합 연산을 통해 구현이 가능했다.
하지만 Union-Find 라는 알고리즘 또한 활용이 가능하다는 것을 알았다.

✒️ Design Algorithm

Set 자료구조와 교집합, 합집합 연산을 통해 구현이 가능했다.

파티에 참여한 인원 NM 개의 파티가 개최되었다.
파티에서 진실을 아는 인원이 있다면, 나머지 구성원들 또한 진실을 알게 된다.

지민이는 모든 파티를 순회하며 거짓말을 최대한 많이 해야만 싶다.
즉 진실을 모르는 이들만 모인 그룹을 찾아야 거짓말을 전파해야 한다.

따라서 처음에는 집합 (Set) 을 활용해야 하는 문제라고 생각했다.

💻 Making Own Code

❌ Wrong Code

import sys

read = sys.stdin.readline
N, M = map(int, read().split())
know_true = set(read().split()[1:])

parties = [set(read().split()[1:]) for _ in range(M)]

for party in parties:
    if know_true.intersection(party):
        know_true = know_true.union(party)

result = 0
for party in parties:
    if not know_true.intersection(party):
        result += 1
print(result)

접근은 좋았으나, 이 코드는 입력 순서 에 따른 오류를 잡지 못했다.

처음에는 모든 파티의 구성원 정보를 전부 입력 받은 후, 이를 한 번 순회하였다.
순회 과정에서 진실을 알게 된 이들과 동행한 사람들도 진실을 알게 되었으므로
해당 동행한 이들을 모두 합집합 연산 (union) 을 통해 포함시켰다.

그리고 다음 줄에는 다시 모든 파티를 순회하면서 최종적으로 산출된 집합을 통해
진실을 아는 이들이 파티에 섞여 있는지를 체크했다. 하지만 오답 처리를 받았다.

그 이유는 새롭게 진실을 알게 된 이들을 고려하지 않고, 연산을 진행했기 때문이다.
아래의 예시가 바로 내가 세운 코드의 반례였다.

4 6
1 1 
1 4	
1 3
2 2 4
1 2
2 2 3
2 4 1

기대한 값은 0 이였으나, 어째서인지 3 이 나온다.

위 예시에서 진실을 아는 이는 1번 이며, 이후 1번4번 과 만나 진실을 듣는다.
그리고 4번2번 과 만났기 때문에 지민이는 거짓말을 할 수 없는 게 정상이다.

하지만 상단의 코드에서는, 진실을 아는 이들을 추려내는 과정에서 이를 고려하지 않는다.
즉, 앞서 입력된 4번2번 이 참석한 파티에서는 거짓말을 해도 된다고 판단한 것이다.

정석대로라면 1번4번 과 진실을 듣고, 4번2번 과 진실을 들어야 정상인데,
단순히 입력받은 순서대로만 이를 처리하면 1번4번 만 진실을 들었다고 처리한다.
그렇다면 지민이는 2번3번 이 모인 자리에서 안일하게 거짓말을 하다 들킬 것이다.

✅ Correct Code

import sys

read = sys.stdin.readline
N, M = map(int, read().split())
know_true = set(read().split()[1:])

# 파티 그룹을 우선 집계하여 이를 체크.
parties = [set(read().split()[1:]) for _ in range(M)]

# 파티를 진행하면서, 진실을 알게 된 이들을 체크.
for _ in range(M):
    for party in parties:
        # 파티 인원 중, 진실을 아는 이들이 있다면 모든 참가자를 포함시킴.
        if know_true.intersection(party):
            know_true = know_true.union(party)

print(know_true)

# 파티 구성원 중, 진실을 알게 된 이들이 포함되지 않은 파티만 체크.
result = 0
for party in parties:
    if not know_true.intersection(party):
        result += 1
print(result)

결국 거짓말을 하면 안되는 인물을 추가 반복을 통해 파악해야 했다.

따라서 정확하게 이를 해결하려면 파티의 수 (M) 만큼 탐색 과정을 또 거쳐야 한다.
그래야만 새롭게 진실을 전달받은 이들에 대해서도 올바르게 탐색을 마칠 수 있다.

✅ Correct Code (with Union Find)

import sys

# x 노드가 속한 집합의 루트 노드를 탐색하는 함수.
def find(x):
    if x != parents[x]:
        parents[x] = find(parents[x])

    return parents[x]

def union(a, b):
    a, b = find(a), find(b)

    # 둘 다 진실을 아는 경우, 굳이 Union을 안해도 됨.
    if a in know_true and b in know_true:
        return

    # a 가 진실을 아는 집합에 소속되어 있다면, b가 소속된 집합을 포함시킴.
    if a in know_true:
        parents[b] = a

    # b 가 진실을 아는 집합에 소속되어 있다면, a가 소속된 집합을 포함시킴.
    elif b in know_true:
        parents[a] = b

    # 루트 노드가 같지 않을 경우, 두 집합을 서로 이어줌.
    # 여기서는 노드의 값이 큰 것이 작은 것에 연결되게끔 함.
    else:
        parents[max(a, b)] = min(a, b)

read = sys.stdin.readline
N, M = map(int, read().split())
know_true = list(map(int, read().split()))[1:]

# 유니온 파인드 연산을 위해, 각 노드 별 루트 노드를 보관하는 list.
parents = list(range(N + 1))
parties = []

for _ in range(M):
    peoples, *party = list(map(int, read().split()))
    # 해당 파티의 참가자들을 대상으로 Union - Find 진행.
    for i in range(peoples - 1):
        union(party[i], party[i + 1])
    parties.append(party)

result = 0
for party in parties:
    for visitor in party:
        # 만약 해당 파티의 참가원이 속한 그룹의 루트 노드가 진실을 아는 이라면, 해당 파티 제외.
        if find(visitor) in know_true:
            break
    else:
        result += 1

print(result)

하지만 이 문제는 유니온 파인드 라는 개념을 통해서도 접근이 가능했다.
해당 알고리즘을 학습한 후, 이를 토대로 작성한 코드는 상단과 같다.

유니온 파인드의 요지는 서로 연관된 노드를 하나의 트리 (집합) 로 묶어 관리하는데,
추후 두 노드가 같은 집합에 소속되었는지를 find 함수를 통해 체크할 수 있다.

유니온 파인드 관련 문제는 금일 중으로, 혹은 내일까지 2~3문제를 더 풀이할 예정이다.
오늘은 이런 알고리즘 방식도 존재하는구나~ 로 마무리 하려 한다.

📖 Conclusion

https://github.com/RookieAND/BaekJoonCode

지금부터라도 잊었던 감을 다시금 되찾는 노력을 기울여야겠다. 분발하자.

profile
항상 왜 이걸 써야하는지가 궁금한 사람

0개의 댓글