QA & Engineering Blog

A Blog about Quality · Automation · Engineering

🏠 홈으로

[level 2] 선인장 숨기기 - 468379

문제 링크

성능 요약

메모리: 184 MB, 시간: 632.76 ms

구분

코딩테스트 연습 > 2025 카카오 하반기 2차

채점결과

정확성: 100.0
합계: 100.0 / 100.0

제출 일자

2026년 05월 29일 12:36:31

문제 설명

m개의 행과 n개의 열로 구성된 격자가 주어지며, 이는 사막 지도를 나타냅니다. 사막 지도의 가장 왼쪽 위칸 좌표는 (0, 0), 오른쪽 아래칸 좌표는 (m-1, n-1)입니다. 이 사막 어딘가에 가로 w, 세로 h 크기의 선인장 구역을 조성하려 합니다. 선인장 구역은 격자 축에 맞춘 연속된 w × h 크기의 부분 격자이며, 회전할 수 없습니다.

비구름은 미리 정해진 순서대로 격자의 여러 칸에 비를 뿌립니다. 이때 빗방울이 처음으로 선인장 구역에 포함된 칸에 떨어졌을 때, 그 시점을 선인장이 처음으로 비를 맞는 순간으로 기록합니다. 당신은 선인장이 가능한 한 늦게 비를 맞도록, 선인장 구역의 위치를 정하려고 합니다.

  • 선인장이 비를 맞지 않도록 선인장 구역의 위치를 정할 수 있다면 해당 위치가 가장 우선됩니다.
  • 가능한 늦게 비를 맞는 선인장 구역 후보가 여러 개라면 그중 가장 위쪽 행, 그래도 여러 개면 가장 왼쪽 열에 위치한 구역을 선택합니다.

격자의 세로 길이와 가로 길이를 나타내는 정수 m, n, 선인장 구역의 세로 길이와 가로 길이를 나타내는 정수 h, w, 그리고 빗방울이 떨어지는 순서대로 칸의 좌표를 담은 2차원 정수 배열 drops가 매개변수로 주어집니다. 주어진 조건을 만족하는 선인장 구역에 포함된 가장 왼쪽 위칸의 좌표를 정수 배열로 return 하도록 solution 함수를 완성해 주세요.


제한사항
  • 1 ≤ m, n ≤ 500,000
  • 1 ≤ m × n ≤ 500,000
  • 1 ≤ hm
  • 1 ≤ wn
  • 1 ≤ drops의 길이 ≤ m × n
    • drops[i]는 [r, c] 형태입니다.
    • drops[i]i + 1번째로 떨어진 빗방울의 좌표를 의미합니다.
    • 0 ≤ r < m
    • 0 ≤ c < n
    • drops의 모든 원소는 서로 다른 칸을 나타냅니다.

테스트 케이스 구성 안내

아래는 테스트 케이스 구성을 나타냅니다. 각 그룹은 하나 이상의 하위 그룹으로 이루어져 있으며, 하위 그룹의 모든 테스트 케이스를 통과하면 해당 그룹에 할당된 점수를 획득할 수 있습니다.

그룹 총점 추가 제한 사항
#1 30% m ≤ 50, n ≤ 50
#2 70% 추가 제한 없음

입출력 예
m n h w drops result
4 5 2 2 [[0, 0], [3, 1], [1, 3], [2, 4], [1, 1], [2, 2], [2, 3], [0, 4]] [2, 2]
3 3 1 1 [[0, 0], [0, 1], [0, 2], [1, 0]] [1, 1]
4 6 3 4 [[1, 2]] [0, 0]
4 6 1 2 [[0, 1], [0, 3], [0, 5], [1, 1], [1, 3], [1, 5], [2, 1], [2, 3], [2, 5], [3, 1], [3, 3], [3, 5]] [3, 4]
2 2 2 2 [[0, 0], [0, 1], [1, 1], [1, 0]] [0, 0]
4 4 3 1 [[2, 0], [1, 3], [3, 2], [0, 1]] [0, 2]

입출력 예 설명

입출력 예 #1

아래 그림은 4 × 5 크기의 지도 격자입니다. 각 칸의 큰 숫자는 빗방울이 떨어지는 순서를, 작은 숫자는 좌표를 나타냅니다.

trs_ex1_1.png

노란색으로 표시된 구역을 선인장 구역으로 두면, 6번째로 비가 떨어질 때 선인장이 처음 비를 맞게 되며, 이보다 더 늦게 젖도록 하는 배치는 존재하지 않습니다. 따라서 노란색 구역의 가장 왼쪽 위 좌표인 [2, 2]를 return 해야 합니다.

입출력 예 #2

아래 그림은 3 × 3 크기의 지도 격자입니다. 각 칸의 큰 숫자는 빗방울이 떨어지는 순서를, 작은 숫자는 좌표를 나타냅니다.

trs_ex2.png

모든 빗방울이 떨어질 때까지 좌표가 (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)인 칸은 젖지 않습니다. 이 5칸 어디에나 1 × 1 크기의 선인장 구역을 놓을 수 있지만, 그중 가장 위쪽 행, 그리고 가장 왼쪽 열에 해당하는 좌표는 (1, 1)입니다. 따라서 [1, 1]을 return 해야 합니다.

입출력 예 #3

아래 그림은 4 × 6 크기의 지도 격자입니다. 각 칸의 큰 숫자는 빗방울이 떨어지는 순서를, 작은 숫자는 좌표를 나타냅니다.

trs_ex3.png

선인장 구역을 어디에 배치하더라도 첫 번째 빗방울만에 구역이 젖습니다. 따라서 가장 위쪽 행, 그중에서도 가장 왼쪽 열에 위치하는 좌표인 [0, 0]을 return 해야 합니다.

입출력 예 #4

아래 그림은 4 × 6 크기의 지도 격자입니다. 각 칸의 큰 숫자는 빗방울이 떨어지는 순서를, 작은 숫자는 좌표를 나타냅니다.

trs_ex4.png

따라서 [3, 4]를 return 해야 합니다.

입출력 예 #5

아래 그림은 2 × 2 크기의 지도 격자입니다. 각 칸의 큰 숫자는 빗방울이 떨어지는 순서를, 작은 숫자는 좌표를 나타냅니다.

trs_ex5.png

따라서 [0, 0]을 return 해야 합니다.

입출력 예 #6

아래 그림은 4 × 4 크기의 지도 격자입니다. 각 칸의 큰 숫자는 빗방울이 떨어지는 순서를, 작은 숫자는 좌표를 나타냅니다.

trs_ex6.png

따라서 [0, 2]를 return 해야 합니다.

출처: 프로그래머스 코딩 테스트 연습, https://school.programmers.co.kr/learn/challenges

💡 Solutions

📄 선인장 숨기기.py

from collections import deque

def sliding_min(arr, k):
    """
    [슬라이딩 윈도우 + Monotonic Deque]

    길이 k인 창이 한 칸씩 이동할 때,
    매번 창 안의 최솟값(min)을 O(1) amortized로 구한다.

    ---

    처음 시도했던 방식 (maxlen + min):
        window = deque(maxlen=k)
        window.append(x)
        res.append(min(window))   # ← 창 전체를 매번 훑음 → O(k)

    이 방식도 '슬라이딩 윈도우' 아이디어는 맞지만,
    w나 h가 크면 O(m * n * (w + h))라 Python에서 TLE 날 수 있다.

    ---

    Monotonic Deque를 쓰는 이유:
        deque 자체가 min을 자동으로 해주는 게 아니다.
        우리가 '앞으로 최솟값 후보가 될 수 있는 인덱스'만 남기도록
        규칙을 직접 만들어 주는 것이다.

        - popleft(): 창 밖으로 나간(만료된) 인덱스 제거
        - pop()    : 지금 값보다 크거나 같아서, 앞으로 min이 될 일 없는 인덱스 제거
        - append() : 새 후보 등록

        dq[0]이 항상 현재 창의 최솟값 인덱스가 되므로,
        min(window)처럼 매번 O(k) 탐색할 필요가 없다 → 전체 O(n)
    """
    dq = deque()  # 인덱스 저장 (값이 아닌 인덱스! arr[dq[i]]는 증가 순서)
    res = []

    for i, x in enumerate(arr):
        # 1) 창 밖으로 나간 인덱스 제거
        #    현재 i 기준 창은 [i-k+1, i]
        #    i-k 보다 작거나 같으면 이미 창 밖
        while dq and dq[0] <= i - k:
            dq.popleft()

        # 2) 최솟값 후보가 될 수 없는 인덱스 제거
        #    뒤에서부터 보며, arr[dq[-1]] >= x 이면
        #    x가 창 안에 있는 동안 절대 최솟값이 될 수 없음
        while dq and arr[dq[-1]] >= x:
            dq.pop()

        # 3) 현재 인덱스를 후보로 등록
        dq.append(i)

        # 4) 창이 k개 채워졌을 때부터 결과 기록
        if i >= k - 1:
            res.append(arr[dq[0]])  # dq[0] = 현재 창에서 가장 작은 값의 인덱스

    return res


def solution(m, n, h, w, drops):
    """
    [핵심 아이디어]

    문제: h x w 선인장 구역을 어디에 두면
         가능한 한 늦게(또는 아예 안) 비를 맞을까?

    ---

    1단계) 각 칸에 '몇 번째 빗방울'이 떨어졌는지 기록
        - drops[i] = [r, c]  →  grounds[r][c] = i + 1
        - 비 안 맞은 칸     →  inf

    2단계) 선인장 좌상단 (top_r, top_c)마다 '처음 젖는 순서' 계산
        - h x w 영역 안 칸들 중 가장 빠른(작은) drop 순서 = first_wet
        - 영역 안에 inf가 하나라도 있으면 → 영원히 안 젖음 (최우선)
        - first_wet이 클수록 좋음 (늦게 젖음)
        - 동점이면 → 행 작은 것 → 열 작은 것

    3단계) 2D 구간 min을 슬라이딩 윈도우로 구하기
        - 한 번에 h x w 전체를 min()하면 O(h * w) → 너무 느림
        - 가로 w min → 세로 h min 으로 2-pass 분리 (분리 가능!)
        - 각 pass는 sliding_min()으로 O(m * n)

    ---

    예시 직관:
        first_wet(top_r, top_c)
            = min( grounds[r][c] for r in [top_r..top_r+h-1],
                              c in [top_c..top_c+w-1] )

        이 min을 모든 (top_r, top_c)에 대해 구한 뒤,
        row-major 순회로 score가 가장 큰 위치를 고른다.
    """
    INF = float('inf')
    grounds = [[INF for _ in range(n)] for _ in range(m)]

    # drops 순서대로 각 칸에 빗방울 번호 기록
    for i, drop in enumerate(drops):
        a, b = drop
        grounds[a][b] = i + 1

    # --------------------------------------------------
    # Pass 1) 각 행에서 가로 w 구간의 min
    #
    # row_min[r][c] = grounds[r][c : c+w] 의 최솟값
    #
    # 슬라이딩 윈도우가 가로로 w칸씩 밀리며 min을 구한다.
    # --------------------------------------------------
    row_min = [sliding_min(grounds[r], w) for r in range(m)]

    rows = m - h + 1  # 선인장 좌상단 row 후보 수
    cols = n - w + 1  # 선인장 좌상단 col 후보 수

    # --------------------------------------------------
    # Pass 2) row_min 결과에 대해 세로 h 구간의 min
    #
    # window_first_wet[r][c]
    #   = (top_r=r, top_c=c)에 h x w 선인장을 뒀을 때
    #     처음 비를 맞는 순서
    #
    # 가로 min 결과를 세로로 h칸씩 슬라이딩하면
    # 2D h x w 구간 min이 된다.
    # --------------------------------------------------
    window_first_wet = [[INF for _ in range(cols)] for _ in range(rows)]

    for c in range(cols):
        # 같은 열 c에서, 각 행의 가로-w-min 값들을 모음
        column = [row_min[r][c] for r in range(m)]
        col_mins = sliding_min(column, h)

        for r in range(rows):
            window_first_wet[r][c] = col_mins[r]

    # --------------------------------------------------
    # Pass 3) 정답 위치 선택
    #
    # - score가 클수록 좋음 (inf = 안 젖음 = 최우선)
    # - for r → for c 순서 = tie-break (위쪽 행, 왼쪽 열)
    #   동점이면 먼저 만난 (r, c)가 더 위·더 왼쪽이므로
    #   별도 처리 없이 row-major 순회만으로 충분
    # --------------------------------------------------
    best_score = -1
    answer = [0, 0]

    for r in range(rows):
        for c in range(cols):
            score = window_first_wet[r][c]
            if score > best_score:
                best_score = score
                answer = [r, c]

    return answer
# 더 많은 코드들은...
# https://kimsc9976.github.io/algorithm/프로그래머스/
질문/피드백

이 문제 풀이가 궁금하면 GitHub Issues에 남겨주세요. 확인되는 대로 답변드릴게요.

이 문제로 질문하기 →