문제 후기

1. 먼저 그래프로 구성되어 있다는 것에서 DFS와 BFS 둘 중 하나로 해결겠다는 생각을 했고 문제를 읽고 가장 먼 노드의 갯수를 세는 것으로 BFS로 풀 것을 정했다.

2. 단순히 방문한 노드를 체크하는 것으로 하니 시간초과가 발생했다.

3. 배열 상태에 있는 것을 dictionary를 통해 hash형태로 변환하여 모든 vertext를 확인하지 않고 해당 노드의 간선만 확인하여 진행했더니 문제가 해결됐다.

문제 설명

n개의 노드가 있는 그래프가 있습니다. 각 노드는 1부터 n까지 번호가 적혀있습니다. 1번 노드에서 가장 멀리 떨어진 노드의 갯수를 구하려고 합니다. 가장 멀리 떨어진 노드란 최단경로로 이동했을 때 간선의 개수가 가장 많은 노드들을 의미합니다.

노드의 개수 n, 간선에 대한 정보가 담긴 2차원 배열 vertex가 매개변수로 주어질 때, 1번 노드로부터 가장 멀리 떨어진 노드가 몇 개인지를 return 하도록 solution 함수를 작성해주세요.

제한사항

  • 노드의 개수 n은 2 이상 20,000 이하입니다.
  • 간선은 양방향이며 총 1개 이상 50,000개 이하의 간선이 있습니다.
  • vertex 배열 각 행 [a, b]는 a번 노드와 b번 노드 사이에 간선이 있다는 의미입니다.

입출력 예

 

n vectex return
6 [[3, 6], [4, 3], [3, 2], [1, 3], [1, 2], [2, 4], [5, 2]] 3

입출력 예 설명

예제의 그래프를 표현하면 아래 그림과 같고, 1번 노드에서 가장 멀리 떨어진 노드는 4,5,6번 노드입니다.


from _collections import deque

def solution(n, edge):
    answer = 1      # 정답값
    now_visit = deque([])   # 방문 노드 큐
    next_visit = []         # 다음 방문 노드
    visited = [0] * (n + 1) # 노드 방문 여부 확인
    visited[1] = 1

    # dictionary를 통해 hash로 빠르게 간선을 찾음.
    node_dic = {}
    for i in range(1,n+1):
        node_dic[i] = []

    for i in edge:
        node_dic[i[0]].append(i[1])
        node_dic[i[1]].append(i[0])

    # 1번 노드부터 시작
    for i in node_dic[1]:
        now_visit.append(i)
        visited[i] = 1
        node_dic[1] = []


    while True:
        # 방문할 노드가 있을때까지 반복
        while now_visit:
            node = now_visit.pop()

            # 해당 노드의 hash값을 통해 다음 방문 노드 확인
            for i in node_dic[node]:
                # 노드 방문 여부 확인
                if visited[i] == 1:
                    continue
                visited[i] = 1
                next_visit.append(i)

        # 다음 방문할 노드가 있으면 계속 진행
        if next_visit:
            now_visit = deque(next_visit)
            answer = len(next_visit)
            next_visit = []
        else:
            # 없는 경우 결과 리턴
            return answer

문제 후기

1. 처음에는 단순하게 진입 지점과 진출 시점을 기준으로 카메라를 설치하려했습니다. 해당 방법으로 했더니 정답 자체가 아니어서 알고리즘이 틀린 것으로 알게 됐다.

2. 2번째로는 나가는 시점을 기준으로 카메라를 설치했다. 메모이제이션을 통해 미리 최대 길이(60,000)만큼 배열을 만들고 나가는 지점에 카메라를 기록하는 방식으로 했다. 정확성에서는 통과했지만 효율성에서 케이스 한 개가 안됐다.

3. 3번째로 새로운 카메라가 추가된 경우 미리 나머지 차량을 확인하는 방식을 선택했다. 해당 알고리즘으로 했을 때 효율성 또한 만족하는 것으로 나왔다.

문제 설명

고속도로를 이동하는 모든 차량이 고속도로를 이용하면서 단속용 카메라를 한 번은 만나도록 카메라를 설치하려고 합니다.

고속도로를 이동하는 차량의 경로 routes가 매개변수로 주어질 때, 모든 차량이 한 번은 단속용 카메라를 만나도록 하려면 최소 몇 대의 카메라를 설치해야 하는지를 return 하도록 solution 함수를 완성하세요.

제한사항

  • 차량의 대수는 1대 이상 10,000대 이하입니다.
  • routes에는 차량의 이동 경로가 포함되어 있으며 routes[i][0]에는 i번째 차량이 고속도로에 진입한 지점, routes[i][1]에는 i번째 차량이 고속도로에서 나간 지점이 적혀 있습니다.
  • 차량의 진입/진출 지점에 카메라가 설치되어 있어도 카메라를 만난것으로 간주합니다.
  • 차량의 진입 지점, 진출 지점은 -30,000 이상 30,000 이하입니다.

입출력 예

routes return
[[-20,15], [-14,-5], [-18,-13], [-5,-3]] 2

입출력 예 설명

-5 지점에 카메라를 설치하면 두 번째, 네 번째 차량이 카메라를 만납니다.

-15 지점에 카메라를 설치하면 첫 번째, 세 번째 차량이 카메라를 만납니다.


def solution(routes):
    
    chk_camera = [0] * len(routes)      # 총 확인해야하는 차량 수
    count = 0   # 결과값
    routes.sort(key=lambda x:x[1])      # 진출 지점으로 정렬

    for car in range(len(routes)):
        # 만약 해당 차량이 카메라 테스트에서 통과한 경우 패스
        if chk_camera[car]:
            continue
        # 해당 차량이 지나가는 카메라가 없으므로 새로 만듬.
        camera = routes[car][1]
        count +=1
        
        # 새로 만든 카메라가 이후 차량 중에서 지나가는 차량이 있는지 확인
        for num in range(car + 1, len(routes)):
            if routes[num][0] <= camera <= routes[num][1]:
                chk_camera[num] = True

    return count

문제 후기

제한 사항에서 M과 N이 최대 20인 것을 확인하고 효율성검사가 없다는 것을 알았다. 

해당 문제는 2차원배열을 어떻게 해결해야하는지에 대해서 확인 하는 문제다. key값을 이동하고 회전하면서 lock과 비교하면서 답을 찾아간다. 이때 padding을 이용하면 좀 더 수월하게 할 수 있다.

회전을 할 때는 규칙을 활용하면 쉽게 회전할 수 있다. 규칙은

temp_arr[x][size - y - 1] = arr[y][x] 이다.

문제 설명

고고학자인 튜브는 고대 유적지에서 보물과 유적이 가득할 것으로 추정되는 비밀의 문을 발견하였습니다. 그런데 문을 열려고 살펴보니 특이한 형태의 자물쇠로 잠겨 있었고 문 앞에는 특이한 형태의 열쇠와 함께 자물쇠를 푸는 방법에 대해 다음과 같이 설명해 주는 종이가 발견되었습니다.

잠겨있는 자물쇠는 격자 한 칸의 크기가 1 x 1 N x N 크기의 정사각 격자 형태이고 특이한 모양의 열쇠는 M x M 크기인 정사각 격자 형태로 되어 있습니다.

자물쇠에는 홈이 파여 있고 열쇠 또한 홈과 돌기 부분이 있습니다. 열쇠는 회전과 이동이 가능하며 열쇠의 돌기 부분을 자물쇠의 홈 부분에 딱 맞게 채우면 자물쇠가 열리게 되는 구조입니다. 자물쇠 영역을 벗어난 부분에 있는 열쇠의 홈과 돌기는 자물쇠를 여는 데 영향을 주지 않지만, 자물쇠 영역 내에서는 열쇠의 돌기 부분과 자물쇠의 홈 부분이 정확히 일치해야 하며 열쇠의 돌기와 자물쇠의 돌기가 만나서는 안됩니다. 또한 자물쇠의 모든 홈을 채워 비어있는 곳이 없어야 자물쇠를 열 수 있습니다.

열쇠를 나타내는 2차원 배열 key와 자물쇠를 나타내는 2차원 배열 lock이 매개변수로 주어질 때, 열쇠로 자물쇠를 열수 있으면 true를, 열 수 없으면 false를 return 하도록 solution 함수를 완성해주세요.

제한사항

  • key는 M x M(3 ≤ M ≤ 20, M은 자연수)크기 2차원 배열입니다.
  • lock은 N x N(3 ≤ N ≤ 20, N은 자연수)크기 2차원 배열입니다.
  • M은 항상 N 이하입니다.
  • key와 lock의 원소는 0 또는 1로 이루어져 있습니다.
    • 0은 홈 부분, 1은 돌기 부분을 나타냅니다.

입출력 예

keylockresult

key lock result
[[0, 0, 0], [1, 0, 0], [0, 1, 1]] [[1, 1, 1], [1, 1, 0], [1, 0, 1]] true

입출력 예에 대한 설명

key를 시계 방향으로 90도 회전하고, 오른쪽으로 한 칸, 아래로 한 칸 이동하면 lock의 홈 부분을 정확히 모두 채울 수 있습니다.


def rotation(arr):
    size = len(arr)
    temp_arr = [[0] * len(arr) for _ in range(len(arr))]
    for y in range(size):
        for x in range(size):
            temp_arr[x][size - y - 1] = arr[y][x]
    return temp_arr

def padding(size, my, mx, key):
    temp_arr = [[0] * size for _ in range(size)]
    for y in range(len(key)):
        for x in range(len(key)):
            temp_arr[y + my][x + mx] = key[y][x]
    for i in temp_arr:
        print(i)
    print("!"*10)
    return temp_arr

def chk_key(key, lock, a ,b):

    for y in range(a, a + b):
        for x in range(a, a + b):
            # if lock[y][x] == 2:
            #     continue
            if lock[y][x] + key[y][x] == 2 or lock[y][x] + key[y][x] == 0:
                return False
    return True

def solution(key, lock):
    key_size = len(key)
    lock_size = len(lock)
    key_matrix = [[2] * (lock_size + 2 * key_size - 2) for _ in range(lock_size + 2 * key_size - 2)]


    for y in range(lock_size):
        for x in range(lock_size):
            key_matrix[y + key_size - 1][x + key_size - 1] = lock[y][x]

    for _ in key_matrix:
        print(_)

    for _ in range(4):
        for my in range(len(key_matrix) - key_size+1):
            for mx in range(len(key_matrix) - key_size+1):

                temp_key = padding(len(key_matrix), my, mx, key)
                answer = chk_key(temp_key,key_matrix, key_size - 1, lock_size)

                if answer:
                    return answer

        key = rotation(key)
    return False

문제 후기

비슷한 문제를 이전 카카오 코테에서도 효율성으로 인해서 풀지 못한 기억이 있었다. 이번 문제 역시 제한사항으로 백만이 주어지는 것을 보고 단순 완전탐색으로 할 경우 효율성에서 오류가 날 것으로 봤다.

그래도 테스트 해보고 싶어 완전탐색으로 1차적으로 코딩을 했고, 예상했던 것처럼 효율성에서 실패했다.

2차로 했을때는 heap을 사용해서 풀었더니 해결됐다.

 

해결 방식은 테스트케이스가 어떻게 나오는지 직접해보면 규칙이 보인다. minimum 값이 어디있는지 확인해보면서 하면 규칙을 찾기 쉽다.

문제 설명

일렬로 나열된 n개의 풍선이 있습니다. 모든 풍선에는 서로 다른 숫자가 써져 있습니다. 당신은 다음 과정을 반복하면서 풍선들을 단 1개만 남을 때까지 계속 터트리려고 합니다.

  1. 임의의 인접한 두 풍선을 고른 뒤, 두 풍선 중 하나를 터트립니다.
  2. 터진 풍선으로 인해 풍선들 사이에 빈 공간이 생겼다면, 빈 공간이 없도록 풍선들을 중앙으로 밀착시킵니다.

여기서 조건이 있습니다. 인접한 두 풍선 중에서 번호가 더 작은 풍선을 터트리는 행위는 최대 1번만 할 수 있습니다. 즉, 어떤 시점에서 인접한 두 풍선 중 번호가 더 작은 풍선을 터트렸다면, 그 이후에는 인접한 두 풍선을 고른 뒤 번호가 더 큰 풍선만을 터트릴 수 있습니다.

당신은 어떤 풍선이 최후까지 남을 수 있는지 알아보고 싶습니다. 위에 서술된 조건대로 풍선을 터트리다 보면, 어떤 풍선은 최후까지 남을 수도 있지만, 어떤 풍선은 무슨 수를 쓰더라도 마지막까지 남기는 것이 불가능할 수도 있습니다.

일렬로 나열된 풍선들의 번호가 담긴 배열 a가 주어집니다. 위에 서술된 규칙대로 풍선들을 1개만 남을 때까지 터트렸을 때 최후까지 남기는 것이 가능한 풍선들의 개수를 return 하도록 solution 함수를 완성해주세요.


제한 사항

  • a의 길이는 1 이상 1,000,000 이하입니다.
    • a[i]는 i+1 번째 풍선에 써진 숫자를 의미합니다.
    • a의 모든 수는 -1,000,000,000 이상 1,000,000,000 이하인 정수입니다.
    • a의 모든 수는 서로 다릅니다.

입출력 예

a result
[9,-1,-5] 3
[-16,27,65,-2,58,-92,-71,-68,-61,-33] 6

입출력 예 설명

입출력 예 #1

  • 첫 번째 풍선(9가 써진 풍선)을 최후까지 남기는 방법은 다음과 같습니다.
    1. [9, -1, -5] 에서 -1, -5가 써진 풍선을 고른 뒤, -1이 써진 풍선(번호가 더 큰 것)을 터트립니다.
    2. [9, -5] 에서 9, -5가 써진 풍선을 고른 뒤, -5가 써진 풍선(번호가 더 작은 것)을 터트립니다.
  • 두 번째 풍선(-1이 써진 풍선)을 최후까지 남기는 방법은 다음과 같습니다.
    1. [9, -1, -5] 에서 9, -1이 써진 풍선을 고른 뒤, 9가 써진 풍선(번호가 더 큰 것)을 터트립니다.
    2. [-1, -5] 에서 -1, -5가 써진 풍선을 고른 뒤, -5가 써진 풍선(번호가 더 작은 것)을 터트립니다.
  • 세 번째 풍선(-5가 써진 풍선)을 최후까지 남기는 방법은 다음과 같습니다.
    1. [9, -1, -5] 에서 9, -1이 써진 풍선을 고른 뒤, 9가 써진 풍선(번호가 더 큰 것)을 터트립니다.
    2. [-1, -5] 에서 -1, -5가 써진 풍선을 고른 뒤, -1이 써진 풍선(번호가 더 큰 것)을 터트립니다.
  • 3개의 풍선이 최후까지 남을 수 있으므로, 3을 return 해야 합니다.

입출력 예 #2

  • 최후까지 남을 수 있는 풍선은 -16, -92, -71, -68, -61, -33이 써진 풍선으로 모두 6개입니다.

# 단순 계산 알고리즘(완전탐색)
def solution2(a):
    answer = 0

    minimum = min(a)
    min_idx = a.index(minimum)
    answer += 1
    min_lidx, min_ridx = 0, len(a) - 1

    if min_idx == 0:
        min_ridx = a.index(min(a[min_idx+1:]))
        answer += 1
    elif min_idx == len(a) - 1:
        min_lidx = a.index(min(a[:min_idx]))
        answer += 1
    else:
        min_ridx = a.index(min(a[min_idx + 1:]))
        min_lidx = a.index(min(a[:min_idx]))
        answer += 2

    while min_lidx > 0:
        min_lidx = a.index(min(a[:min_lidx]))
        answer += 1

    while min_ridx < len(a) - 1:
        min_ridx = a.index(min(a[min_ridx+1:]))
        answer += 1

    return answer
import heapq

# 힙을 활용한 탐색
def solution(a):
    
    answer = 0      # 결과값
    min_idx = a.index(min(a))   # 가장 작은 숫자의 인덱스
    answer += 1
    heap_left, heap_right = [], []  # min_idx를 기준으로 왼쪽, 오른쪽
    
    # 만약 min_idx의 왼쪽이 없을 경우
    if min_idx == 0:
        for i in range(min_idx+1, len(a)):
            heap_right.append((a[i],i))
    # 만약 min_idx의 오른쪽이 없을 경우
    elif min_idx == len(a) - 1:
        for j in range(min_idx):
            heap_left.append((a[j],j))
    # min_idx의 왼쪽, 오른쪽
    else:
        for i in range(min_idx+1, len(a)):
            heap_right.append((a[i],i))
        for j in range(min_idx):
            heap_left.append((a[j],j))

    # 왼쪽, 오른쪽 heap정렬
    heapq.heapify(heap_left)
    heapq.heapify(heap_right)

    temp  = 1000000     # 확인용 변수
    while heap_left:
        chk_lmin = heapq.heappop(heap_left)
        chk_idx = chk_lmin[1]
        # 가장 작은 값 인덱스가 0인 경우
        if chk_idx == 0:
            answer += 1
            break
        # heap에서 가장 작은 값은 이전 값보다 왼쪽에 있어야함.
        elif chk_idx < temp:
            answer += 1
            temp = chk_idx

    temp2 = 0   # 확인용 변수
    while heap_right:
        chk_rmin = heapq.heappop(heap_right)
        chk_idx = chk_rmin[1]
        # 가장 작은 값 인덱스가 마지막인 경우
        if chk_idx == len(a) - 1:
            answer += 1
            break
        # heap에서 가장 작은 값은 이전 값보다 오른쪽에 있어야 함.
        elif chk_idx > temp2:
            temp2 = chk_idx
            answer += 1
    return answer

문제 후기

단순히 숫자의 조합으로 해결할 수 있어서 처음에는 같은 수가 있는 순열 식을 활용하려 했다.

문제를 예를 들면 [2,1(a),1(b)] [2,1(b),1(a)] 와 같은 것이다.

계산 하는 방식은 총 갯수 n, 중복하는 숫자의 갯수 m이면 n!/ m! 이다. 따라서 1,2의 갯수만큼 나누는 것으로 했다. 하지만 Factorial의 특성으로 인해서 특정 숫자를 넘어가면 시간초과가 발생했다.

다시 처음부터 경우의 수를 확인해본 결과 1,2,3,5,8,13... 순으로 피보나치수열인 것을 확인할 수 있었다.

문제 설명

가로 길이가 2이고 세로의 길이가 1인 직사각형모양의 타일이 있습니다. 이 직사각형 타일을 이용하여 세로의 길이가 2이고 가로의 길이가 n인 바닥을 가득 채우려고 합니다. 타일을 채울 때는 다음과 같이 2가지 방법이 있습니다.

  • 타일을 가로로 배치 하는 경우
  • 타일을 세로로 배치 하는 경우

예를들어서 n이 7인 직사각형은 다음과 같이 채울 수 있습니다.

직사각형의 가로의 길이 n이 매개변수로 주어질 때, 이 직사각형을 채우는 방법의 수를 return 하는 solution 함수를 완성해주세요.

제한사항

  • 가로의 길이 n은 60,000이하의 자연수 입니다.
  • 경우의 수가 많아 질 수 있으므로, 경우의 수를 1,000,000,007으로 나눈 나머지를 return해주세요.

입출력 예

nresult

n result
4 5

입출력 예 설명

입출력 예 #1
다음과 같이 5가지 방법이 있다.


# 같은 수가 있을 때 순열 공식을 활용.
def solution(n):
    answer = 1
    count_one = n - 2
    count_two = 1

    while count_one > 1:

        answer += math.factorial(count_one+count_two) // (math.factorial(count_one) * math.factorial(count_two))
        count_one -= 2
        count_two += 1

        # print(answer)
        # print(count_one, count_two)
    if count_one == 1:
        answer += math.factorial(count_one + count_two) // (math.factorial(count_one) * math.factorial(count_two))
    else:
        answer += 1
    return answer % 1000000007
# 피보나치 수열로 계산
def solution(n):
	answer = 1
    start1 = 1
    start2 = 2

    num = 2
    while num < n:
        answer = start1 + start2
        start1 = start2
        start2 = answer
        num += 1
    return answer % 1000000007

문제 후기

2018 카카오 블라인드채용에서 나온 문제입니다. 문제를 풀면서 확인하는 능력은 문자열을 계산하는 능력으로 music info 부분을 잘 처리해야한다고 생각하고 문제를 풀었습니다. 자세한 문제 해설은 사이트(tech.kakao.com/2017/11/14/kakao-blind-recruitment-round-3/)에서 확인해보세요

문제 설명

방금그곡

라디오를 자주 듣는 네오는 라디오에서 방금 나왔던 음악이 무슨 음악인지 궁금해질 때가 많다. 그럴 때 네오는 다음 포털의 '방금그곡' 서비스를 이용하곤 한다. 방금그곡에서는 TV, 라디오 등에서 나온 음악에 관해 제목 등의 정보를 제공하는 서비스이다.

네오는 자신이 기억한 멜로디를 가지고 방금그곡을 이용해 음악을 찾는다. 그런데 라디오 방송에서는 한 음악을 반복해서 재생할 때도 있어서 네오가 기억하고 있는 멜로디는 음악 끝부분과 처음 부분이 이어서 재생된 멜로디일 수도 있다. 반대로, 한 음악을 중간에 끊을 경우 원본 음악에는 네오가 기억한 멜로디가 들어있다 해도 그 곡이 네오가 들은 곡이 아닐 수도 있다. 그렇기 때문에 네오는 기억한 멜로디를 재생 시간과 제공된 악보를 직접 보면서 비교하려고 한다. 다음과 같은 가정을 할 때 네오가 찾으려는 음악의 제목을 구하여라.

  • 방금그곡 서비스에서는 음악 제목, 재생이 시작되고 끝난 시각, 악보를 제공한다.
  • 네오가 기억한 멜로디와 악보에 사용되는 음은 C, C#, D, D#, E, F, F#, G, G#, A, A#, B 12개이다.
  • 각 음은 1분에 1개씩 재생된다. 음악은 반드시 처음부터 재생되며 음악 길이보다 재생된 시간이 길 때는 음악이 끊김 없이 처음부터 반복해서 재생된다. 음악 길이보다 재생된 시간이 짧을 때는 처음부터 재생 시간만큼만 재생된다.
  • 음악이 00:00를 넘겨서까지 재생되는 일은 없다.
  • 조건이 일치하는 음악이 여러 개일 때에는 라디오에서 재생된 시간이 제일 긴 음악 제목을 반환한다. 재생된 시간도 같을 경우 먼저 입력된 음악 제목을 반환한다.
  • 조건이 일치하는 음악이 없을 때에는 `(None)`을 반환한다.

입력 형식

입력으로 네오가 기억한 멜로디를 담은 문자열 m과 방송된 곡의 정보를 담고 있는 배열 musicinfos가 주어진다.

  • m은 음 1개 이상 1439개 이하로 구성되어 있다.
  • musicinfos는 100개 이하의 곡 정보를 담고 있는 배열로, 각각의 곡 정보는 음악이 시작한 시각, 끝난 시각, 음악 제목, 악보 정보가 ','로 구분된 문자열이다.
    • 음악의 시작 시각과 끝난 시각은 24시간 HH:MM 형식이다.
    • 음악 제목은 ',' 이외의 출력 가능한 문자로 표현된 길이 1 이상 64 이하의 문자열이다.
    • 악보 정보는 음 1개 이상 1439개 이하로 구성되어 있다.

출력 형식

조건과 일치하는 음악 제목을 출력한다.

입출력 예시

m musicinfo answer
ABCDEFG [12:00,12:14,HELLO,CDEFGAB, 13:00,13:05,WORLD,ABCDEF] HELLO
CC#BCC#BCC#BCC#B [03:00,03:30,FOO,CC#B, 04:00,04:08,BAR,CC#BCC#BCC#B] FOO
ABC [12:00,12:14,HELLO,C#DEFGAB, 13:00,13:05,WORLD,ABCDEF] WORLD

설명

첫 번째 예시에서 HELLO는 길이가 7분이지만 12:00부터 12:14까지 재생되었으므로 실제로 CDEFGABCDEFGAB로 재생되었고, 이 중에 기억한 멜로디인 ABCDEFG가 들어있다.
세 번째 예시에서 HELLO는 C#DEFGABC#DEFGAB로, WORLD는 ABCDE로 재생되었다. HELLO 안에 있는 ABC#은 기억한 멜로디인 ABC와 일치하지 않고, WORLD 안에 있는 ABC가 기억한 멜로디와 일치한다.


def solution(m, musicinfos):
    answer = ["(None)",-1]
    m = list(str(m))
    word = 0

    # "#"합치기
    while word < len(m):
        if m[word] == "#":
            m[word - 1] += "#"
            del m[word]
        else:
            word += 1

    # 시간 계산
    for music in musicinfos:
        arr = music.split(',')  # 시작, 종료, 노래, 계이름
        start, end = arr[0].split(':'), arr[1].split(':')   # 시간 확인
        start_hour, start_min = int(start[0]), int(start[1])    # 시작 시작
        end_hour, end_min = int(end[0]), int(end[1])        # 종료 시간

        hour, min =  end_hour - start_hour, end_min - start_min # 플레이 시간

        time = hour * 60 + min + 1  # 시간 -> 분

        name = arr[2]   # 노래 제목
        scale = list(str(arr[3]))   # 음계
        chk = 0
        # "#" 합치기
        while chk < len(scale):
            if scale[chk] == "#":
                scale[chk - 1] += "#"
                del scale[chk]
            else:
                chk += 1

        idx = 0
        res = []
        # 시간 만큼 계이름 입력
        while idx < time:
            res.append(scale[idx % len(scale)])
            idx += 1


        loc = 0     # 체크 시작 위치
        # 시작위치 처음부터 끝까지 확인
        while loc < len(res) - len(m):
            correct = True  # 일치여부 확인
            for c in range(len(m)):
                # m 과 계이름 확인
                if res[loc+c] != m[c]:
                    correct = False
                    break

            # 일치하면 재생 시간 계산.
            if correct:
                if answer[1] < time:
                    answer = [name, time]
                    break
            loc += 1
    
    return answer[0]    # 이름 출력

문제 후기

이전에는 중복을 없애는 것은 단순하게 dictionary를 사용해서 hash만 생각했었다. 해당 문제를 풀면서 집합의 개념을 다시 한번 확인했고 부분집합(issubset)의 활용하는 것을 알게 됐습니다.

문제 설명

후보키

프렌즈대학교 컴퓨터공학과 조교인 제이지는 네오 학과장님의 지시로, 학생들의 인적사항을 정리하는 업무를 담당하게 되었다.

그의 학부 시절 프로그래밍 경험을 되살려, 모든 인적사항을 데이터베이스에 넣기로 하였고, 이를 위해 정리를 하던 중에 후보키(Candidate Key)에 대한 고민이 필요하게 되었다.

후보키에 대한 내용이 잘 기억나지 않던 제이지는, 정확한 내용을 파악하기 위해 데이터베이스 관련 서적을 확인하여 아래와 같은 내용을 확인하였다.

  • 관계 데이터베이스에서 릴레이션(Relation)의 튜플(Tuple)을 유일하게 식별할 수 있는 속성(Attribute) 또는 속성의 집합 중, 다음 두 성질을 만족하는 것을 후보 키(Candidate Key)라고 한다.
    • 유일성(uniqueness) : 릴레이션에 있는 모든 튜플에 대해 유일하게 식별되어야 한다.
    • 최소성(minimality) : 유일성을 가진 키를 구성하는 속성(Attribute) 중 하나라도 제외하는 경우 유일성이 깨지는 것을 의미한다. 즉, 릴레이션의 모든 튜플을 유일하게 식별하는 데 꼭 필요한 속성들로만 구성되어야 한다.

제이지를 위해, 아래와 같은 학생들의 인적사항이 주어졌을 때, 후보 키의 최대 개수를 구하라.

위의 예를 설명하면, 학생의 인적사항 릴레이션에서 모든 학생은 각자 유일한 학번을 가지고 있다. 따라서 학번은 릴레이션의 후보 키가 될 수 있다.
그다음 이름에 대해서는 같은 이름(apeach)을 사용하는 학생이 있기 때문에, 이름은 후보 키가 될 수 없다. 그러나, 만약 [이름, 전공]을 함께 사용한다면 릴레이션의 모든 튜플을 유일하게 식별 가능하므로 후보 키가 될 수 있게 된다.
물론 [이름, 전공, 학년]을 함께 사용해도 릴레이션의 모든 튜플을 유일하게 식별할 수 있지만, 최소성을 만족하지 못하기 때문에 후보 키가 될 수 없다.
따라서, 위의 학생 인적사항의 후보키는 학번, [이름, 전공] 두 개가 된다.

릴레이션을 나타내는 문자열 배열 relation이 매개변수로 주어질 때, 이 릴레이션에서 후보 키의 개수를 return 하도록 solution 함수를 완성하라.

제한사항

  • relation은 2차원 문자열 배열이다.
  • relation의 컬럼(column)의 길이는 1 이상 8 이하이며, 각각의 컬럼은 릴레이션의 속성을 나타낸다.
  • relation의 로우(row)의 길이는 1 이상 20 이하이며, 각각의 로우는 릴레이션의 튜플을 나타낸다.
  • relation의 모든 문자열의 길이는 1 이상 8 이하이며, 알파벳 소문자와 숫자로만 이루어져 있다.
  • relation의 모든 튜플은 유일하게 식별 가능하다.(즉, 중복되는 튜플은 없다.)

입출력 예

Relation Result
[["100","ryan","music","2"],["200","apeach","math","2"],["300","tube","computer","3"],["400","con","computer","4"],["500","muzi","music","3"],["600","apeach","music","2"]] 2

입출력 예 설명

입출력 예 #1
문제에 주어진 릴레이션과 같으며, 후보 키는 2개이다.


# 순열 구현
def perm(lst, n):
    arr = []
    if n > len(lst):
        return arr

    if n == 1:
        for i in lst:
            arr.append([i])
    else:
        for i in range(len(lst)):
            temp = [val for val in lst]
            temp.remove(lst[i])
            for p in perm(temp, n-1):
                arr.append([lst[i]]+p)
    return arr
# 조합 구현
def comb(lst, n):
    arr = []

    if n > len(lst):
        return arr

    if n == 1:
        for i in lst:
            arr.append([i])

    else:
        for i in range(len(lst)-n+1):
            for temp in comb(lst[i+1:], n-1):
                arr.append([lst[i]] + temp)
    return arr


def solution(relation):
    # 행, 열 정의
    row, col = len(relation), len(relation[0])

    col_arr = [num for num in range(col)]

    candidate_key = []  # 후보키 가능 리스트, 유일성 만족
    key_list = []       # 키 리스트
    result_key = []     # 결과값

    # 조합으로 가능한 키 리스트 추출.
    for num in range(1, col+1):
        key_list.extend(comb(col_arr, num))

    # 키 리스트로 후보키 유일성 체크
    for key_col in key_list:
        check_key = []
        
        # 키가 후보키인지 확인
        for rel in range(row):
            data = ""
            for num in key_col:
                data += relation[rel][num]
            if data not in check_key:
                check_key.append(data)
            else:
                break
                
        # 키가 유일성을 만족.
        if len(check_key) == row:
            # 집합 형태로 추가.
            candidate_key.append(set(key_col))

    # 최소성확인
    for key in candidate_key:
        tmp = True
        if not result_key:
            result_key.append(key)
        else:
            for chk in result_key:
                # 부분집합으로 존재하면 최소성 만족 x
                if chk.issubset(key):
                    tmp = False
                    break
            if tmp:
                result_key.append(key)
    # 최종 후보키 출력
    return len(result_key)

문제 후기

해당 문제를 보고 닉네임과 uid의 관계를 정립하는 것이 중요하다 생각하여 dictionary를 활용했습니다. 이후는 record의 순서대로 입장과 퇴장을 출력하면 됩니다.

문제 설명

오픈채팅방

카카오톡 오픈채팅방에서는 친구가 아닌 사람들과 대화를 할 수 있는데, 본래 닉네임이 아닌 가상의 닉네임을 사용하여 채팅방에 들어갈 수 있다.

신입사원인 김크루는 카카오톡 오픈 채팅방을 개설한 사람을 위해, 다양한 사람들이 들어오고, 나가는 것을 지켜볼 수 있는 관리자창을 만들기로 했다. 채팅방에 누군가 들어오면 다음 메시지가 출력된다.

[닉네임]님이 들어왔습니다.

채팅방에서 누군가 나가면 다음 메시지가 출력된다.

[닉네임]님이 나갔습니다.

채팅방에서 닉네임을 변경하는 방법은 다음과 같이 두 가지이다.

  • 채팅방을 나간 후, 새로운 닉네임으로 다시 들어간다.
  • 채팅방에서 닉네임을 변경한다.

닉네임을 변경할 때는 기존에 채팅방에 출력되어 있던 메시지의 닉네임도 전부 변경된다.

예를 들어, 채팅방에 Muzi와 Prodo라는 닉네임을 사용하는 사람이 순서대로 들어오면 채팅방에는 다음과 같이 메시지가 출력된다.

Muzi님이 들어왔습니다.
Prodo님이 들어왔습니다.

채팅방에 있던 사람이 나가면 채팅방에는 다음과 같이 메시지가 남는다.

Muzi님이 들어왔습니다.
Prodo님이 들어왔습니다.
Muzi님이 나갔습니다.

Muzi가 나간후 다시 들어올 때, Prodo 라는 닉네임으로 들어올 경우 기존에 채팅방에 남아있던 Muzi도 Prodo로 다음과 같이 변경된다.

Prodo님이 들어왔습니다.
Prodo님이 들어왔습니다.
Prodo님이 나갔습니다.
Prodo님이 들어왔습니다.

채팅방은 중복 닉네임을 허용하기 때문에, 현재 채팅방에는 Prodo라는 닉네임을 사용하는 사람이 두 명이 있다. 이제, 채팅방에 두 번째로 들어왔던 Prodo가 Ryan으로 닉네임을 변경하면 채팅방 메시지는 다음과 같이 변경된다.

Prodo님이 들어왔습니다.
Ryan님이 들어왔습니다.
Prodo님이 나갔습니다.
Prodo님이 들어왔습니다.

채팅방에 들어오고 나가거나, 닉네임을 변경한 기록이 담긴 문자열 배열 record가 매개변수로 주어질 때, 모든 기록이 처리된 후, 최종적으로 방을 개설한 사람이 보게 되는 메시지를 문자열 배열 형태로 return 하도록 solution 함수를 완성하라.

제한사항

  • record는 다음과 같은 문자열이 담긴 배열이며, 길이는 1 이상 100,000 이하이다.
  • 다음은 record에 담긴 문자열에 대한 설명이다.
    • 모든 유저는 [유저 아이디]로 구분한다.
    • [유저 아이디] 사용자가 [닉네임]으로 채팅방에 입장 - Enter [유저 아이디] [닉네임] (ex. Enter uid1234 Muzi)
    • [유저 아이디] 사용자가 채팅방에서 퇴장 - Leave [유저 아이디] (ex. Leave uid1234)
    • [유저 아이디] 사용자가 닉네임을 [닉네임]으로 변경 - Change [유저 아이디] [닉네임] (ex. Change uid1234 Muzi)
    • 첫 단어는 Enter, Leave, Change 중 하나이다.
    • 각 단어는 공백으로 구분되어 있으며, 알파벳 대문자, 소문자, 숫자로만 이루어져있다.
    • 유저 아이디와 닉네임은 알파벳 대문자, 소문자를 구별한다.
    • 유저 아이디와 닉네임의 길이는 1 이상 10 이하이다.
    • 채팅방에서 나간 유저가 닉네임을 변경하는 등 잘못 된 입력은 주어지지 않는다.

입출력 예

record result
["Enter uid1234 Muzi", "Enter uid4567 Prodo","Leave uid1234","Enter uid1234 Prodo","Change uid4567 Ryan"] ["Prodo님이 들어왔습니다.", "Ryan님이 들어왔습니다.", "Prodo님이 나갔습니다.", "Prodo님이 들어왔습니다."]

입출력 예 설명

입출력 예 #1
문제의 설명과 같다.


def solution(record):

    queue = record.copy()  # record를 queue로 복사
    user = {}       # User uid, 닉네임 저장
    result = []     # 결과값
    chk = []        # 행동과 uid 저장 
    idx = 0         # queue 인텍스 값
    
    # 모든 값 탐색
    while idx < len(queue):
        # 행동, uid, 닉네임을 구분
        value = list(map(str,queue[idx].split()))
        # 입장한 경우
        if value[0] == "Enter":
            # 행동, uid 저장
            chk.append([value[0],value[1]])
            # dict에 uid, 닉네임 저장
            user[value[1]] = value[2]
        # 닉네임 변경한 경우
        elif value[0] == "Change":
            # dict에 닉네임 변경
            user[value[1]] = value[2]
        # 나간 경우
        else:
            # 행동, uid 저장
            chk.append([value[0], value[1]])
        idx += 1
    # 저장한 chk를 통해 차례대로 출력        
    for num in range(len(chk)):
        if chk[num][0] == "Enter":
            result.append(user[chk[num][1]] + "님이 들어왔습니다.")
        else:
            result.append(user[chk[num][1]] + "님이 나갔습니다.")
    return result

+ Recent posts