문제 후기

제한 사항에서 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

문제 후기

해당 문제는 LRU(Least Recently Used) 알고리즘을 참고하여 푸는 방식이다. 

LRU 알고리즘이란 메모리 크기를 가지고 있고 Queue 형태로 캐시 메모리에 들어가는 것이다. 이 때 캐시 메모리 안에 있다면 해당 캐시를 삭제 후 다시 queue에 넣는 방식이다. 자세한 것은 아래의 이미지를 참고한다.

해당 문제를 풀면서 메모리가 갱신되는 것을 잊고 있어서 실수가 발생했다. 다음에도 주의해야할 것 같다.

문제 설명

캐시

지도개발팀에서 근무하는 제이지는 지도에서 도시 이름을 검색하면 해당 도시와 관련된 맛집 게시물들을 데이터베이스에서 읽어 보여주는 서비스를 개발하고 있다.
이 프로그램의 테스팅 업무를 담당하고 있는 어피치는 서비스를 오픈하기 전 각 로직에 대한 성능 측정을 수행하였는데, 제이지가 작성한 부분 중 데이터베이스에서 게시물을 가져오는 부분의 실행시간이 너무 오래 걸린다는 것을 알게 되었다.
어피치는 제이지에게 해당 로직을 개선하라고 닦달하기 시작하였고, 제이지는 DB 캐시를 적용하여 성능 개선을 시도하고 있지만 캐시 크기를 얼마로 해야 효율적인지 몰라 난감한 상황이다.

어피치에게 시달리는 제이지를 도와, DB 캐시를 적용할 때 캐시 크기에 따른 실행시간 측정 프로그램을 작성하시오.

입력 형식

  • 캐시 크기(cacheSize)와 도시이름 배열(cities)을 입력받는다.
  • cacheSize는 정수이며, 범위는 0 ≦ cacheSize ≦ 30 이다.
  • cities는 도시 이름으로 이뤄진 문자열 배열로, 최대 도시 수는 100,000개이다.
  • 각 도시 이름은 공백, 숫자, 특수문자 등이 없는 영문자로 구성되며, 대소문자 구분을 하지 않는다. 도시 이름은 최대 20자로 이루어져 있다.

출력 형식

  • 입력된 도시이름 배열을 순서대로 처리할 때, 총 실행시간을 출력한다.

조건

  • 캐시 교체 알고리즘은 LRU(Least Recently Used)를 사용한다.
  • cache hit일 경우 실행시간은 1이다.
  • cache miss일 경우 실행시간은 5이다.

입출력 예제

캐시크기(cacheSize) 도시이름(cities) 실행시간
3 [Jeju, Pangyo, Seoul, NewYork, LA, Jeju, Pangyo, Seoul, NewYork, LA] 50
3 [Jeju, Pangyo, Seoul, Jeju, Pangyo, Seoul, Jeju, Pangyo, Seoul] 21
2 [Jeju, Pangyo, Seoul, NewYork, LA, SanFrancisco, Seoul, Rome, Paris, Jeju, NewYork, Rome] 60
5 [Jeju, Pangyo, Seoul, NewYork, LA, SanFrancisco, Seoul, Rome, Paris, Jeju, NewYork, Rome] 52
2 [Jeju, Pangyo, NewYork, newyork] 16
0 [Jeju, Pangyo, Seoul, NewYork, LA] 25

from _collections import deque

def solution(cacheSize, cities):
    queue = deque([0] * cacheSize)  # 캐시 생성
    answer = 0     # 정답값
    if cacheSize == 0:  # 캐시가 0이면 전부 chache miss
        return 5 * len(cities)
    for name in cities:     # 캐시 메모리에 돌리기
        name = name.lower() # 전부 소문자로
        if name in queue:   # 만약 도시가 캐시에 있다면
            queue.remove(name)  # 캐시 최신으로 교체
            queue.append(name)
            answer += 1     # cache hit

        else:
            queue.popleft() # 가장 옛날 캐시 삭제
            queue.append(name)  # 캐시에 추가
            answer += 5     # cache miss

    return answer   # 정답 리턴

 

문제 후기

단순히 a와 b의 숫자를 반으로 줄여가면 된다.

문제를 풀면서 틀렸던 부분은 단순히 두 값의 절대값이 1일 때 끝나도록 했는데 (4, 5)와 같이 대진이 아니더라도 절대값이 1이 발생하는 부분이 있다는 것을 실수했다.

문제 설명

△△ 게임대회가 개최되었습니다. 이 대회는 N명이 참가하고, 토너먼트 형식으로 진행됩니다. N명의 참가자는 각각 1부터 N번을 차례대로 배정받습니다. 그리고, 1번↔2번, 3번↔4번, ... , N-1번↔N번의 참가자끼리 게임을 진행합니다. 각 게임에서 이긴 사람은 다음 라운드에 진출할 수 있습니다. 이때, 다음 라운드에 진출할 참가자의 번호는 다시 1번부터 N/2번을 차례대로 배정받습니다. 만약 1번↔2번 끼리 겨루는 게임에서 2번이 승리했다면 다음 라운드에서 1번을 부여받고, 3번↔4번에서 겨루는 게임에서 3번이 승리했다면 다음 라운드에서 2번을 부여받게 됩니다. 게임은 최종 한 명이 남을 때까지 진행됩니다.

이때, 처음 라운드에서 A번을 가진 참가자는 경쟁자로 생각하는 B번 참가자와 몇 번째 라운드에서 만나는지 궁금해졌습니다. 게임 참가자 수 N, 참가자 번호 A, 경쟁자 번호 B가 함수 solution의 매개변수로 주어질 때, 처음 라운드에서 A번을 가진 참가자는 경쟁자로 생각하는 B번 참가자와 몇 번째 라운드에서 만나는지 return 하는 solution 함수를 완성해 주세요. 단, A번 참가자와 B번 참가자는 서로 붙게 되기 전까지 항상 이긴다고 가정합니다.

제한사항

  • N : 21 이상 220 이하인 자연수 (2의 지수 승으로 주어지므로 부전승은 발생하지 않습니다.)
  • A, B : N 이하인 자연수 (단, A ≠ B 입니다.)

입출력 예

N A B answer
8 4 7 3

입출력 예 설명

입출력 예 #1
첫 번째 라운드에서 4번 참가자는 3번 참가자와 붙게 되고, 7번 참가자는 8번 참가자와 붙게 됩니다. 항상 이긴다고 가정했으므로 4번 참가자는 다음 라운드에서 2번이 되고, 7번 참가자는 4번이 됩니다. 두 번째 라운드에서 2번은 1번과 붙게 되고, 4번은 3번과 붙게 됩니다. 항상 이긴다고 가정했으므로 2번은 다음 라운드에서 1번이 되고, 4번은 2번이 됩니다. 세 번째 라운드에서 1번과 2번으로 두 참가자가 붙게 되므로 3을 return 하면 됩니다.


def solution(n,a,b):
    answer = 1  # 정답값

    while True:     # 리턴할 때 까지 반복
        # 두 값의 차이가 1의 차이가 날때
        # 주의) a와 b가 대진을 해야하는데 2,3 같이 1의 차이가 나도 대결 안하는 경우
        if abs(a - b) == 1 and a // 2 != b // 2:
            return answer   # 정답 리턴
        else:
            if a % 2 == 1:  # 홀수일 때
                a = (a + 1) // 2
            else:
                a = a // 2  # 짝수일 때

            if b % 2 == 1:
                b = (b + 1) // 2    # 홀수일 때
            else:
                b = b // 2      # 짝수일 때
            answer += 1     # 대진횟수

+ Recent posts