더보기

N by M 크기의 얼음 틀이 있습니다. 구멍이 뚫려 있는 부분은 0 칸막이가 존재하는 부분은 1입니다.  구멍이 뚫려있는 부분끼리 상, 하, 좌, 우로 붙어있는 경우 서로 연결되어 있는 것으로 판단합니다. 이러한 경우에서 얼음 틀의 모양이 그래프로 주어진다면 생성되는 아이스크림의 갯수는 몇 개일까요?

graph_45 = [
    [0,0,1,1,0],
    [0,0,0,1,1],
    [1,1,1,1,1],
    [0,0,0,0,0]
]

n = 4
m = 5

 

첫째 / BFS 방식으로 풀기

 

출발점에서부터 점진적으로 진행해가는 방식으로 접근하는 경우 BFS를 고려해볼 수 있습니다.

일단 BFS니까 아묻따 디큐부터 임포트해주고 시작합니다.

 

from collections import deque

 

 

1. 함수를 작성합니다. 입력받은 출발점 좌표로부터 시작하여 출발점 좌표가 1이면 바로 False를 리턴하고, 출발점 좌표가 0이면 주변을 탐색하면서 0이 있으면 1로 바꾸고, 더이상 바꿀 0이 없는 경우에는 True를 리턴하는 함수를 작성합니다.

2. 함수 작성 이후, 주어진 그래프의 (0, 0)부터 시작하여 n*m 좌표마다 함수를 적용하도록 이터레이션을 돌립니다. True가 반환되는 갯수가 곧 음료수 얼음 덩어리의 갯수가 됩니다.

 

def ice_bfs(row_init, col_init):
    # 만약 주어진 좌표가 0이라면 가보자고!
    if graph[row_init][col_init] == 0:
        q = deque([[row_init, col_init]])
        # ---------------------------------------- 무한 루프 
        while True:
            if not q:
                return 1

            row, col = q.popleft()
            for [dx, dy] in [[0, -1], [0, 1], [1, 0], [-1, 0]]:
                # 이동한 값이 지도 안에 있고
                if 0 <= row + dx < n and 0 <= col + dy < m:
                    # 얼릴 수 있다면
                    if graph[row+dx][col+dy] == 0:
                        # 1번 도장을 찍고, 그 다음 할일 추가하세요
                        graph[row+dx][col+dy] = 1
                        q.append([row+dx, col+dy])
        # ---------------------------------------- 무한 루프 
    else:
        return 0
result = 0

for row in range(n):
    for col in range(m):
        result += ice_bfs(row, col)

print(result)
# 3

 

 

둘째 / DFS 방식으로 풀기

위에서 작성한 함수를 DFS방식을 활용한 재귀함수로 바꾸어 작성해 봅시다. (같은 기능!)

 

def dfs_recursive(row, col):
    # 주어진 점이 틀 밖이라면 바로 함수 종료
    if row <= -1 or row >= n or col <= -1 or col >= m:
        return False
    
    # [1] 주어진 점이 0이라면 True를 반환
    if graph[row][col] == 0:
        
        graph[row][col] = 1  # 일단 True 반환 전에 0->1로 바꾸고
        dfs_recursive(row, col-1)
        dfs_recursive(row-1, col)
        dfs_recursive(row, col+1)
        dfs_recursive(row+1, col) # 상하 좌우도 전부 점검해주기!

        return True

    # [2] 주어진 점이 1이라면 False를 반환
    else:
        return False

 

같은 방식으로 모든 좌표점에 작성한 함수를 돌려주면서 True가 반환되는 개수를 세어 주면 끝~

 

result = 0

for row in range(n):
    for col in range(m):
        result += dfs_recursive(row, col)

print(result)
# 3

 

 

 

짱짱 재밌군!

BFS도 DFS도 자유자재로 활용할 수 있도록 열심히 반복!

 

 

다음 문제로는 카카오 2022 블라인드 공개채용 <양과 늑대> 문제를 풀어봅시다...

 

 

 

 

백준 2178번 미로탐색

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

 

2178번: 미로 탐색

첫째 줄에 두 정수 N, M(2 ≤ N, M ≤ 100)이 주어진다. 다음 N개의 줄에는 M개의 정수로 미로가 주어진다. 각각의 수들은 붙어서 입력으로 주어진다.

www.acmicpc.net

 

 

n = 4
m = 6

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

 

0과 1로 이루어진 n * m 크기의 미로 그래프가 있다. 미로에서 1은 이동할 수 있는 칸을 나타내고, 0은 이동할 수 없는 칸을 나타낸다. 이러한 미로가 주어졌을 때, (1, 1)에서 출발하여 (N, M)의 위치로 이동할지나야 하는 최소의 칸 수를 구하는 프로그램을 작성하시오. 한 칸에서 다른 칸으로 이동할 때, 서로 인접한 칸으로만 이동할 수 있다. 위의 예에서는 15칸을 지나야 (N, M)의 위치로 이동할 수 있다. 칸을 셀 때에는 시작 위치와 도착 위치도 포함한다.

 

 

dx = [-1, 1, 0, 0]
dy = [0, 0, 1, -1]

 

먼저 상하좌우로 움직일수 있는 x, y 델타값을 리스트로 작성한다.

def dfs_escape(start = (0, 0)):
	# x, y의 현재 포지션을 정해 준다. 
    x_pos = start[0]
    y_pos = start[1]
    
    # 할 일을 기록할 디큐 리스트 q를 작성한다.
    q = deque()
    
    # 가장 처음 초기값을 세팅해준다. x, y의 좌표값 위치를 튜플로 집어넣는다.
    q.append((x_pos, y_pos))
    
    # 할 일이 없을 때까지 반복!
    while q:
    	now_x, now_y = q.popleft()
        
        # 상하좌우 4번 반복
        for i in range(4):
        	next_x = now_x + dx[i]
        	next_y = now_y + dy[i]
            
            # 만약 다음으로 갈 좌표값이 미로를 벗어나지 않는다면
            if 0 <= next_x < n and 0 <= next_y < m:
                if graph[next_x][next_y] == 1:
                    graph[next_x][next_y] = graph[now_x][now_y] + 1
                    q.append((next_x, next_y))

 

bfs_miro()


graph
# [[3, 0, 9, 10, 11, 12],
#  [2, 0, 8, 0, 12, 0],
#  [3, 0, 7, 0, 13, 14],
#  [4, 5, 6, 0, 14, 15]]


graph[n-1][m-1]
# 15

 

 

 

 

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

 

18352번: 특정 거리의 도시 찾기

첫째 줄에 도시의 개수 N, 도로의 개수 M, 거리 정보 K, 출발 도시의 번호 X가 주어진다. (2 ≤ N ≤ 300,000, 1 ≤ M ≤ 1,000,000, 1 ≤ K ≤ 300,000, 1 ≤ X ≤ N) 둘째 줄부터 M개의 줄에 걸쳐서 두 개

www.acmicpc.net

 

어떤 나라에는 1번부터 N번까지의 도시와 M개의 단방향 도로가 존재한다. 모든 도로의 거리는 1이다. 이 때 특정한 도시 X로부터 출발하여 도달할 수 있는 모든 도시 중에서, 최단 거리가 정확히 K인 모든 도시들의 번호를 출력하는 프로그램을 작성하시오. 또한 출발 도시 X에서 출발 도시 X로 가는 최단 거리는 항상 0이라고 가정한다. 예를 들어 N=4, K=2, X=1일 때 다음과 같이 그래프가 구성되어 있다고 가정하자.

 

이 때 1번 도시에서 출발하여 도달할 수 있는 도시 중에서, 최단 거리가 2인 도시는 4번 도시 뿐이다. 2번과 3번 도시의 경우, 최단 거리가 1이기 때문에 출력하지 않는다. 

 

[ 입력 조건 ] 첫째 줄에 도시의 개수 N, 도로의 개수 M, 거리 정보 K, 출발 도시의 번호 X가 주어진다. (2 ≤ ≤ 300,000, 1 ≤ ≤ 1,000,000, 1 ≤ ≤ 300,000, 1 ≤ ≤ N) 둘째 줄부터 M개의 줄에 걸쳐서 두 개의 자연수 A, B가 공백을 기준으로 구분되어 주어진다. 이는 A번 도시에서 B번 도시로 이동하는 단방향 도로가 존재한다는 의미다. (1 ≤ A, ≤ N) 단, AB는 서로 다른 자연수이다.

 

[출력조건] X로부터 출발하여 도달할 수 있는 도시 중에서, 최단 거리가 K인 모든 도시의 번호를 한 줄에 하나씩 오름차순으로 출력한다. 이 때 도달할 수 있는 도시 중에서, 최단 거리가 K인 도시가 하나도 존재하지 않으면 -1을 출력한다.

입력 예시 1.

4 4 2 1
1 2
1 3
2 3
2 4
입력 예시 2.

4 4 1. 
1 2
1 3
2 3
2 4
입력 예시 3.

4 3 2 1
1 2
1 3
1 4

출력 예시 1.
4

출력 예시 2.
2
3
출력 예시 3.
-1

 

예시 2번으로 문제를 풀어보자.

n, m, k, x = map(int , input().split())
# 4 4 1 1


graph = [[] for _ in range(n+1)]
for _ in range(m):
    a, b = map(int, input("출발 도착").split())
    graph[a].append(b)
    
    
graph
# [[], [2, 3], [3, 4], [], []]

 

먼저 입력을 받아 n, m, k, x값을 할당받고,

graph 리스트를 만들어 각 도시별로 연결된 도시들을 표시해준다.

 

INF = float("inf")
[INF] * (n + 1)
# [inf, inf, inf, inf, inf]

 

앞으로 출발 도시 x로부터 각각의 모든 도시까지 거리를 distance로 계산해줄 것이다.

(n+1)을 곱해주는 이유는 가상의 0번 도시를 설정하여 인덱스를 편하게 계산해주기 위함이다.

앞으로 distance에서 자기 자신과의 거리는 0으로, 자기 자신을 제외한 다른 도시는 모두 INF로 시작하는 점을 기억하자.

 

def solution(start):
    # 앞으로 방문해야할 도시 리스트는 디큐 리스트 q로 둔다.
    q = deque()
    # 출발점 도시 x로부터의 거리를 나타내는 디큐 리스트 distance를 만든다.
    distance = deque([INF] * (n+1))
    
    # 초기값 세팅
    q.append(start)
    distance[start] = 0
    
    # 더이상 방문할 도시가 없을 때까지 반복하기
    while q:
    	# 지금 방문하는 도시 now
    	now = q.popleft()
        for next_city in graph[now]:
        	# 만약 아무도 오지 않았던 신규 방문지라면
            if distance[next_city] == INF:
            	# 본 문제에서는 도시간의 거리가 1이므로 1을 더하지만
                # 문제 조건 따라 1이 아니게 될 수 있음
            	distance[next_city] = distance[now] + 1
                q.append(next_city)
                
     # 디스턴스 리스트를 리턴
     return distance

 

dis_result = bfs_city(x)
dis_result
# deque([inf, 0, 1, 1, 2])

dis_k = [i for i, v in enumerate(dis_result) if v == k]
dis_k
# [2, 3]

if dis_k == []:
    print(-1)
else:
    while dis_k:
        print(dis_k.pop(0))
# 2
# 3

 

 

연습으로 미로 탐색 백준 2178번 문제를 풀어보자.

 

from collections import deque

 

디큐는 속도가 빠른 리스트로 생각해도 좋다.

 

코딩 테스트에서 파이썬으로 Queue, BFS, FIFO 방식의 알고리즘 접근이 필요할 때 리스트를 디큐로 바꾸어주면 속도 이슈 해결이 가능하다.

 

a_list = [1, 2, 3, 4, 5]
# [1, 2, 3, 4, 5]

a_dq = deque(a_list)
# deque([1, 2, 3, 4, 5])

 

이렇게 간단하게 디큐 함수로 바꾸어주기만 하면 된다.

 

a_dq.pop()
a_dq.popright()
# 5

a_dq.popleft()
# 1

a_dq
# deque([2, 3, 4])

a_dq.append(1004)
# deque([2, 3, 4, 1004])

 

  • 디큐 맨 마지막 값을 뺄 때 : .pop()
  • 디큐 맨 처음 값을 뺄 때 : .popleft()
    • 주의 / .pop(0)과 같이 사용하면 에러 발생!
  • pop과 popleft 모두 기존의 디큐를 수정, 업데이트하는 파괴적 함수!
  • 디큐 맨 뒤에 값을 추가할 때 : .append()

 

디큐를 활용한 bfs

graph_list = [
    [],
    [2,3,8], 
    [1,7],
    [1,4,5],
    [3,5],
    [3,4],
    [7],
    [2,6,8],
    [1,7]
]


def bfs(graph, start):
    need_visit = deque([])
    visited    = deque([])
    need_visit.append( start )
    while need_visit:
        node = need_visit.popleft()
        if node not in visited:
            visited.append(node)
            temp = graph[node]
            temp_reverse = list(reversed(temp))
            need_visit.extend( temp_reverse )
    return visited
    
    
  bfs(graph_list, 1)
  # deque([1, 8, 3, 2, 7, 5, 4, 6])

 

스택과 큐 모두 일을 처리하는 순서, 값을 처리하는 순서에 대한 개념.

새롭게 추가되는 값 / 지금 당장 처리해야하는 값을 생각하며 일을 한다.

 

 

Stack

  • Last In First Out : LIFO
  • 예 / 엘레베이터 : 늦게 온 사람이 제일 먼저 내린다. (문이 하나!)
  • 새로운 것 : 뒤로 쌓는다 -> 리스트 : .append()
  • 처리할 것 : 뒤에서부터 꺼내 쓴다 -> 리스트 : .pop()
task = ['eat', 'poop', 'sleep']
task.append('cry')
>>> ['eat', 'poop', 'sleep', 'cry']
task.pop()
>>> ['eat', 'poop', 'sleep']
 

리스트.pop() 은 파괴적 함수로 리스트의 마지막 값을 제거한다.


Queue

  • First In First Out : FIFO
  • 예 / 놀이공원 매표소 : 먼저 온 사람이 먼저 떠난다! (출입구가 앞 뒤로 두 개!)
  • 새로운 것 : 뒤로 쌓는다 -> 리스트 : .append() 메소드
  • 처리할 것 : 앞에서부터 꺼내 쓴다 -> 리스트 : .pop(0) 메소드
task = ['eat', 'poop', 'sleep']
task.append('cry')
>>> ['eat', 'poop', 'sleep', 'cry']
task.pop(0)
>>> ['poop', 'sleep', 'cry']
 

task.pop(0)과 같이 맨 앞의 인덱스(0)을 지정해 주어 앞에 있던 eat이 사라졌다.


참고사항

파이썬 언어의 특성상 Queue를 사용할 때 속도 이슈가 있음을 주의하면 좋다! 제일 앞의 원소를 추출하는 pop(0) 때문이다. 따라서 코딩테스트에서 특히 주의할 것. 로직에 결함이 없더라도 시간 초과로 통과가 안될 수 있음...!

코딩 테스트에서 반드시 큐를 사용하겠다고 한다면 --> deque라고 하는 패키지를 불러서 사용하는 것이 좋다.

 

 

인접행렬

  • 자기 자신 : 0
  • 연결 된 엣지 : 1 (또는 1*weight)
  • 연결 안 된 엣지 : Inf (무한대 값으로 0과 구분)
# infinite 값을 선언하기
INF = float('inf')

# 행렬 그래프로 나타내기 - 가중치 weight 고려해서 표현
graph = [
    [0, 7, 5],
    [7, 0, INF],
    [5, INF, 0]
]
 

도시끼리 연결되었는지 어떻게 확인할까?

graph[0][1]
>>> 7 (0번과 1번 도시가 연결됨)

graph[2][1]
>>> inf (2번과 1번 도시가 연결 안 됨)
 

 

인접리스트

모든 노드에 연결된 노드에 대한 정보를 차례차례 연결하여 저장

# 연결된 도시만 리스트로 표현

graph = [
    [(1, 7), (2, 5)], # --> 0번 도시의 연결 리스트업
    [(0, 7)], # --> 1번 도시의 연결 리스트업
    [(0, 5)]  # --> 2번 도시의 연결 리스트업
]
 

도시끼리 연결되었는지 연결리스트로는 어떻게 확인할까?

graph[0][0][1]
>>> 7

# 행렬보다 직관성은 떨어질 수 있다.
 

리스트 대신 딕셔너리로 표현하여 연결성을 좀 더 직관적으로 표현할 수도 있다.

graph_dict = {
    "0" : [("1", 7), ("2", 5)],
    "1" : [("0", 7)],
    "2" : [("0", 5)],
}
 

만약 문제에서 0번 도시가 없고 1번 도시부터 있는 경우

graph = [
    [], # --> 0번 도시는 비워두고 시작
    [(1, 7), (2, 5)], # --> 1번 도시부터 기록
    [(0, 7)],
    [(0, 5)] 
]
 

이렇게 0번 도시를 빈칸으로 넣어 주면 나중에 인덱스 헷갈릴 일이 없다!!! 꿀팁

 

 

[메모리]

인접 행렬(Adjacency Matrix): 모든 관계를 저장하므로 노드 개수가 많을 수록 불필요한 메모리가 소요된다.

인접 리스트(Adjacency List): 연결된 정보만을 저장하기 때문에 메모리를 효율적으로 사용한다.

출처 입력

[속도]

인접 행렬(Adjacency Matrix): 모든 경우의 수가 제시되어 있으므로 인접리스트보다 빠르다.

인접 리스트(Adjacency List): 인접행렬방식에 비해 정보를 얻는 속도가 느리다. 인접리스트방식에서는 연결된 데이터를 하나씩 확인해야 하기 때문이다.

출처 입력

"두 노드 간에 연결이 있는지 없는지에 대한 확인" 문제

→ 인접행렬이 빠르다

 

"노드에 연결된 모든 노드들을 순회/탐색" 문제

→ 인접리스트가 효율적이다

 

 

엣지를 따라 모든 노드를 방문하는 것을 그래프 탐색이라고 한다.

대표적으로 DFS(깊이 우선 탐색), BFS(너비 우선 탐색)이 있다.

 

 

Depth First Search (깊이 우선 탐색)

깊이 우선 탐색은 기본적으로 Stack이다. (Last In First Out)

오른쪽 왼쪽 상관은 없음

 

예제

graph_list = [
    [],        # 0번 도시(없음)
    [2, 3, 8], # 1번 도시
    [1, 7],    # 2번 도시
    [1, 4, 5], # 3번 도시
    [3, 5],    # 4번 도시
    [3, 4],    # 5번 도시
    [7],       # 6번 도시
    [2, 6, 8], # 7번 도시
    [1, 7]     # 8번 도시
]
 

1번 도시 방문

  • To do : [2, 3, 8]
    • 2번 도시 방문
    • To do : [1, 7] -> 1번은 됐고 7번 방문
      • To do : [2, 6, 8] -> 2번은 됐고 6번 방문
        • To do : [7] / 끝!
        • 8번 방문 / 끝!
    • 3번 도시 방문
    • To do : [1, 4, 5] -> 1번은 됐고 4번 방문
      • To do : [3, 5] -> 3번은 됐고 5번 방문 / 끝!
Done = [1]

Done = [1, 2]

Done = [1, 2, 7]

Done = [1, 2, 7, 6]

Done = [1, 2, 7, 6, 8]

Done = [1, 2, 7, 6, 8, 3]

Done = [1, 2, 7, 6, 8, 3, 4]

Done = [1, 2, 7, 6, 8, 3, 4, 5]

예제 문제 DFS 코드(함수)로 구현하기

graph = [
    [], #0
    [2, 3, 8], #1
    [1, 7], #2
    [1, 4, 5], #3
    [3, 5], #4
    [3, 4], #5
    [7], #6
    [2, 6, 8], #7
    [1, 7], #8
]

def dfs(graph, start_city) :
    to_visit = graph[start_city]
    done_visit = [start_city]

    while to_visit:
        visited = to_visit.pop()
        if visited not in done_visit:
            done_visit.append(visited)
            to_visit.extend(graph[visited])

    return done_visit

dfs(graph, 1)
>>> [1, 8, 7, 6, 2, 3, 5, 4]
 

pop으로 꺼내서 할당하는 visited의 값이 가장 큰 숫자부터 고르기때문에 [1, 8, 7, 6, 2, 3, 5, 4]가 나온다. 예제 문제처럼 pop으로 꺼내는 도시가 가장 작은 숫자부터 고르게 하려면 어떻게 해야 할까?

def dfs_2(graph, start_city) :
    to_visit = list(reversed(graph[start_city])) #💛
    done_visit = [start_city]
    while to_visit:
        visited = to_visit.pop()
        if visited not in done_visit:
            done_visit.append(visited)
            temp = graph[visited] #💛
            temp_reversed = list(reversed(temp)) #💛
            to_visit.extend(temp_reversed) #💛
    return done_visit

dfs_2(graph, 2)
>>> [1, 2, 7, 6, 8, 3, 4, 5]
 

뒤집어서 맨 뒤부터 추가해주기!

 

 

 

BFS의 경우 pop()을 pop(0)으로 변경해주면 끝인데,

속도 이슈가 있기때문에 코딩테스트에 권장하지 않음

-> 너비 우선 탐색법이 필요한 경우 depop을 고려

 

+ Recent posts