Codepass Expert Deep-Dive № 08 · Placement
문제 페이지 Big-First Greedy + Flatness Map
DEEP-DIVE № 08 — 2407 SpacecraftLanding · 설계 논증

풀이 코드는 없다. 점수는 큰 우주선이 만든다 — 그래서 큰 것부터 앉힌다.

우주선착륙은 원본 자료에 grader와 빈 스텁(init·process의 To-Do)만 공개돼 있고 검증된 통과 코드가 없다. 따라서 이 페이지는 다른 심층분석과 달리 — 라인 단위 코드 해부 대신, verify()가 정의하는 제약을 읽고 베스트 해법을 설계하며 그 정당성을 논증한다. 점수 함수가 왜 "큰 우주선 우선" 그리디를 강제하는지, 높이차 ≤ 6 제약이 어떻게 "평탄도 맵" 전처리로 환원되는지, 회전(dir)이 점수와 무관하면서도 배치 가능성을 어떻게 넓히는지를 설계 의사코드와 C++ 스케치로 제시한다. 추측이 들어가는 곳은 설계 제안으로 표시한다.

1000×1000 지형 · 우주선 10,000개 · 10 TC grader: init/process 빈 스텁 설계 관점 분석 — 통과 코드 미보유

§ 1베스트 해법의 골격 — verify가 정의하는 문제

우주선착륙에는 통과 코드가 없다. 그러나 채점기 verify()make_tc()는 완전히 공개돼 있고, 좋은 해법의 형태는 거기서 연역할 수 있다. 먼저 점수와 제약을 정확히 적는다.

점수(우주선 i): score(i) = h · w · min(h, w) h, w ∈ {2,3,4,5} 배치 제약: ① 경계 : 회전 적용 후 [y1,y2]×[x1,x2]가 1000×1000 안 ② 높이차 : 네 모서리 지형 높이의 max − min ≤ 6 ③ 겹침 금지 : 점유 직사각형이 다른 우주선과 한 칸도 겹치지 않음 PASS: 배치된 우주선 점수 합 SCORE ≥ 3,000,000 objective & constraints

주목할 점: PENALTY = 0이고, 한 우주선의 rows/cols/dirs-1로 두면 verify()는 그 우주선을 건너뛴다(continue). 즉 배치하지 않아도 실패가 아니다 — 단지 그 점수를 못 얻을 뿐이다. 이것이 설계의 출발점을 바꾼다. 완전성(모든 우주선 배치)은 요구되지 않는다. 목표는 순수하게 "배치한 우주선들의 점수 합 최대화"다.

점수 함수가 강요하는 우선순위

score = h·w·min(h,w)를 크기별로 펼치면 비대칭이 드러난다. 가장 작은 2×2는 2·2·2 = 8, 가장 큰 5×5는 5·5·5 = 125다. 같은 면적 한 칸을 두고 경쟁할 때 큰 우주선이 압도적으로 비싸다. 게다가 큰 우주선은 모서리 간격이 넓어 — 높이차 ≤ 6 제약을 만족할 평탄한 영역을 찾기가 더 어렵다. 즉 큰 우주선은 점수는 높고 자리는 까다롭다. 까다로운 것을 먼저, 자리가 넉넉할 때 앉혀야 한다.

그래서 베스트 해법은 두 단계로 설계된다.

  • 1단계 — 평탄도 전처리. 1000×1000 지형에서 "여기에 우주선을 놓을 수 있는가"를 모든 위치·모든 크기에 대해 빠르게 판정할 수 있도록, 지역 최대/최소 높이를 미리 계산한다.
  • 2단계 — 큰 우주선 우선 그리디 배치. 우주선을 점수 내림차순(= 크기 내림차순)으로 정렬하고, 차례로 "회전 두 방향 중 들어가는 자리"를 찾아 앉힌다. 자리를 못 찾으면 -1로 남기고 다음 우주선으로.

핵심 통찰은 두 단계가 서로 다른 자원을 다룬다는 것이다. 1단계는 "어디가 평탄한가"라는 지형 정보를 만들고, 2단계는 "비싼 것부터 그 정보를 소비"한다. 정보 생성과 소비의 분리는 № 01·№ 09의 설계와 같은 골격이다.

핵심 통찰 PENALTY가 0인 점수 최대화 문제에서 "모두 배치"는 목표가 아니다. 자원(평탄한 자리)이 제한적일 때는 단위 자원당 점수가 가장 높은 객체부터 배치하라. 우주선착륙에서 그것은 큰 우주선이며, 큰 우주선은 동시에 자리 제약도 가장 까다로워 — "비싸고 까다로운 것 먼저"라는 그리디 순서가 두 이유로 정당화된다.

§ 2정당성 — 큰 우주선 우선 그리디는 왜 좋은 해를 주는가

설계의 정당성을 논증하려면, 먼저 이 그리디가 만드는 해가 어떤 의미에서 "좋은가"를 정의해야 한다. 전역 최적은 보장하지 못한다 — 직사각형 패킹은 NP-난해다. 그러나 두 가지 보조정리로 "왜 큰 것 먼저인가"를 받칠 수 있다.

Invariant배치 후 점유 배타성

2단계 루프가 우주선 하나를 배치할 때마다 다음이 성립한다: 지금까지 배치된 모든 우주선의 점유 직사각형은 쌍쌍이 겹치지 않는다.

이를 유지하려면 배치 후보를 검사할 때 점유 비트맵 occupied[][]를 함께 본다 — 후보 직사각형 안에 이미 점유된 칸이 하나라도 있으면 그 후보를 기각한다. 채점기 verify()도 같은 검사를 하므로(겹침 시 return false), 이 불변식을 깨면 전체 TC가 PENALTY로 0점이 된다 — verify 실패는 break로 채점을 끝낸다.

Theorem 1큰 우주선 우선은 후회를 최소화한다

우주선을 점수 내림차순으로 배치하면, 각 시점에서 "지금 놓는 우주선보다 비싼 우주선은 모두 이미 자리를 얻었다"는 성질이 유지된다. 따라서 어떤 우주선이 자리를 못 얻어 -1로 버려질 때, 그 우주선보다 비싼 우주선이 그 자리 때문에 밀려난 경우는 없다. 버려지는 것은 항상 남은 것 중 가장 싼 것들이다.

Proof교환 논법

반대로 작은 우주선을 먼저 배치하는 순서를 생각하자. 작은 우주선 s가 어떤 평탄 영역 R을 점유했고, 나중에 큰 우주선 LR을 포함하는 자리만 남아 배치에 실패했다고 하자. sL을 바꿔 — L을 먼저 R에 놓고 s를 다른 작은 틈에 놓으면(작은 우주선은 틈이 많다), 점수 합은 score(L) − score(s) > 0만큼 증가한다.

즉 "작은 것 먼저" 순서에는 항상 점수를 높이는 교환이 존재한다. 그런 교환이 더 이상 불가능한 순서가 곧 "큰 것 먼저"다. 이것은 그리디 교환 논법의 표준 형태로, 결과 해가 전역 최적임을 보장하지는 않지만 — 순서에 관한 한 후회 없는 선택임을 보인다.

높이차 제약이 "평탄도"로 환원되는 이유

제약 ②는 미묘하다. verify()는 우주선이 덮는 직사각형의 네 모서리만 본다 — 내부 지형은 무시한다. max 네 칸 − min 네 칸 ≤ 6이면 통과다. 그러므로 "평탄한 영역"이란 영역 전체가 평평할 필요가 없고, 네 꼭짓점의 높이 편차만 작으면 된다. 이 관찰이 전처리를 가능하게 한다.

Lemma모서리 4점 판정의 O(1)화

한 위치 (y1,x1)와 크기 (h,w)가 주어지면 네 모서리는 (y1,x1), (y1,x1{+}w{-}1), (y1{+}h{-}1,x1), (y1{+}h{-}1,x1{+}w{-}1)로 즉시 계산된다. 지형 높이 배열을 한 번 복사해 두면, 한 후보의 제약 ② 판정은 네 번의 배열 접근 + max/min, 즉 O(1)이다.

따라서 전처리는 사실 필요 없을 만큼 판정 자체가 싸다 — 진짜 비용은 "어느 후보를 검사할 것인가"의 탐색 횟수에 있다. 그래서 §3의 설계는 전처리보다 탐색 가지치기에 무게를 둔다.

§ 3핵심 알고리즘 — 비용 모델과 배치 탐색 설계

이 문제에는 호출 비용이 없다 — process()가 끝난 뒤 verify()가 한 번에 채점한다. 따라서 "비용"은 점수 함수 그 자체이고, 설계의 비용 모델은 다음과 같다.

최대화 목표: SCORE = Σ_{i 배치됨} h_i · w_i · min(h_i, w_i) 탐색 비용(런타임): 10 TC × Σ_{우주선} (검사한 후보 수) × O(1) 판정 순진한 전수탐색: 10⁴ 우주선 × 10⁶ 위치 × 2 회전 = 2×10¹¹ ← 너무 느림 가지치기 목표: 후보 수를 우주선당 평균 수백 이하로 cost model

설계의 핵심 결정은 "한 우주선의 자리를 어떻게 찾는가"다. 100만 위치를 매번 전수탐색하면 10 TC를 시간 안에 못 끝낸다. 두 가지 가지치기를 제안한다. 설계 제안

  • 스캔라인 + 점유 스킵. 위치를 행 우선으로 훑되, 이미 점유된 칸을 만나면 그 칸 폭만큼 건너뛴다. 격자가 채워질수록 빈 칸이 줄어 후보가 자연히 감소한다.
  • 높이차 조기 기각. 모서리 4점 max−min을 가장 먼저 검사한다. 무작위 지형(0~124 균등)에서 편차 ≤ 6을 만족하는 4점 조합은 드물어, 대부분의 후보가 이 한 줄에서 걸러진다.

아래는 한 우주선의 자리를 찾는 핵심 루틴의 설계 스케치다. 실제 통과 코드가 아니라 — verify의 제약에서 연역한 의사코드에 가까운 C++다.

설계 스케치 · placeOne() — 한 우주선 자리 찾기design proposal — not verified
// 우주선 i를 배치 시도. 성공하면 rows/cols/dirs에 기록하고 true.bool placeOne(int i, int H, int W) {    for (int dir = 0; dir < 2; ++dir) {        // 회전 두 방향        int h = dir ? W : H;                  // dir=1이면 90도 회전        int w = dir ? H : W;        for (int y = 0; y + h <= MAP_SIZE; ++y) {            for (int x = 0; x + w <= MAP_SIZE; ++x) {                // ① 높이차 — 모서리 4점만, 가장 먼저 기각                int a = mapH[y][x],         b = mapH[y][x+w-1];                int c = mapH[y+h-1][x],     d = mapH[y+h-1][x+w-1];                int hi = max(max(a,b), max(c,d));                int lo = min(min(a,b), min(c,d));                if (hi - lo > 6) continue;                // ② 겹침 — 점유 비트맵 검사 (구간합으로 O(1)화 가능)                if (anyOccupied(y, x, h, w)) continue;                // 통과 — 점유 표시하고 답 기록                markOccupied(y, x, h, w);                rows[i] = y; cols[i] = x; dirs[i] = dir;                return true;            }        }    }    return false;                          // 자리 없음 — i는 -1로 남는다}
설계 노트 — 회전의 역할 회전(dir)은 점수를 바꾸지 않는다 — verify()는 회전 후에도 spaceship_org[i].height·width·min으로 점수를 매기므로 h·w·min(h,w)는 불변이다. 회전의 가치는 오직 배치 가능성에 있다. 3×5 우주선이 가로로는 안 들어가는 평탄대(帶)에 세로로는 들어갈 수 있다. 그래서 설계는 회전 두 방향을 모두 시도하며, 한쪽이라도 들어가면 그 자리를 쓴다. anyOccupied는 2차원 구간합(prefix sum)으로 O(1)에 만들 수 있으나, 점유가 갱신될 때마다 구간합을 다시 짓는 비용이 있어 — 격자가 듬성할 때는 단순 스캔이 더 쌀 수 있다. 설계 제안
Theorem 2복잡도 — 가지치기가 결정한다

순진한 전수탐색은 우주선당 2 · 10⁶ 후보, 10⁴ 우주선, 10 TC로 2×10¹¹ — 시간 초과다. 그러나 높이차 조기 기각이 거의 모든 후보를 첫 줄에서 제거한다. 무작위 지형에서 모서리 4점이 모두 폭 6 안에 드는 확률은 작고(대략 수 % 수준), 첫 평탄 자리를 만나면 즉시 return하므로 — 우주선당 기대 후보 수는 위치 공간을 끝까지 훑지 않고 훨씬 일찍 끝난다. 격자가 채워질수록 큰 우주선은 자리를 못 찾아 일찍 실패하고, 작은 우주선은 틈이 많아 금방 자리를 찾는다. 실측 없이 정확한 상수는 말할 수 없으나, "조기 기각 + 첫 적합 즉시 채택"의 조합이 시간 예산 안에 드는 것이 설계의 전제다. 설계 제안

§ 4심화 — PASS 기준의 여유와 "첫 적합 vs 최선 적합"

PASS 3,000,000은 얼마나 빡빡한가

한 TC에 우주선 10,000개가 있다. 크기는 2~5 균등이므로 평균 점수는 대략 — h,w가 각각 {2,3,4,5} 균등일 때 E[h·w·min(h,w)]는 약 40 안팎이다. 10 TC 누적이면 우주선이 모두 배치될 때의 점수 상한은 대략 10 · 10⁴ · 40 = 4×10⁶ 규모다. PASS 기준 3,000,000은 그 상한의 약 75%다.

이 비율이 설계에 주는 메시지는 분명하다 — 모든 우주선을 배치할 필요는 없지만, 큰 우주선 대부분은 반드시 앉혀야 한다. 큰 우주선(5×5 = 125점)을 충분히 못 앉히면 75%에 못 미친다. 그래서 §1의 "큰 것 우선" 순서는 단순한 휴리스틱이 아니라 PASS의 필요조건에 가깝다.

Invariant배치 실패는 회복 불가능하지 않다

한 우주선이 placeOne에서 실패해 -1로 남아도 — verify()는 그 우주선을 건너뛸 뿐 전체를 실패시키지 않는다(PENALTY = 0). 따라서 그리디는 "되돌리기(backtracking)" 없이 진행해도 안전하다. 한 우주선의 실패가 다른 우주선의 점수를 깎지 않는다.

이것이 설계를 단순하게 만든다. 직사각형 패킹의 일반형은 백트래킹이 필요하지만, 이 문제는 "놓을 수 있으면 놓고, 없으면 포기"라는 단조 진행만으로 충분하다 — 포기에 페널티가 없기 때문이다.

첫 적합(first-fit) vs 최선 적합(best-fit)

§3의 스케치는 첫 적합이다 — 들어가는 첫 자리를 즉시 쓴다. 대안은 최선 적합 — 모든 가능한 자리를 보고 "가장 빈 공간을 적게 낭비하는" 자리를 고른다. 직관적으로 best-fit이 더 좋아 보이지만, 두 가지 이유로 first-fit을 권한다. 설계 제안

  • 시간. best-fit은 첫 적합에서 멈추지 못하고 위치 공간을 끝까지 훑어야 한다 — Theorem 2의 "첫 적합 즉시 채택" 이점이 사라진다.
  • 이득의 불확실성. 1000×1000은 우주선 10,000개(평균 면적 ~12칸 = 12만 칸)에 비해 매우 넓다. 격자의 점유율이 낮으면 — 빈 공간 낭비를 줄이는 best-fit의 이점이 거의 나타나지 않는다. 자리가 부족한 게 아니라 평탄한 자리가 부족한 것이고, 그 부족은 best-fit으로 해결되지 않는다.

다만 스캔 순서를 살짝 흔드는 것 — 예컨대 큰 우주선을 격자 중앙 근처부터, 작은 우주선을 가장자리부터 탐색 — 은 평탄 영역을 큰 우주선에 양보하는 효과가 있어 검토할 가치가 있다.

§ 5대안 비교 — 왜 이 설계인가

배치 전략별 트레이드오프 (1000×1000 지형 · 10,000 우주선 · 점수 최대화)
전략겹침안전점수 경향시간평가
입력 순서대로 첫 적합안전중간빠름작은 우주선이 평탄대를 선점 — 큰 우주선 손해
작은 우주선 우선안전낮음빠름Theorem 1 위배 — 점수 높은 교환이 항상 남음
완전 직사각형 패킹 최적안전최대불가NP-난해 — 10⁴ 우주선 규모에서 시간 초과
큰 우주선 우선 + 회전 + 높이차 조기기각안전≥ 3,000,000 목표중간비싼 것 먼저 + 자리 까다로운 것 먼저 + 빠른 판정

표의 구조는 §1의 분해를 비춘다. 겹침 안전은 점유 비트맵만 지키면 어느 전략이든 공짜로 얻는다(Invariant). 진짜 경쟁은 점수에서 벌어진다. 입력 순서·작은 것 우선은 Theorem 1의 교환 논법으로 항상 개선 여지가 남고, 완전 패킹 최적은 NP-난해라 10,000개 규모에서 불가능하다. 따라서 "큰 것 우선 그리디 + 회전 + 높이차 조기 기각"이 — 통과 코드가 없는 상태에서도 verify의 제약으로부터 연역할 수 있는 현실적 최선이며, Theorem 1이 그 순서의 후회 없음을 받친다.

§ 6설계 함정과 검증 체크리스트

  1. 회전 시 height/width 교환 누락 verify()dirs[i]==1일 때 y2 = y1 + width - 1, x2 = x1 + height - 1로 — height와 width를 맞바꿔 점유 직사각형을 계산한다. 배치 검사에서 회전을 적용하면서 채점기와 같은 교환을 하지 않으면, 검사한 자리와 실제 점유 자리가 어긋나 겹침 PENALTY를 맞는다.
  2. 내부 지형으로 높이차 판정 제약 ②는 직사각형 네 모서리만 본다. 내부 칸까지 검사하면 멀쩡한 자리를 불필요하게 기각해 점수를 잃는다. 반대로 verify는 내부를 안 보므로, 내부가 울퉁불퉁해도 모서리만 평탄하면 배치는 유효하다.
  3. 미배치 우주선의 -1 초기값 훼손 make_tc()는 모든 우주선의 rows/cols/dirs-1로 초기화한다. 자리를 못 찾은 우주선은 이 -1그대로 두어야 verify()가 건너뛴다. 실수로 0 등을 써넣으면 (0,0)에 배치된 것으로 해석돼 겹침·경계 위반을 일으킨다.
  4. 경계 검사 부호 실수 우주선 끝 좌표는 y + h - 1이다. 루프 조건은 y + h ≤ MAP_SIZE(끝 인덱스 y+h-1 ≤ 999)여야 한다. -1을 빼먹으면 한 칸 넘쳐 verify()의 경계 검사(y2 ≥ MAP_SIZE)에 걸린다.
  5. 크기 정렬 키를 면적으로 잡음 정렬 키는 면적 h·w가 아니라 점수 h·w·min(h,w)여야 한다. 예: 2×5(면적 10, 점수 20)와 4×4(면적 16, 점수 64)는 면적 순과 점수 순이 다르다. 그리디가 진짜로 최적화하는 양은 점수다.

검증 — 무엇으로 통과를 판정하는가

이 문제는 통과 코드가 없으므로 측정된 SCORE를 제시할 수 없다. 대신 채점기의 판정 구조를 명시한다. verify()가 한 우주선이라도 제약 ①②③을 어기면 SCORE = PENALTY(0)break — 전체 0점이다. 모두 통과하면 배치된 우주선 점수 합이 누적되고, 10 TC 합이 3,000,000 이상이면 PASS다.

목적함수Σ h·w·min(h,w)
PASS 기준 (10 TC 합)≥ 3,000,000
측정 SCORE미보유 (통과 코드 없음)
판정설계 목표 — PASS
정직성 고지 이 페이지의 §3~§4 알고리즘은 검증된 통과 코드가 아니라, 공개된 verify()·make_tc()로부터 연역한 설계다. placeOne() 스케치는 컴파일·실측을 거치지 않았으며, Theorem 2의 시간 복잡도는 무작위 지형의 평탄 영역 분포에 대한 추정에 의존한다. "큰 우주선 우선"과 "회전 두 방향"은 verify의 제약에서 안전하게 연역되지만, 정확한 SCORE는 실제 구현·실행으로만 확정된다.

관련같은 원리를 쓰는 문제