[2024.03.20] 그래프 1

체리마루·2024년 3월 20일

그래프 기본

  • 그래프
    : 정점들의 집합과 이들을 연결하는 간선들의 집합으로 구성된 자료구조 (선형 자료구조나 트리 자료구조로 표현하기 어려운 N:N 관계를 가지는 원소들을 표현하기에 용이)

  • 그래프 유형
    무향 그래프
    유향 그래프
    가중치 그래프
    사이클 없는 방향 그래프
    완전 그래프: 정점들에 대해 가능한 모든 간선들을 가진 그래프
    부분 그래프: 원래 그래프에서 일부의 정점이나 간선을 제외한 그래프

  • 인접 정점
    인접(Adjacency): 두 개의 정점에 간선이 존재(연결됨)하면 서로 인접해 있다고 한다. 완전 그래프에 속한 임의의 두 정점들은 모두 인접해 있다.

  • 그래프 경로
    경로는 간선들을 순서대로 나열한 것. 경로 중 한 정점을 최대 한 번만 지나는 경로를 단순경로라고 한다. 시작한 정점에서 끝나는 경로를 사이클이라 한다.

# 1. 그래프를 코드로 표현

# 인접 행렬
# V * V 배열을 활용해서 표현
# 갈 수 없다면 0, 있다면 1(or 가중치)을 저장

# 장점:
# 노드 간의 연결 정보를 한 방에 확인 가능
# 행렬곱을 이용해서 탐색이 쉽게 가능
# 간선이 많을수록 유리

# 단점:
# 노드 수가 커지면 메모리가 낭비된다
# 연결이 안된 것도 저장됨
# => 노드 수 + 메모리 제한 반드시 확인할 것!

# 특징: 양방향 그래프는 중앙 우하단 대각선 기준으로 대칭됨

graph = [
    [0, 1, 0, 1, 0],
    [1, 0, 1, 0, 1],
    [0, 1, 0, 0, 0],
    [1, 0, 0, 0, 1],
    [0, 1, 0, 1, 0],
]

# 인접 리스트
# V개의 노드가 갈 수 있는 정보만 저장

# 장점:
# 메모리 사용량이 적다
# 탐색할 때 갈 수 있는 곳만 확인하기 때문에 시간적으로 효율적임

# 단점:
# 특정 노드 간 연결 여부를 확인하는데 시간이 걸림

graph = [
    [1, 3],
    [0, 2, 4],
    [1],
    [0, 4],
    [1, 3],
]

DFS

# 인접 행렬 DFS: 재귀
graph = [
    [0, 1, 0, 1, 0],
    [1, 0, 1, 0, 1],
    [0, 1, 0, 0, 0],
    [1, 0, 0, 0, 1],
    [0, 1, 0, 1, 0],
]

visited = [0] * 5

def dfs(now):
    # 다음 재귀 호출 전
    visited[now] = 1
    print(now, end=' ')

    # 다음 재귀 호출
    # dfs: 현재 노드에서 다른 노드들을 확인
    # 다른 노드들 == 반복문
    for to in range(5):
        # 갈 수 없다면 pass
        if graph[now][to] == 0:
            continue
        # 이미 방문했다면 pass
        if visited[to]:
            continue

        dfs(to)

dfs(0)

# 인접 행렬 DFS: 재귀
graph = [
    [0, 1, 0, 1, 0],
    [1, 0, 1, 0, 1],
    [0, 1, 0, 0, 0],
    [1, 0, 0, 0, 1],
    [0, 1, 0, 1, 0],
]

visited = [0] * 5
path = []

def dfs(now):
    # 다음 재귀 호출
    # dfs: 현재 노드에서 다른 노드들을 확인
    # 다른 노드들 == 반복문
    for to in range(5):
        # 갈 수 없다면 pass
        if graph[now][to] == 0:
            continue
        # 이미 방문했다면 pass
        if visited[to]:
            continue

        visited[to] = 1
        path.append(to)
        dfs(to)

# 출발점 초기화
visited[0] = 1
path.append(0)
dfs(0)
print(path)

# 인접 리스트 DFS: 재귀
graph = [
    [1, 3],
    [0, 2, 4],
    [1],
    [0, 4],
    [1, 3],
]

visited = [0] * 5
path = []

def dfs(now):
    # 다음 재귀 호출
    # 인접 리스트
    # 차이점1. 갈 수 없는 곳 조건 필요 없음
    # 차이점2. for문 작성 시 인덱스 사용 필요 없음

    for to in graph[now]:
        # 이미 방문했다면 pass
        if visited[to]:
            continue

        visited[to] = 1
        path.append(to)
        dfs(to)

# 출발점 초기화
visited[0] = 1
path.append(0)
dfs(0)
print(path)

BFS

# 인접 행렬 BFS
graph = [
    [0, 1, 0, 1, 0],
    [1, 0, 1, 0, 1],
    [0, 1, 0, 0, 0],
    [1, 0, 0, 0, 1],
    [0, 1, 0, 1, 0],
]

def bfs(start):
    visited = [0] * 5

    # 시작 노드를 큐에 추가 + 방문 표시
    queue = [start]
    visited[start] = 1

    while queue:
        now = queue.pop(0)
        print(now, end=' ')

        # 갈 수 있는 곳을 체크
        for to in range(5):
            if graph[now][to] == 0:
                continue
            if visited[to]:
                continue

            visited[to] = 1
            queue.append(to)

bfs(0)

# 인접 리스트 BFS
graph = [
    [1, 3],
    [0, 2, 4],
    [1],
    [0, 4],
    [1, 3],
]

visited = [0] * 5

def bfs(start):

    # 시작 노드를 큐에 추가 + 방문 표시
    queue = [start]
    visited[start] = 1

    while queue:
        now = queue.pop(0)
        print(now, end=' ')

        # 갈 수 있는 곳을 체크
        for to in graph[now]:
            if visited[to]:
                continue

            visited[to] = 1
            queue.append(to)

bfs(0)

Union-Find(Disjoint set)

  • 서로소 집합(Disjoint-sets)
    : 서로소 또는 상호배타 집합들은 서로 중복 포함된 원소가 없는 집합들이다. 다시 말해 교집합이 없다. 집합에 속한 하나의 특정 멤버를 통해 각 집합들을 구분한다. 이를 대표자(representative)라 한다.

  • 상호배타 집합을 표현하는 방법
    : 연결리스트 / 트리

  • 상호배타 집합 연산
    Make-Set(x): 집합 만들기 (처음에는 자기 자신이 대표)
    Find-Set(x): 집합의 대표 찾기
    Union(x, y): 같은 집합으로 묶기

# 1~6번 노드 존재

# 1. make_set
def make_set(n):
    return [i for i in range(n)]

# 2. find_set: 대표자를 찾아보자
# 부모 노드를 보고, 부모 노드도 연결이 되어 있다면 다시 반복
# 언제까지? 자기 자신이 대표인 데이터를 찾을 때까지
parents = make_set(7)
def find_set(x):
    # 자기 자신이 대표라면? 끝
    if parents[x] == x:
        return x

    # 위의 조건이 걸리지 않았다? 대표자가 따로 있다.
    return find_set(parents[x])

# 3. union
def union(x, y):
    x = find_set(x)
    y = find_set(y)

    # 이미 같은 집합에 속해있다면 continue
    if x == y:
        return

    # 다른 집합이라면 합침
    # 예) 더 작은 루트 노드에 합쳐라
    if x < y:
        parents[y] = x
    else:
        parents[x] = y

union(1, 3)
union(2, 3)
union(5, 6)

# make 과정: 초기화, 나 자신을 부모로 갖는 단일 그룹 조성
def init(n):
    # 각 요소가 자기 자신을 부모로 갖는 일차원 배열을 반환
    parent = list(range(n)) # 0 ~ n-1
    return parent

# find: x의 그룹의 대표(조상)를 찾아라
def find(x):
    if x == parent[x]:
        return x
    parent[x] = find(parent[x])
    return parent[x]

# union: x와 y의 속한 두 그룹을 하나로 합쳐라 (각 대표자들을 찾고 합치기)
def union(x, y):
    root_x = find(x)
    root_y = find(y)

    parent[root_x] = root_y # root_x -> root_y


N = 5 # 요소의 개수
parent = init(N)

union(0, 1)
union(3, 2)
union(0, 4)

print(find(1))
print(find(3))
print("부모 테이블", parent)
profile
멋쟁이 토마토 개발자 🍅

0개의 댓글