ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 99클럽 코테 스터디 16일차 TIL - 비슷한 문제가 생각날 때
    Coding Test 2024. 4. 9. 23:52

    문제1 - 부대 복귀 문제링크

    문제 설명

    양방향 그래프가 주어지고 출발지 노드들과 목적지가 주어질 때, 각 출발지에서 목적지 노드까지의 최단경로 거리를 구하는 문제입니다.

    목적지에 도달할 수 없다면 -1입니다.

     

    제약조건

    노드n: 최대 10만개

    간선roads: 최대 50만개

    출발지sources: 최대 500개

     

    풀이 과정

    처음엔 그래프상에서 최단경로 길이를 구하는 문제라서 다익스트라를 사용해서 출발지 노드에서 목적지 노드까지의 최단경로거리를 구했습니다.

     

    시행착오1(다익스트라, 실패)

    import heapq
    
    def solve(graph, dists, src, dest):
        q = []
        dists[src][src] = 0
        heapq.heappush(q, (0, src))
    
        while q:
            curDist, curPos = heapq.heappop(q)
    
            if curDist > dists[src][curPos]:
                continue
    
            for nxt in graph[curPos]:
                if dists[src][nxt] > curDist + 1:
                    dists[src][nxt] = curDist + 1
                    heapq.heappush(q, (dists[src][nxt], nxt))
        
        if dists[src][dest] == float('inf'):
            return -1
    
        return dists[src][dest]
        
    
        
    def solution(n, roads, sources, destination):
        answer = []
        graph = [[] for _ in range(n + 1)]
        for road in roads:
            u, v = road
            graph[u].append(v)
            graph[v].append(u)
    
        dists = [[float('inf') for _ in range(n + 1)] for _ in range(n + 1)]
    
        for source in sources:
            if source == destination:
                answer.append(0)
            elif not graph[source]:
                answer.append(-1)
            else:
                res = solve(graph, dists, source, destination)
                answer.append(res)
            
        return answer

     

    이후 제출한 결과 몇 개의 테스트케이스에 대해 시간초과가 발생했습니다.

    제 풀이의 시간복잡도를 계산해보면, O(len(source) * len(roads)*logn)입니다.

    이는 최대 500 * 500000 * 16 = 40억회의 연산이 필요해서 사실상 시간초과가 발생할 수밖에 없습니다.

     

    시행착오2(성공)

    어떻게 하면 좀 더 빠를지 고민하다가, 문제를 뒤집어서 목적지에서 출발하여 인접 노드를 탐색하면서 최단거리를 갱신하는 방식으로 구현하였습니다.

    from collections import deque
    
    def solution(n, roads, sources, destination):
        answer = []
        
        q = deque()
    
        dists = [float('inf') for _ in range(n + 1)]
    
        graph = [[] for _ in range(n + 1)]
        for road in roads:
            graph[road[0]].append(road[1])
            graph[road[1]].append(road[0])
    
        for nxt in graph[destination]:
            q.append((nxt, 1))
            dists[nxt] = 1
        
        while q:
            curPos, cnts = q.popleft()
    
            if dists[curPos] != cnts:
                continue
            
            for nxt in graph[curPos]:
                if dists[nxt] > cnts + 1:
                    dists[nxt] = cnts + 1
                    q.append((nxt, dists[nxt]))
            
        for source in sources:
            if source == destination:
                answer.append(0)
            elif dists[source] == float('inf'):
                answer.append(-1)
            else:
                answer.append(dists[source])
    
        return answer

    목적지에서 출발하여 다른 노드들까지의 최단거리만 구하면 되므로 한 번만 다익스트라 알고리즘을 수행하면 됩니다. 그래서 dists 배열을 1차원으로 구현할 수 있었습니다.

     

    제출 결과

     

    시행착오3(성공)

    더 간단한 풀이는 없을까 고민하던 중, 어차피 간선의 가중치는 모두 1이라서 다익스트라를 굳이 사용할 필요가 없겠다라는 생각이 들었습니다. 그래서 일반적인 방문체크를 수행하는 BFS로 풀 수 있겠다고 생각했습니다.

    from collections import deque
    def solution(n, roads, sources, destination):
        answer = []
        graph = [[] for _ in range(n + 1)]
        for road in roads:
            graph[road[0]].append(road[1])
            graph[road[1]].append(road[0])
    
        q = deque()
        vis = set()
        q.append((destination, 0))
        vis.add(destination)
        dists = [-1 for _ in range(n + 1)]
        dists[destination] = 0
    
        while q:
            curPos, cnts = q.popleft()
    
            for nxt in graph[curPos]:
                if nxt in vis:
                    continue
                vis.add(nxt)
    
                dists[nxt] = cnts + 1
                q.append((nxt, cnts + 1))
        
        for source in sources:
            answer.append(dists[source])
    
        return answer

    목적지에서부터 시작하여 인접 노드를 BFS 탐색하며 최단거리를 구합니다. 이미 방문했던 노드라면 더 빠른 경로가 있었다는 뜻이기 때문에 다시 방문하지 않아도 됩니다.

     

    제출 결과

     

    dist값을 비교하는 조건 대신 set을 이용해 방문체크를 하기 때문에 더 시간이 적게 걸렸습니다.

     

    문제2 - 풍선 터뜨리기 문제링크

    문제 설명

    각자 고유한 숫자가 적힌 풍선이 일렬로 주어지는데, 인접한 두개의 풍선을 선택하고 그중 하나를 터뜨린 다음, 다시 풍선을 밀어서 빈 공간을 채웁니다.

    이 과정을 풍선 하나가 남을 때까지 반복하는데, 풍선을 터뜨릴 때 규칙이 있습니다.

    규칙: 일반적으로는 숫자가 더 큰 풍선을 터뜨리되, 단 한 번 숫자가 더 작은 풍선을 터뜨려도 됩니다.

    우리가 구해야 할 것은, 모든 경우의 수를 다 체크해보았을 때 최후에 남을 풍선이 될 수 있는 풍선들을 모아 반환하는 것입니다.

     

    풀이 과정

    처음에 이 문제를 봤을 때는 DFS나 백트래킹같은 재귀함수 형태로 풀어야 할 것 같았습니다. 모든 경우를 다 체크해가면서 확인해야했기 때문입니다. 하지만, 단 한 번 숫자가 더 작은 풍선을 터뜨릴 수 있다는 규칙이 추가됨에 따라 어떻게 코드를 작성해야할지 어려움을 겪었습니다.

    그래서 이 방법은 미뤄두고, 특정 풍선이 무조건 죽는 경우를 생각해보았습니다. 최악의 상황을 가정하여 풍선A와 A의 풍선이 계속 대결을 펼친다고 가정하겠습니다. 그럼 A보다 숫자가 큰 풍선은 모두 터질 것입니다. 마침내 A보다 작은 숫자 풍선들만 남을텐데, 이때 풍선A 양쪽에 풍선A보다 작은 숫자가 있다면 이 풍선은 살릴 수 없습니다.

    이때 한쪽에만 더 작은 풍선이 있으면 1회용 스킬인 더 작은 숫자풍선 터뜨리기를 하면 되므로 살 수 있습니다. 더 작은 풍선이 한쪽에 2개 이상 있을지라도 그들끼리 터지는 과정속에서 1개만 남게될 것이고 이에 따라 한쪽에 작은 풍선이 1개만 있을 때만 고려해도 됩니다.

    def solution(balloons):
        if len(balloons) <= 2:
            return len(balloons)
        
        answer = 2
        n = len(balloons)
        
        leftMin = [0 for _ in range(n)]
        rightMin = [0 for _ in range(n)]
        
        leftMin[0] = balloons[0]
        rightMin[n - 1] = balloons[n - 1]
        
        for i in range(1, n - 1):
            leftMin[i] = min(leftMin[i - 1], balloons[i])
            rightMin[n - i - 1] = min(rightMin[n - i], balloons[n - i - 1])
        
        for i in range(1, n - 1):
            answer += 1 if not (leftMin[i - 1] < balloons[i] and balloons[i] > rightMin[i + 1]) else 0
        
        return answer

     

    제출결과

     

    제3 - 호텔 대실 문제링크

     

    문제 설명

    사람들이 호텔방을 특정 시간동안 사용하고 싶어합니다. 이때 방을 최소개수만 사용해서 사용 요청 리스트를 모두 처리하는 방 개수를 구하면 됩니다.

    사용 요청은 [시작시간, 끝나는시간]형태로 주어지며, 끝난 후에는 10분간의 방 정리 시간이 필요해서 끝나고 10분 이후에 다시 해당 방에 손님을 받을 수 있습니다.

     

    풀이 과정

    우선, 시작시간이 빠르고, 만약 시작시간이 같다면 끝나는시간이 더 빠른 것이 앞에 오도록 정렬해줍니다.

    정렬에 앞서, 시간값이 "시:분" 꼴의 문자열 형태로 주어졌기 때문에 해당 시간을 분으로 바꿔주는 toMinutes 함수를 만들어 숫자로 파싱한 후 정렬해주었습니다.

    이후, 예약요청을 순회하면서, 현재 사용중인 방들의 끝나는 시간이 담겨 있는 최소힙에서 가장 빨리 끝나는 방을 뽑아봤을 때, 이 방에 들어갈 수 있다면 들어가고, 들어갈 수 없다면 나머지 확인하지 않은 방들은 전부 들어갈 수 없기 때문에 새로운 방을 하나 배정해줍니다.

    import heapq
    
    def toMinutes(s):
        temp = s.split(':')
        res = 60 * int(temp[0]) + int(temp[1])
        return res
        
    def solution(book_time):
        # : 기준 파싱해서 튜플로 만들어 저장
        # 시작 시간기준 정렬
        # 순회하며 최소힙에 끝나는 시간 넣음
        # 최소힙에 아무것도 없거나 top 시간이 아직 안됐으면 방 하나 추가
        # 시간 됐으면 해당 요소 팝 시키고 추가
        
        for bt in book_time:
            for i in range(len(bt)):
                bt[i] = toMinutes(bt[i])
        
        book_time.sort()
        hq = []
        cnts = 0
    
        for bt in book_time:
            if not hq:
                cnts += 1
                heapq.heappush(hq, bt[1])
                continue
            
            if bt[0] < hq[0] + 10:
                cnts += 1
            else:
                heapq.heappop(hq)
            heapq.heappush(hq, bt[1])
                
                
        return cnts

     

    제출결과

     

    총평

    까다로운 문제가 많았습니다. 풍선 문제는 예전에 리트코드에서 trapping rain water을 풀면서 leftMax, rightMax를 구해가며 풀었던 기억이 생각나서 풀 수 있었고, 호텔방 문제는 프로세스 스케쥴러나 철로 문제같은 스위핑 문제를 풀어보면서 정렬과 우선순위 큐의 활용에 익숙해져서 풀 수 있었습니다.

     

Designed by Tistory.