http://boj.kr/2252 

 

2252번: 줄 세우기

첫째 줄에 N(1≤N≤32,000), M(1≤M≤100,000)이 주어진다. M은 키를 비교한 회수이다. 다음 M개의 줄에는 키를 비교한 두 학생의 번호 A, B가 주어진다. 이는 학생 A가 학생 B의 앞에 서야 한다는 의미이다. 학생들의 번호는 1번부터 N번이다.

www.acmicpc.net

 

 

위상 정렬 기본 문제이다.

위상 정렬이 무엇인가 하면, 어떤 유향 그래프가 있을 때 선후 관계를 해치지 않도록 노드를 정렬하는 것이다. 

당연하게도 이 유향 그래프에는 사이클이 있으면 안 된다. 사이클이 있으면 선후 관계라는 게 있을 수가 없기 때문이다.

 

위상 정렬을 하는 방법은 두 가지가 있다. 하나는 재귀를 사용하는 방법, 다른 하나는 재귀를 사용하는 방법이다.

조금 더 직관적이지만 구현이 복잡한 노 재귀 방법부터 보자.

 

 

다음과 같은 사이클이 없는 유향그래프가 있고, 정렬된 배열을 S라고 하자.

첫 번째 방법은 한 마디로 설명하면 '시작 노드를 그래프에서 빼서 순서대로 놓는 것'이다.

시작노드들이란 그 노드로 들어오는 간선이 없는 노드들을 말한다.

즉 인접 리스트 등에서 어떤 노드보다 뒤에 있다고 설명되지 않는 노드들이다.

초기 그래프와 정렬된(텅 빈) 배열 S이다.

여기서 시작노드들은 1과 5이다. 노드 1이나 5로 들어오는 간선이 없기 때문에 1과 5는 시작 노드이다.

위상 정렬을 하기 위해서는 이 시작 노드 중 하나를 선택해서, 그 노드와 그 노드로부터 출발하는 진출 간선을 모두 삭제한다. 1을 선택해보자.

 

 

 

 

 

1과 1의 진출 간선이 삭제되었다. 그리고 1이 정렬된 배열 S의 맨 뒤에 추가되었다. 

간단하게도 이것을 계속 반복하면 된다.

삭제된 1의 진출 간선과 연결되어 있던 2는 진입 간선을 하나 잃었다. (1의 진출 간선=2의 진입 간선)

이제 진입 간선이 없는 시작 노드는 2, 5가 되었다. 다시 그 중에서 하나를 선택해 그 노드와 진출 간선을 모두 삭제한다. 이번에는 2를 선택해보자.

 

 

 

 

2를 삭제하고 2로부터 출발하는 진출 간선들을 모두 삭제했다. 그리고 2를 정렬된 배열 S의 맨 뒤에 추가했다.

이제 시작 노드는 5 하나이다.

 

 

 

5를 삭제하고 배열 S에 추가했고, 5로부터 출발하는 5의 진출 간선들을 모두 삭제했다. 

이제 시작 노드는 4와 6이다. 이 중 4를 선택해보자.

 

 

 

 

4를 삭제하고 배열 S에 추가한 뒤, 4의 진출 간선들을 모두 삭제했다. 

이제 시작 노드는 6 하나 뿐이다.

 

 

 

6을 삭제하고 배열 S에 추가했다. 그리고 배열 S에 추가했다.

이제 시작 노드는 3 하나 뿐이다.

 

 

3을 삭제하고 배열 S의 맨 뒤에 추가했다. 이제 남은 노드가 없으므로 배열 S가 위상 정렬이 완료된 배열이 되었다.

 

 

import sys


def solution():
    for _ in range(N):
        u = start_v.pop()  # 시작 노드 중 하나를 꺼낸다
        answer.append(u+1)  # 배열의 맨 뒤에 삽입
        for node in out[u]:  # 노드 u와 연결된 간선들을 삭제하면 그 간선들과 연결된 노드들의 진입 간선 수가 줄어든다
            in_num[node] -= 1
            if in_num[node] == 0:  # 진입 간선이 0이 된 간선은 시작 노드 배열에 추가
                start_v.append(node)


answer = []
input = sys.stdin.readline
N, M = map(int, input().split())  # N = 학생수, M = 비교횟수
start_v = []  # 진입 간선이 0인 시작 노드
in_num = [0 for _ in range(N)]  # 진입 간선의 수. in_num=0인 노드가 시작 노드가 된다

out = [[] for _ in range(N)]  # 모든 노드에 대해 선후 관계를 정리한 배열

# 노드들의 선후 관계가 입력으로 들어온다. 배열의 인덱스는 0부터 시작이므로 각각 1씩 마이너스해서 배열에 담는다.
for _ in range(M):
    a, b = map(int, input().split())
    out[a-1].append(b-1)
    in_num[b-1] += 1

for idx, num in enumerate(in_num):
    if num == 0:
        start_v.append(idx)  # 진입간선이 0인 정점들은 먼저 줄세운다

solution()

for a in answer:
    print(a, end=' ')

위상정렬.pptx
0.05MB

 

 

 

두 번째 방법은 재귀를 이용하는 것이다. 모습이 DFS를 닮았기 때문에 DFS 방법이라고 한다.

이번에는 아무 노드나 선택한 후, 그 노드와 연결된 노드들을 재귀로 찾아서 배열의 맨 앞에 삽입하면 된다.

import sys
from collections import deque


def solution():
    for i in range(N):
        if not visited[i]:
            dfs(i)


def dfs(v):
    visited[v] = True
    for next_v in out[v]:
        if not visited[next_v]:
            dfs(next_v)
    answer.appendleft(v+1)


answer = deque()
input = sys.stdin.readline
N, M = map(int, input().split())  # N = 학생수, M = 비교횟수
visited = [False for _ in range(N)]
out = [[] for _ in range(N)]  # 인접 리스트. 진출 간선들을 정리한 리스트.
for _ in range(M):
    a, b = map(int, input().split())
    out[a-1].append(b-1)
    
solution()
for a in answer:
    print(a, end=' ')

 

크루스칼 알고리즘은 최소 신장 트리(MST: Minimum Spanning Tree)를 찾는 알고리즘이다.

프림 알고리즘과 같이 그리디 알고리즘에 속한다.

간선을 가중치 순으로 정렬하고 가중치가 짧은 순서대로 그래프에 포함시키기 때문이다.

 

프림 알고리즘은 비어 있는 트리에 정점을 하나씩 추가시키면서 조금씩 트리의 크기를 키워나가는 방식이었다.

반면 크루스칼 알고리즘은 애초에 모든 정점을 크기가 1인 트리라고 가정한다.

이러한 트리 사이의 간선을 가중치가 작은 순서대로 정렬한 후, 조각조각 간선을 이어 붙여서 최종적으로 하나의 트리로 만들어낸다.

이때 주의점은 정점을 이어붙이는 프림 알고리즘과는 달리 간선을 이어붙이기 때문에 가중치가 작은 순서대로 무조건 간선을 이어붙이다간 사이클이 생길 수가 있다는 것이다.

사이클이 생기면 더이상 트리가 아니기 때문에, 사이클이 생기도록 해선 안된다.

그렇다면 사이클이 생기는 지는 어떻게 알 수 있을까? 직접 노드의 부모를 타고 올라가서 루트가 같은지 확인해야 할까?

처음에는 그렇게 생각했지만, 트리마다 루트를 정해준 후에 두 트리를 합칠 때 루트를 비교하는 방법이 있다는 것을 알게 되었다.

예를 들어보자. A와 B가 결혼하려고 하는데 결혼하기 전에 혹시 A와 B의 집안이 같은 집안인지 확인하고 싶다. 

그럴때 A에게 부모를 물어보고, A의 부모에게 부모를 물어보고, A의 부모의 부모에게 부모를 물어보고...계속 이것을 반복해야 할까? 너무 오래 걸리는 방법이다.

한번에 알아낼 수 있는 방법이 있다. A에게 그의 시조가 누구인지 물어보면 된다.

A의 시조는 ㅇㅇㅇ이고 B의 시조는 ㅁㅁㅁ이니까 두 집안은 다른 집안이야! 이렇게 시조를 저장해놓으면 두 집안이 같은지 O(1)만에 알아낼 수 있을 것이다.

 

여기 코드에서 시조, 즉 루트를 알아내는 함수는 find_set이다. find-set은 집합 처리 부분에서 나오는 그것인데 집합의 루트를 반환한다고 생각하면 된다. 

 

그리고 두 집안이 다르면 결혼을 성사시키는데, 미국의 경우 대부분 여자가 남자의 성을 따라간다.

트리에도 이것을 적용해볼 수 있다. 남자 A의 시조가 스미스 씨였고 여자 B의 시조는 베이커 씨였는데, A와 B가 결혼함으로써 B의 시조가 스미스 씨가 되는 것이다! (실제로는 아니겠지만 크루스칼 알고리즘 사회에서는 그렇다고 치자)

즉, 노드 B의 루트가 A의 루트를 가리키게 하면 된다.

실제로 크루스칼 알고리즘에서 누가 누구를 가리킬 것이냐에 대해서는 그저 한 가지 기준을 정하면 되지만 여기서는 노드의 숫자가 큰 것이 → 작은 것을 가리키게 하는 방법을 썼다.

 

여기 코드에서 두 트리를 병합하는 함수는 union_set이다.

 

갑자기 생각난건데, 전 세계 인구를 족보 순으로 가장 가까운 순으로 연결시켜서 하나의 족보로 만든다면 그것이 크루스칼 알고리즘이 아닐까?

 

# 크루스칼 알고리즘


# 모든 간선의 정보를 저장할 클래스. a는 한 쪽 노드, b는 다른 한 쪽 노드이며 dist는 a와 b 사이의 가중치(거리)이다.
class Edge:
    def __init__(self, a, b, dist):
        self.a = a
        self.b = b
        self.dist = dist


# 루트 노드를 반환하는 함수.
# 루트 노드를 구할 때 그냥 root[x]해도 되겠지만 집합 처리 개념을 사용하기 위해 함수로 따로 빼냈다.
def find_set(x):
    return root[x]


# 두 트리를 병합하는 함수. 일관성을 위해 노드 번호가 더 작은 것을 루트로 만든다.
def union_set(a, b):
    a = find_set(a)
    b = find_set(b)
    if a < b:
        root[b] = a
    else:
        root[a] = b


# edges에는 [정점 a, 정점 b, a-b 사이의 거리]를 담은 Edge 객체들이 저장되어 있다.
edges = list()
edges.append(Edge(1, 7, 12))
edges.append(Edge(1, 4, 28))
edges.append(Edge(1, 2, 67))
edges.append(Edge(1, 5, 17))
edges.append(Edge(2, 4, 24))
edges.append(Edge(2, 5, 62))
edges.append(Edge(3, 5, 20))
edges.append(Edge(3, 6, 37))
edges.append(Edge(4, 7, 13))
edges.append(Edge(5, 6, 45))
edges.append(Edge(5, 7, 73))

n = len(edges)
root = [i for i in range(n)]  
# 모든 트리의 루트를 저장. 맨 처음에는 노드가 하나의 트리이므로 자기 자신이 루트이다.

total = 0  # 최단 거리

edges.sort(key=lambda x: x.dist)  # 우선 간선을 가중치 순으로 정렬. 가중치가 작은 순으로 순회

for edge in edges:  # 모든 간선에 대해서 반복한다.
    if find_set(edge.a) != find_set(edge.b):
    # 두 노드가 포함된 트리의 루트가 서로 다를 때(=두 트리를 합쳐도 사이클이 생기지 않을 때)
        union_set(edge.a, edge.b)  # 두 노드(가 포함되어 있는 트리)를 병합
        total += edge.dist
print(total)

'CS > 알고리즘' 카테고리의 다른 글

Merge Sort in Python (병합 정렬)  (0) 2020.06.09
Insertion Sort in Python (삽입 정렬)  (0) 2020.06.09
프림 알고리즘 (Python)  (0) 2020.05.07
Call by Value vs Call by Reference  (0) 2020.04.28
Selection Sort (선택 정렬)  (0) 2020.04.24

http://boj.kr/3190 

 

3190번: 뱀

문제  'Dummy' 라는 도스게임이 있다. 이 게임에는 뱀이 나와서 기어다니는데, 사과를 먹으면 뱀 길이가 늘어난다. 뱀이 이리저리 기어다니다가 벽 또는 자기자신의 몸과 부딪히면 게임이 끝난다. 게임은 NxN 정사각 보드위에서 진행되고, 몇몇 칸에는 사과가 놓여져 있다. 보드의 상하좌우 끝에 벽이 있다. 게임이 시작할때 뱀은 맨위 맨좌측에 위치하고 뱀의 길이는 1 이다. 뱀은 처음에 오른쪽을 향한다. 뱀은 매 초마다 이동을 하는데 다음과 같은 규칙을 따

www.acmicpc.net

 

 

'뱀'은 머리가 붙었다가 꼬리가 떼졌다가 즉 앞뒤로 데이터를 넣었다가 뺐다가 해야 하는 이유로 deque를 사용했다.

board에서 뱀은 2, 사과는 1, 나머지는 0으로 표시했다.

 

move 리스트에 담은 L개의 방향 전환 정보는 '게임 시작 후 X초가 지난 후에 방향을 전환한다'는 뜻이다. 

 

time을 증가시켜가면서 매 초마다 move 안에 담긴 X초와 같은지 비교하고, 같으면 방향을 바꿨다. 

그리고 그렇게 방향을 전환했을 때마다 move 리스트의 인덱스를 가리키는 idx를 1씩 증가시켰다.

방향 전환이 끝나도 뱀은 계속해서 이동하므로 while True: 안에서 이동하도록 해 주었다.

import sys
from collections import deque


def Print(lst):
    for i in lst:
        for j in i:
            print(j, end=' ')
        print("")
    print("")


def get_next_direction(now, rotate):  # (현재 방향, 움직임 명령(L or D))
    if now == 0:
        if rotate == 'L':
            return 2
        else:
            return 3
    elif now == 1:
        if rotate == 'L':
            return 3
        else:
            return 2
    elif now == 2:
        if rotate == 'L':
            return 1
        else:
            return 0
    elif now == 3:
        if rotate == 'L':
            return 0
        else:
            return 1


def solve():
    time = 0
    idx = 0  # move의 인덱스
    direct = 0  # 초기 방향 -> (0)
    while True:
        time += 1
        next_r, next_c = snake[0][R] + dx[direct], snake[0][C] + dy[direct]

        if 0 <= next_r < N and 0 <= next_c < N:
            if board[next_r][next_c] == SNAKE:  # 몸에 부딪힘
                return time
            snake.appendleft([next_r, next_c])  # 새로운 머리
            if board[next_r][next_c] != APPLE:
                tail_r, tail_c = snake.pop()
                board[tail_r][tail_c] = 0
            board[next_r][next_c] = SNAKE
        else:  # 벽에 부딪힘
            return time

        if idx < L:
            if time == move[idx][0]:
                direct = get_next_direction(direct, move[idx][1])  # 현재 방향, 다음 회전 명령
                idx += 1
                

dx = (0, 0, -1, 1)
dy = (1, -1, 0, 0)

R, C = 0, 1
APPLE, SNAKE = 1, 2
input = sys.stdin.readline
N = int(input())

board = [[0 for _ in range(N)] for _ in range(N)]
board[0][0] = 2  # 처음에 뱀은 (0, 0)에 위치하고 있음
K = int(input())
for _ in range(K):
    i, j = map(int, input().split())
    board[i-1][j-1] = APPLE

L = int(input())  # 뱀의 방향 전환 정보

move = []
for _ in range(L):
    i, j = input().split()
    move.append([int(i), j])

snake = deque([[0, 0]])  # row, col
print(solve())

'알고리즘 > 시뮬레이션' 카테고리의 다른 글

17837번. 새로운 게임 2  (0) 2020.05.03
17822번. 원판 돌리기  (0) 2020.05.02
17140번. 이차원 배열과 연산  (0) 2020.04.27
SWEA 2382번. 미생물 격리  (0) 2020.04.24
17144번. 미세먼지 안녕!  (0) 2020.04.21

http://boj.kr/17822 

 

17822번: 원판 돌리기

반지름이 1, 2, ..., N인 원판이 크기가 작아지는 순으로 바닥에 놓여있고, 원판의 중심은 모두 같다. 원판의 반지름이 i이면, 그 원판을 i번째 원판이라고 한다. 각각의 원판에는 M개의 정수가 적혀있고, i번째 원판에 적힌 j번째 수의 위치는 (i, j)로 표현한다. 수의 위치는 다음을 만족한다. (i, 1)은 (i, 2), (i, M)과 인접하다. (i, M)은 (i, M-1), (i, 1)과 인접하다. (i, j)는 (i, j-1), (i, j

www.acmicpc.net

 

 

1. 원판을 2차원 배열로 생각해서 같은 원에 있는 숫자들은 같은 행에 속한 숫자들이라고 생각하고, 위아래로 접한 숫자들은 같은 열에 속한 숫자들이라고 생각한다.

행 (펼쳐준다)

2. 문제에서 주어진 일련의 과정을 몇가지로 나눈다.

 

1) 기저사례를 확인한다. 원판에 수가 남아있는지 확인해서 남아있으면 더 진행하고, 남아있지 않으면 반복문을 종료한다.

원판에 숫자가 남아있는지 추적하기 위해 N * M = remain이라는 변수를 따로 두었다. 

 

2) 각 행을 회전시킨다. 회전을 쉽게 하기 위해서 각 원판의 자료형을 list가 아닌 deque로 두었다. deque을 시계방향으로 회전하려면 deque.rotate(k), 반시계방향으로 회전하려면 deque.rotate(-k) 하면 된다. 너무 쉽다.

너무 쉬워서 찜찜하면 temp 배열을 하나 새로 만들어서 직접 위치를 계산해서 옮겨주면 된다.

 

3) 2차원 배열을 순환하면서 각 원소마다 인접한 숫자가 있는지 확인하고, 있으면 그 숫자를 지울 것이라는 표시를 담은 erase 배열에 True라고 체크한다.

문제를 처음 풀었을 때는 지문에서 각 행렬마다 어떻게 비교해야되는지 예외 케이스까지 조건을 친절하게 알려줬으므로 그대로 따라서 if-elif 문으로 비교했다. 물론 그래도 된다. 

 

하지만 어떤 문제에서는 이 문제처럼 직접 비교 조건을 알려주지 않고 그냥 인접한 숫자끼리 비교하라고만 할 수도 있다. 그럴 때는 지문에서 알려준 것처럼 i=0일 때, i=N-1일 때, 0 < i < N-1, j=0일 때, j=M-1일때, 0 < j < M-1 일때로 나눠서 코딩하기보다는 BFS나 DFS 탐색 문제처럼 동서남북 방향의 원소와 비교해야 한다고 생각하고 코딩하는 편이 빠를 것 같다. 중첩 if문으로 코딩하면 중복된 코드가 많아져서 자료형이라도 수정해야 될 때 긴 시간을 소모해야 할 수도 있다.

 

나는 미로찾기 문제를 BFS나 DFS로 풀 때처럼 dx와 dy를 미리 선언해주었다. 하지만 동서남북 방향의 네 원소와 모두 비교할 필요는 없고, →방향과 ↓방향의 원소와만 비교해도 된다. 어차피 중복되기 때문이다. 주의해야 할 점이 col은 원형 리스트이므로, col과 아래쪽 방향 next_c를 비교하려고 할 때 next_c가 범위를 벗어나면 0으로 바꿔줘야 한다. 반면 row는 원형 리스트가 아니므로 범위를 벗어나면 무시하면 된다.

 

4) 3번에서 지울 숫자들을 다 표시했으면, 이번에는 그 erase 배열을 순환하면서 True라고 체크된 숫자들을 0으로 지우고, 지웠으면 다시 erase 배열을 반복문에서 사용하기 위해서 False로 돌려놓는다. 5)번을 수행하기 위해서 지운 숫자가 있음을 표시하는 bool 변수 once_erase를 True로 체크한다.

 

5) 지운 숫자가 있음을 표시하는 bool 변수 once_erase가 False면 평균을 구해서 평균보다 작은 숫자는 +1, 큰 숫자는 -1을 해 주었다.

import sys
from collections import deque


def solve():
    remain = N * M
    for x, direct, k in move:  # x=x의 배수인 원판을 회전,direct=시계(0),반시계(1),k=몇칸 회전하는지
        if remain <= 0:  # 남아 있는 숫자가 없으면 반복문을 빠져나온 후 sum을 리턴
            break

        # x의 배수인 원판(행)을 모두 구해서 회전시킨다.
        if direct == 0:  # 시계방향
            for next_x in range(1, N+1):  # next_x = x의 배수인 원판(=행)
                if next_x % x == 0:
                    circle[next_x-1].rotate(k)
        else:  # 반시계방향
            for next_x in range(1, N+1):
                if next_x % x == 0:
                    circle[next_x-1].rotate(-k)

        # 인접한 것이 있는지 찾고, 있으면 지운다
        for row in range(N):
            for col in range(M):
                if circle[row][col]:  # 이미 지운 숫자는 보지 않는다
                    for i in range(2):  # 오른쪽, 아래쪽과 비교
                        next_r, next_c = row + dx[i], col + dy[i]
                        if next_c > M-1:  # col이 맨 끝에 도달했을 때는 next_c를 0으로 설정한다.
                            next_c = 0

                        if next_r < N:  # r은 범위를 벗어나면 그냥 무시한다
                            if circle[next_r][next_c] == circle[row][col]:
                                erase[next_r][next_c], erase[row][col] = True, True

        once_erased = False  # 지운 숫자가 있음을 체크할 bool 변수
        for row in range(N):
            for col in range(M):
                if erase[row][col]:  # 인접한 숫자가 있는 원소 (erase가 True로 체크된 원소)
                    once_erased = True
                    circle[row][col] = 0
                    remain -= 1  # 숫자를 지웠으니 남은 숫자를 의미하는 remain에서 1을 빼준다
                    erase[row][col] = False  # 다음 반복문에서 재사용하기 위해 다시 False로 돌려놓음

        # 지울 숫자가 없으면 평균을 구하고 평균보다 크면 -1, 작으면 +1
        if not once_erased:
            s = sum([sum(row) for row in circle]) / remain
            for row in range(N):
                for col in range(M):
                    if circle[row][col] > s:
                        circle[row][col] -= 1
                    elif 0 < circle[row][col] < s:
                        circle[row][col] += 1

    # 원판의 합을 리턴
    return sum([sum(row) for row in circle])


dx = (0, 1)
dy = (1, 0)
input = sys.stdin.readline
N, M, T = map(int, input().split())  # N=원판의 갯수,M=원판에 적혀있는 수의 갯수,T=회전수
circle = [deque(map(int, input().split()))for _ in range(N)]
erase = [[False for _ in range(M)] for _ in range(N)]  # 지울 숫자임을 표시하는 bool 배열
move = [list(map(int, input().split())) for _ in range(T)]
print(solve())

 

'알고리즘 > 시뮬레이션' 카테고리의 다른 글

3190번. 뱀  (0) 2020.05.04
17837번. 새로운 게임 2  (0) 2020.05.03
17140번. 이차원 배열과 연산  (0) 2020.04.27
SWEA 2382번. 미생물 격리  (0) 2020.04.24
17144번. 미세먼지 안녕!  (0) 2020.04.21

http://boj.kr/17142 

 

17142번: 연구소 3

인체에 치명적인 바이러스를 연구하던 연구소에 승원이가 침입했고, 바이러스를 유출하려고 한다. 바이러스는 활성 상태와 비활성 상태가 있다. 가장 처음에 모든 바이러스는 비활성 상태이고, 활성 상태인 바이러스는 상하좌우로 인접한 모든 빈 칸으로 동시에 복제되며, 1초가 걸린다. 승원이는 연구소의 바이러스 M개를 활성 상태로 변경하려고 한다. 연구소는 크기가 N×N인 정사각형으로 나타낼 수 있으며, 정사각형은 1×1 크기의 정사각형으로 나누어져 있다. 연구소는

www.acmicpc.net

 

 

문제가 이해가 안되서 애 먹었다.

모든 바이러스는 동서남북 방향으로 확산할 때마다 1초가 걸리는데,

비활성화된 바이러스는 활성화되도록 바꾸고/바이러스가 없는 빈칸(0)은 바이러스로 바꾼다.

문제에서 찾으라는 것은 모든 칸에 바이러스가 존재하게 될 때의 시간이다. (벽은 제외하고..)

바이러스의 활성화/비활성화 여부는 상관없고, 모든 칸에 0이 없어질 때의 시간을 찾는 것이다.

참고로 바이러스는 동서남북으로 확산할 때 무조건 1초가 걸린다. 비활성화 바이러스를 활성화시키는데는 0초, 빈칸을 바이러스로 확산시키는데는 1초가 걸린다고 생각하면 나처럼 고생한다.

 

밑의 두 예제를 보면 이해가 될 것이다.

5 1
2 2 2 1 1
2 1 1 1 1
2 1 1 1 1
2 1 1 1 1
2 2 2 1 1
# Answer: 0

2가 몇개고 1이 몇개고 어쩌구저쩌구하지만 주어지는 입력을 보면 빈 칸(0)이 없다. 그래서 답이 0초다.

 

4 2
0 1 1 0
2 1 1 2
2 1 1 2
0 1 1 0
# Answer: 2

line 3에 있는 두 개의 2를 선택했을 때의 변화는 다음과 같다.

1초: 위아래로 확산되면서 line 2의 빈 칸(0)은 바이러스로, line 4의 비활성화 바이러스(2)는 활성화 바이러스로 바꾼다.

2초: line 4의 활성화 바이러스가 된 2는 line 5의 빈 칸(0)을 바이러스로 바꾼다.

 

 

문제를 해결하기 위해서 먼저 입력을 board[N][N]에 받은 후에,

virus 리스트에는 바이러스들의 위치를 저장했고 empty_origin에는 빈 칸의 갯수를 저장했다.

virus 리스트에 저장된 것들은 모두 비활성화된 바이러스들이다.

이들 중 DFS로 총 M개를 뽑아 이를 활성화 바이러스로 선택했다.

 

그리고 뽑힌 바이러스들을 큐에 넣고 BFS 탐색을 시작한다.

BFS 탐색을 하는 이유는 모든 바이러스에서 동시에 네 방향으로 확산이 일어나기 때문이다.

depth를 시간이라고 생각하고 큐에 넣고 탐색을 돌리되, 반복문을 반복하는 조건은

큐에 바이러스가 있을 때거나 빈 칸이 아직 남아있을 때이다. 둘 중에 하나만 하면 안되고 둘다 만족해야 하는 이유는 

while empty > 0: 만 하면, 빈 칸은 남아있지만 벽으로 인해서 더이상 바이러스가 이동을 하지 못할 때 무한루프를 돌며, 큐에 뭐가 없는데 자꾸 pop을 시도하게 된다.

while queue: 만 하게 되면, 이미 빈 칸은 0개가 되었는데 비활성화된 바이러스만 남아있을 때, 비활성화된 바이러스를 활성화시키는 시간까지 카운트하게 된다.

 

그리고 어차피 큐에 시간(depth)을 넣어서 돌리는데 반복문이 종료되면 이 depth를 리턴하면 되지 굳이 time을 따로 변수로 빼서 저장해야되냐!라고 생각할수도있는데, 생각해보면 마지막 순간의 depth가 큐에 저장되는데 이를 pop하지 않으므로 따로 저장해줘야 한다.

 

# 연구소 3
import sys
from collections import deque


def bfs(selected):
    queue = deque(selected)
    #lab = deepcopy(board)
    visited = [[0 for _ in range(N)] for _ in range(N)]
    empty = empty_origin  # 남은 빈 칸의 갯수
    time = 0  # 빈 칸의 갯수를 0으로 만드는 데까지 걸린 시간

    for i, j, _ in selected:
        visited[i][j] = 1  # 초기 바이러스의 방문 표시

    while queue and empty > 0:  # 확산 중인 바이러스가 있고 & 빈 칸이 남아있으면
        r, c, depth = queue.popleft()
        for d in range(4):
            next_r, next_c = r + dx[d], c + dy[d]
            if 0 <= next_r < N and 0 <= next_c < N and not visited[next_r][next_c]:
                if board[next_r][next_c] != 1:  # 벽이 아닌데 방문하지 않음 -> 방문해야 함
                    visited[next_r][next_c] = 1
                    queue.append([next_r, next_c, depth + 1])
                    time = depth + 1

                    if board[next_r][next_c] == 0:  # 빈 칸이 바이러스가 될 때마다 empty를 줄임
                        empty -= 1

    if empty > 0:  # 확산이 끝났는데 아직도 0이 남아 있으면 불가능한 것임
        return float('inf')

    return time


def dfs(idx, cnt, selected):
    global min_value
    if cnt == M:
        min_value = min(min_value, bfs(selected))
        return
    for i in range(idx, n_virus):
        dfs(i+1, cnt+1, selected+[virus[i]])  # selected에는 뽑힌 virus의 위치가 추가됨


min_value = float('inf')  # 문제의 답. 최소 시간
input = sys.stdin.readline
N, M = map(int, input().split())
board = [list(map(int, input().split())) for _ in range(N)]
virus = []  # 바이러스를 놓을 수 있는 위치를 저장
n_virus = 0  # 바이러스를 놓을 수 있는 위치의 갯수
empty_origin = 0  # 빈 칸의 갯수
dx = (0, 0, -1, 1)
dy = (-1, 1, 0, 0)

for i in range(N):
    for j in range(N):
        if board[i][j] == 2:  # 바이러스를 놓을 수 있는 위치를 저장
            virus.append([i, j, 0])  # row, col, depth
            n_virus += 1
        elif board[i][j] == 0:
            empty_origin += 1

dfs(0, 0, [])

if min_value >= float('inf'):
    print(-1)
else:
    print(min_value)

 

'알고리즘 > BFS와 DFS' 카테고리의 다른 글

11725번. 트리의 부모 찾기 (Python)  (0) 2020.05.23
programmers. 네트워크 (python)  (0) 2020.05.18
15971번. 두 로봇  (0) 2020.04.21
14501번. 퇴사  (0) 2020.04.17
16236번. 아기 상어  (0) 2020.04.13

http://boj.kr/17144

 

17144번: 미세먼지 안녕!

미세먼지를 제거하기 위해 구사과는 공기청정기를 설치하려고 한다. 공기청정기의 성능을 테스트하기 위해 구사과는 집을 크기가 R×C인 격자판으로 나타냈고, 1×1 크기의 칸으로 나눴다. 구사과는 뛰어난 코딩 실력을 이용해 각 칸 (r, c)에 있는 미세먼지의 양을 실시간으로 모니터링하는 시스템을 개발했다. (r, c)는 r행 c열을 의미한다. 공기청정기는 항상 1번 열에 설치되어 있고, 크기는 두 행을 차지한다. 공기청정기가 설치되어 있지 않은 칸에는 미세먼

www.acmicpc.net

 

 

 

모든 미세먼지가 '동시에' 확산한다고 해서 BFS인가 했더니만 그냥 배열 크기만큼 돌면서 시뮬레이션해주는 문제였다.

RxC을 돌며 4방향 확산->공기청정기의 순환->RxC을 돌며 4방향 확산->공기청정기의 순환

이 과정을 T만큼 반복해야 한다.

 

주의해야 할 점은 모든 미세먼지는 동시에 확산된다는 점이다.

즉, 한 번의 로테이션 동안에는 서로 영향을 줘서는 안된다.

주변으로 확산될 미세먼지를 주변에 바로 더하면 안되고 따로 저장해둔 뒤에, 한꺼번에 확산시켜야 된다는 것이다.

(r, c)에서 미세먼지가 확산되버리면 다음 반복문인 (r, c+1)의 미세먼지 양을 계산할 때 영향을 주기 때문이다. 

나는 room[R][C] = [현재 미세먼지, 다음번에 주변으로 확산될 미세먼지] 로 저장했다. 

일단 한번 RxC를 돌면서 현재 미세먼지를 전부 계산한 후에, 다시한번 처음부터 RxC를 돌면서 저장된 미세먼지를 주변으로 확산시켰다.

 

공기청정기로 인한 미세먼지의 이동은 BFS 때처럼 배열을 만들어놓고 range로 총 4번 방향을 꺾게 하는 방법,

아니면 직접 총 8번의 for문으로 구역을 나누어서 이동시키는 방법이 있다.

나는 전자의 방법으로 처음에 시도했는데 하다보니 헷갈려서 시간을 너무 오래 썼다.

보다 간단하고 디버깅도 구역별로 나눠서 가능한 후자가 더 나을 것 같다.

(코드 내에서 전자의 방법은 air_fresh_upside & air_fresh_downside 함수이고

후자의 방법은 73-83번 줄의 for문이다. 윗부분만 for문으로 했기 때문에 4줄이다.)

 

전자이든 후자이든, 중요한 것은 공기청정기에 들어온 미세먼지는 사라진다는 것인데,

이것은 바꿔말하면 공기청정기 다음 위치(문제에서 보면 col + 1 한 곳)에 0이 들어온다는 뜻이 된다.

하지만 헷갈려서 공기청정기 이전 위치를 0으로 바꾼다던가 공기청정기를 다시 -1로 돌려놓지 않는다던가 하는 일이 발생하게 된다...

항상 초록색 네모가 0이 된다고 생각하면 된다.

 

import sys


def Print(lst):
    for i in lst:
        for j in i:
            print(j[0], end=' ')
        print("")
    print("")


def air_fresh_upside(r, c):
    dx = [-1, 0, 1, 0]
    dy = [0, 1, 0, -1]

    for d in range(4):
        while True:
            if 0 <= r + dx[d] <= head_loc[0] and 0 <= c + dy[d] < C:
                if room[r+dx[d]][c+dy[d]][0] == -1:
                    room[r][c][0] = 0
                    break
                else:
                    room[r][c][0] = room[r+dx[d]][c+dy[d]][0]
                r += dx[d]
                c += dy[d]
            else:
                break
    room[head_loc[0]][head_loc[1]][0] = -1


def air_fresh_downside(r, c):
    dx = [1, 0, -1, 0]
    dy = [0, 1, 0, -1]

    for d in range(4):
        while True:
            if tail_loc[0] <= r + dx[d] < R and 0 <= c + dy[d] < C:
                if room[r+dx[d]][c+dy[d]][0] == -1:
                    room[r][c][0] = 0
                    break
                else:
                    room[r][c][0] = room[r + dx[d]][c + dy[d]][0]
                r += dx[d]
                c += dy[d]
            else:
                break
    room[tail_loc[0]][tail_loc[1]][0] = -1


def solve():
    for _ in range(T):
        for r in range(R):
            for c in range(C):
                if room[r][c][0] >= 5:  # 미세먼지가 5 이상이면 확산함
                    dust_num = 0
                    for dr, dc in [[0, 1], [0, -1], [1, 0], [-1, 0]]:  # 상하좌우
                        if 0 <= r + dr < R and 0 <= c + dc < C:  # room 범위 내에서 &
                            if room[r+dr][c+dc][0] != -1:  # 공기청정기 영역이 아니라면 확산
                                dust_num += 1
                                room[r+dr][c+dc][1] += room[r][c][0] // 5
                    room[r][c][0] = room[r][c][0] - (room[r][c][0]//5) * dust_num

        # 확산된 미세먼지(room[r][c][1])를 기존 미세먼지에(room[r][c][0]) 합친다
        for r in range(R):
            for c in range(C):
                room[r][c][0] += room[r][c][1]
                room[r][c][1] = 0

        # 미세먼지의 이동 (방법 1)
        #air_fresh_upside(head_loc[0]-1, head_loc[1])
        air_fresh_downside(tail_loc[0]+1, tail_loc[1])

        # 미세먼지 이동 (방법 2)
        for i in range(0, C-1):
            room[0][i][0] = room[0][i+1][0]
        for i in range(0, head_loc[0]+1):
            room[i][C-1][0] = room[i+1][C-1][0]
        for i in range(0, C-1):
            room[head_loc[0]][C-1-i][0] = room[head_loc[0]][C-2-i][0]
        for i in range(0, head_loc[0]):
            room[head_loc[0]-i][0][0] = room[head_loc[0]-1-i][0][0]
        room[head_loc[0]][head_loc[1]+1][0] = 0
        room[head_loc[0]][head_loc[1]][0] = -1

        #Print(room)

    # sum
    ret = 0
    for r in range(R):
        for c in range(C):
            ret += room[r][c][0]
    return ret + 2


input = sys.stdin.readline
R, C, T = map(int, input().split())
room = [list(map(int, input().split())) for _ in range(R)]
head_loc, tail_loc = [], []  # 공기청정기 윗부분. 아랫부분 위치

for i in range(R):
    for j in range(C):
        if room[i][j] == -1:
            if head_loc:
                tail_loc = [i, j]
            else:
                head_loc = [i, j]
        room[i][j] = [room[i][j], 0]  # [현재 미세먼지, 다음에 확산되서 합쳐질 미세먼지]

print(solve())

'알고리즘 > 시뮬레이션' 카테고리의 다른 글

17140번. 이차원 배열과 연산  (0) 2020.04.27
SWEA 2382번. 미생물 격리  (0) 2020.04.24
16235번. 나무 재테크  (0) 2020.04.20
15685번. 드래곤 커브  (0) 2020.04.16
14891번. 톱니바퀴  (0) 2020.04.13

http://boj.kr/15971 

 

 

최단거리를 찾는 문제이다. 하지만 BFS가 아닌 DFS로 찾아야 한다. 그 이유는 경로는 단 하나이기 때문이다. BFS로 하면 시간초과가 난다.

경로를 DFS로 풀면서 헷갈린 점이 있는데, 이리저리 경로를 헤매는 BFS 같은 경우가 아니므로 visited를 다시 체크해제할 필요가 없다는 것이다.

한번 방문한 점은 다시 방문하지 않을 것이다. 왜냐면 어떤 한 점으로 가는 방법은 하나이기 때문이다. 다른 루트를 통해 그 점으로 갈 수 없다는 뜻이다.

 

그리고 경로는 단 하나이므로, 출발지에서 목적지까지 가는 경로를 구한 후, 하나의 weight를 뺄 수 있으므로 가장 큰 weight을 빼주면 된다. 두 로봇들이 가장 긴 통로를 사이에 두고 ㅇㅡㅇ 통신을 한다고 보면 된다. 그리고 그 를 가장 긴 통로로 선택하면 되고.

 

방법은 이렇지만 파이썬으로 풀기 까다로운 문제인데, 시간초과와 메모리초과가 나기 때문이다.

경로를 N * N의 배열에 저장하면 dfs로 경로 선택을 할 때 매번 N번만큼 반복하게 되므로 시간초과가 난다.

그리고 dfs를 할 때 재귀가 아닌 스택으로 풀면 시간 초과가 난다. (N <= 5000 일때만 통과)

또 재귀를 할 때도 반드시 sys.setrecursionlimit(500000000) 를 넣어줘야 한다.

 

마지막으로 문제에서 점은 1부터 시작한다고 했으므로 점을 입력받은 후 배열 인덱스로 쓰기 전에 -1을 하던가, 배열을 하나 더 크게 만들던가 해야된다는 점을 잊지 말자.

import sys


def dfs(now, max_weight, total):
    if now == end-1:
        print(total - max_weight)
        exit(0)
    for next, d in dist[now]:
        if not visited[next]:
            visited[next] = 1
            dfs(next, max(max_weight, d), total+d)


def dfs_stack():
    stack = [[start-1, 0, 0]]
    while stack:
        now, max_weight, total = stack.pop()
        if now == end - 1:
            print(total - max_weight)
            break
        for next, d in dist[now]:
            if not visited[next]:
                visited[next] = 1
                stack.append([next, max(max_weight, d), total+d])

sys.setrecursionlimit(500000000)

input = sys.stdin.readline
N, start, end = map(int, input().split())
dist = [[] for _ in range(N)]
visited = [0] * N

for _ in range(N-1):
    i, j, d = map(int, input().split())  # 왼쪽방, 오른쪽방, 거리
    dist[i-1].append([j-1, d])  # dist[왼쪽점] = [오른쪽점, 거리]
    dist[j-1].append([i-1, d])  
    # dist[왼쪽점][오른쪽점] = 거리 <- 거리를 dist[N][N]으로 만들면 
    # dfs에서 다음 점을 선택할 때 for문이 무조건 N번 돌아야해서 시간초과
    # dist[왼쪽점][오른쪽점] = 0 인 곳은 빼고 탐색할 수 있도록 해야 한다.

visited[start-1] = 1
dfs(start-1, 0, 0)
#dfs_stack()

'알고리즘 > BFS와 DFS' 카테고리의 다른 글

programmers. 네트워크 (python)  (0) 2020.05.18
17142번. 연구소 3  (0) 2020.04.26
14501번. 퇴사  (0) 2020.04.17
16236번. 아기 상어  (0) 2020.04.13
2178번. 미로 탐색  (0) 2020.04.08

http://boj.kr/16235 

 

 

각 계절별로 아래와 같은 행위를 정의한 다음 함수로 만들고 k년마다 실행하면 된다.

 

봄: 각 칸에 있는 나무들이 양분을 섭취하거나 죽은 나무 리스트에 추가된다.

여름: 죽은 나무들이 그 나이의 절반만큼 양분으로 추가된다.

가을: 어떤 나무의 나이가 5의 배수라면, 인접한 8칸에 1의 나이를 가진 나무들이 추가된다.

겨울: 모든 나무들에게 각기 주어진 만큼의 양분을 추가한다.

 

각 계절별로 따로 함수를 만들어도 되지만 가을, 겨울은 서로 영향을 주지 않기 때문에 하나의 반복문 내에서 실행되도록 했다.

53번줄처럼 밭을 board[N][N]으로,

1x1 크기의 각 칸은 [양분, [그 칸에 있는 나무들의 나이], 죽은 나무 나이/2, 나무의 숫자]로 정해주었고 

각 칸의 인덱스를 0~3으로 접근하면 헷갈리기 때문에 MEAL, TREE, DEAD, NUM = 0, 1, 2, 3으로 define해주었다.

nutrition이 아니라 meal인 이유는 nutrition은 너무 길기 때문에...

 

위에서 설명한 것처럼 board 배열을 3차원으로 초기화하려면 주의해야할 것이 있다.

보통 2차원 배열을 초기화할 때 [0] * N for _ in range(N) 이라는 방법을 쓴다.

나는 여기서 [0]  ->  [양분, [그 칸에 있는 나무들의 나이], 죽은 나무 나이/2, 나무의 숫자] 라고 생각해서

단순히  [[[양분, [그 칸에 있는 나무들의 나이], 죽은 나무 나이/2, 나무의 숫자]] * N for _ in range(N)] for _ in range(N)]이라고 해주었었다. 문제가 되는 부분은 볼드처리 된 부분으로, 저것은 배열이기 때문에 * N 으로 만들면 안 된다! 저렇게 하면 하나의 배열 객체가 만들어진후에 N 개만큼 자가증식한다. 하나가 바뀌면 N 개의 형제들이 모두 값이 바뀌게 된다. shallow copy인 셈이다.

 

파이썬은 1.3초의 시간을 주긴 하지만, 기본적으로 0.3초의 시간 밖에 주지 않아서 시간을 최대한 줄이기 위해 deque를 썼다. 작은 나무 먼저 양분을 먹는다고 했으므로, 우선 입력을 받은 후에 나무 배열을 sort()로 정렬한다. 

그 이후에 나무들을 차례대로 보면서 양분을 먹이거나 죽이거나 하는 과정은 

28번줄에서 popleft로 맨 앞에 있는 가장 어린 나무를 꺼낸 후 

31번째줄에서 맨 뒤에 다시 추가시켜 주는 식으로 반복했다. 이것을 나무의 갯수(NUM)만큼 진행하면 된다.

매번 새로 정렬해야했다면 spring()에서만 시간이 2배로 걸렸을 것이다.

import sys
from collections import deque


def fall_and_winter():
    for i in range(N):
        for j in range(N):
            for t in board[i][j][TREE]:
                if t % 5 == 0:
                    for d in range(8):
                        if 0 <= i + dx[d] < N and 0 <= j + dy[d] < N:
                            board[i+dx[d]][j+dy[d]][TREE].appendleft(1)
                            board[i+dx[d]][j+dy[d]][NUM] += 1
            board[i][j][MEAL] += new_meal[i][j]


def summer():
    for i in range(N):
        for j in range(N):
            board[i][j][MEAL] += board[i][j][DEAD]
            board[i][j][DEAD] = 0


def spring():
    for i in range(N):
        for j in range(N):
            for _ in range(board[i][j][NUM]):
                tree_year = board[i][j][TREE].popleft()
                if tree_year <= board[i][j][MEAL]:  # 작거나 같으면 양분 섭취 가능
                    board[i][j][MEAL] -= tree_year  # 나이만큼 양분 빼기
                    board[i][j][TREE].append((tree_year+1))
                else:  # dead
                    board[i][j][DEAD] += tree_year // 2
                    board[i][j][NUM] -= 1


def solve():
    for k in range(K):
        spring()
        summer()
        fall_and_winter()

    ret = 0
    for i in range(N):
        for j in range(N):
            ret += board[i][j][NUM]
    return ret


MEAL, TREE, DEAD, NUM = 0, 1, 2, 3
input = sys.stdin.readline
N, M, K = map(int, input().split())
board = [[[5, [], 0, 0] for _ in range(N)] for _ in range(N)]  # meal, trees(ages), num, dead
new_meal = [list(map(int, input().split())) for _ in range(N)]

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

for _ in range(M):
    x, y, z = map(int, input().split())
    board[x-1][y-1][TREE].append(z)  # tree 추가

for i in range(N):
    for j in range(N):
        board[i][j][TREE] = deque(sorted(board[i][j][TREE]))
        board[i][j][NUM] = len(board[i][j][TREE])

print(solve())

 

'알고리즘 > 시뮬레이션' 카테고리의 다른 글

SWEA 2382번. 미생물 격리  (0) 2020.04.24
17144번. 미세먼지 안녕!  (0) 2020.04.21
15685번. 드래곤 커브  (0) 2020.04.16
14891번. 톱니바퀴  (0) 2020.04.13
14503번. 로봇 청소기  (0) 2020.04.13

+ Recent posts