[프로그래머스][LV.3] 아이템 줍기 | python3

2025. 3. 19. 19:37프로그래머스/LV.3

문제링크:  아이템 줍기


문제설명

다음과 같은 다각형 모양 지형에서 캐릭터가 아이템을 줍기 위해 이동하려 합니다.

지형은 각 변이 x축, y축과 평행한 직사각형이 겹쳐진 형태로 표현하며, 캐릭터는 이 다각형의 둘레(굵은 선)를 따라서 이동합니다.

만약 직사각형을 겹친 후 다음과 같이 중앙에 빈 공간이 생기는 경우, 다각형의 가장 바깥쪽 테두리가 캐릭터의 이동 경로가 됩니다.

단, 서로 다른 두 직사각형의 x축 좌표 또는 y축 좌표가 같은 경우는 없습니다.

즉, 위 그림처럼 서로 다른 두 직사각형이 꼭짓점에서 만나거나, 변이 겹치는 경우 등은 없습니다.

다음 그림과 같이 지형이 2개 이상으로 분리된 경우도 없습니다.

한 직사각형이 다른 직사각형 안에 완전히 포함되는 경우 또한 없습니다.

지형을 나타내는 직사각형이 담긴 2차원 배열 rectangle, 초기 캐릭터의 위치 characterX, characterY, 아이템의 위치 itemX, itemY가 solution 함수의 매개변수로 주어질 때, 캐릭터가 아이템을 줍기 위해 이동해야 하는 가장 짧은 거리를 return 하도록 solution 함수를 완성해주세요.

제한조건

- rectangle의 세로(행) 길이는 1 이상 4 이하입니다.
- rectangle의 원소는 각 직사각형의 [좌측 하단 x, 좌측 하단 y, 우측 상단 x, 우측 상단 y] 좌표 형태입니다.
  - 직사각형을 나타내는 모든 좌표값은 1 이상 50 이하인 자연수입니다.
  - 서로 다른 두 직사각형의 x축 좌표, 혹은 y축 좌표가 같은 경우는 없습니다.
  - 문제에 주어진 조건에 맞는 직사각형만 입력으로 주어집니다.
- charcterX, charcterY는 1 이상 50 이하인 자연수입니다.
  - 지형을 나타내는 다각형 테두리 위의 한 점이 주어집니다.
- itemX, itemY는 1 이상 50 이하인 자연수입니다.
  - 지형을 나타내는 다각형 테두리 위의 한 점이 주어집니다.
- 캐릭터와 아이템의 처음 위치가 같은 경우는 없습니다.


- 전체 배점의 50%는 직사각형이 1개인 경우입니다.
- 전체 배점의 25%는 직사각형이 2개인 경우입니다.
- 전체 배점의 25%는 직사각형이 3개 또는 4개인 경우입니다.


 

문제풀이

피티씨가 많이 도와줌...

여기서 쓰인 알고리즘. 논리 등을 다른 문제에서도 활용할 수 있도록 정리해야할듯.


1. 문제 접근 방법

  • 여러 개의 직사각형이 겹쳐진 형태에서 가장 바깥쪽 테두리를 따라 이동해야 한다.
  • 캐릭터가 아이템을 줍기 위해 최단 거리로 이동해야 한다.
  • BFS(너비 우선 탐색)를 사용하여 최단 거리를 구한다.

2. 좌표 2배 확장하는 이유

🔹 원래 좌표로 계산하면 테두리를 정확히 찾기 어려움

  • 테두리만 남겨야 하는데, 정수 좌표에서는 테두리가 겹칠 수 있음.
  • 겹치면 대각선으로 이동하는 경우 생김
  • 따라서 모든 좌표를 2배 확장하여 더 정밀하게 처리한다.
  • 이후 탐색이 끝나면 결과를 다시 2로 나누어 원래 거리로 변환한다.

📌 좌표 확장 예시

원래 좌표: (1,1) → (2,2), (3,3) → (6,6)

3. visited 배열을 사용하는 이유

🔹 중복 방문을 방지하여 성능 최적화

  • 이미 방문한 곳을 다시 방문하면 불필요한 탐색이 발생할 수 있음.
  • visited를 사용하면 한 번 지나간 곳은 다시 가지 않도록 방지할 수 있음.
  • BFS에서 로 돌아가는 것을 방지하여 불필요한 연산을 줄인다.

4. BFS가 최단 거리를 보장하는 이유

🔹 BFS의 탐색 방식

  • BFS는 가까운 곳부터 탐색하는 특성이 있다.
  • 즉, 먼저 방문한 노드가 가장 짧은 거리가 된다.
  • Queue(큐)를 사용하여 현재 위치에서 갈 수 있는 모든 곳을 한 번씩 탐색한 후, 그다음 단계로 이동하기 때문.

📌 BFS가 최단 경로를 보장하는 이유

1️⃣ Queue를 사용해 가까운 곳부터 먼저 탐색

2️⃣ 모든 경로를 동시에 탐색하므로 돌아갈 필요 없음

3️⃣ 먼저 도착한 경로가 무조건 가장 짧은 거리

5. 결론

좌표를 2배 확장하여 정밀하게 경계를 추출

visited 배열을 사용하여 중복 방문을 방지

BFS를 사용하여 항상 최단 거리로 이동

 

 

나의코드

from collections import deque

def solution(rectangle, characterX, characterY, itemX, itemY):
    # 좌표평명 2배 확장
    max_size=102 # 50*2+2
    board = [[0]*max_size for _ in range(max_size)] # 좌표평면 생성
    
    # 좌표 2배 확장 및 직사각형 영역 표시
    for x1,y1,x2,y2 in rectangle:
        x1,y1,x2,y2=x1*2,y1*2,x2*2,y2*2 # 2배 확장
        for i in range(x1,x2+1):
            for j in range(y1,y2+1):
                if board[i][j]==0: # 불필요한 연산 줄이기 위해
                    board[i][j]=1 # 내부와 경계 1로
                    
    # 경계 제외 내부 0으로 변경
    for x1,y1,x2,y2 in rectangle:
        x1,y1,x2,y2=x1*2,y1*2,x2*2,y2*2
        for i in range(x1+1,x2):
            for j in range(y1+1,y2):
                board[i][j]=0 # 내부영역제거
                
    # BFS로 최단 거리 탐색
    dx=[1,-1,0,0]
    dy=[0,0,1,-1]
    
    characterX, characterY, itemX, itemY = characterX*2, characterY*2, itemX*2, itemY*2
    queue=deque([(characterX,characterY,0)]) # (현재 x,y 위치, 이동거리)
    visited=set()
    visited.add((characterX,characterY))
    
    while queue:
        x,y,dist=queue.popleft()
        
        if (x,y) == (itemX,itemY): # 모든 방향을 탐색하면서 가장 빨리 아이템과 만난 루트 확인
            return dist // 2 # 2배 확장했으니 2로 나눠줌
        
        for i in range(4): # 모든 방향 탐색. 
            nx,ny=x+dx[i],y+dy[i]
            if ( 0 <=  max_size and 0 <= ny < max_size and board[nx][ny] == 1 and (nx,ny) not in visited):
                visited.add((nx,ny))
                queue.append((nx,ny,dist+1))
                
    return -1 # 도달 못 할 경우

 


 

더보기

참고하기

import itertools  # 무한 반복을 위한 itertools.count() 사용


def is_movable(cur_x, cur_y, next_x, next_y, rectangles):
    """
    현재 위치(cur_x, cur_y)에서 다음 위치(next_x, next_y)로 이동할 수 있는지 판단하는 함수.
    이동 가능하려면:
      1. 이동하는 길이 테두리(경계선) 위에 있어야 함.
      2. 이동하는 길이 직사각형 내부에 있으면 안 됨.
    """
    # 이동 중간 지점 (테두리에 있는지 확인하기 위해)
    x, y = (cur_x + next_x) / 2, (cur_y + next_y) / 2

    # 중간 지점이 직사각형의 경계선에 있는지 확인
    is_on_any_border = any(
        (x in (x1, x2) and y1 <= y <= y2) or  # 세로 경계
        (y in (y1, y2) and x1 <= x <= x2)  # 가로 경계
        for x1, y1, x2, y2 in rectangles
    )

    # 중간 지점이 직사각형 내부에 있는지 확인
    is_inside_any_rect = any(
        x1 < x < x2 and y1 < y < y2  # 내부에 있다면 이동 불가
        for x1, y1, x2, y2 in rectangles
    )

    return is_on_any_border and not is_inside_any_rect  # 테두리에 있고 내부가 아니면 이동 가능


def solution(rectangle, characterX, characterY, itemX, itemY):
    """
    캐릭터가 지형의 테두리를 따라 이동하며 아이템을 줍기 위한 최단 거리를 구하는 함수.

    - rectangle: 직사각형 정보를 담은 리스트 [(x1, y1, x2, y2), ...]
    - characterX, characterY: 캐릭터의 초기 위치
    - itemX, itemY: 아이템의 위치
    """
    cur_pos = (characterX, characterY)  # 현재 위치
    prev_pos = None  # 이전 위치 (뒤로 가는 것 방지)
    
    for dist in itertools.count():  # 무한 루프 (둘레를 한 바퀴 돌면 종료)
        if cur_pos == (characterX, characterY) and prev_pos:
            # 출발 위치로 돌아왔다면 전체 둘레(perimeter) 저장 후 종료
            perimeter = dist
            break
        elif cur_pos == (itemX, itemY):
            # 아이템 위치에 도달하면 해당 거리 저장
            dist_to_item = dist
        
        # 4방향 탐색 (오른쪽, 왼쪽, 위, 아래)
        for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
            next_pos = (cur_pos[0] + dx, cur_pos[1] + dy)

            # 다음 위치가 이전 위치가 아니고, 이동 가능하면 이동
            if next_pos != prev_pos and is_movable(*cur_pos, *next_pos, rectangle):
                prev_pos, cur_pos = cur_pos, next_pos  # 위치 업데이트
                break  # 한 번 이동했으면 다음 루프로 진행

    # 전체 둘레에서 아이템까지의 거리와 반대 방향 거리 중 최솟값 반환
    return min(dist_to_item, perimeter - dist_to_item)