Codepass Expert Training § 02 · Constraint Inference
Training 목차 C++ only
TRAINING § 02 — N=64만 보고도 데이터를 상상하는 30초

시드를 안 돌려도 데이터는 보인다. 제약이 분포를 미리 적어놓았다.

§01은 printf로 데이터를 봤다. 그러나 진짜 빠른 응시자는 출력 한 줄을 찍기 전에 이미 분포를 안다. static const int N = 64, % MAP_SIZE, % 100 + 1 — 이 세 줄이 격자 크기, 좌표 범위, 무게 분포를 전부 확정한다. 데이터를 만드는 코드는 데이터의 명세서다. 이 페이지는 그 명세서를 읽고, 시험 시작 30초 안에 알고리즘 등급을 판정하는 추론 기술이다.

4 checklists · 제약 → 분포 N값 함의표 PASS 역산 공식 30-sec quick-ref

§ 1전제 — 제약은 데이터의 명세서다

Expert 시험은 입력 파일을 주지 않는다. 대신 main.cpp 안의 채점기가 pseudo_rand()로 데이터를 생성한다. 그런데 생성 코드는 자유롭게 쓰여 있지 않다 — 거의 모든 값이 pseudo_rand() % N + K라는 한 줄짜리 공식으로 만들어진다. 그 공식의 NK를 읽으면, 시드를 한 번도 돌리지 않고도 그 변수의 범위·분포·평균·표준편차가 전부 확정된다.

이것이 §01과 §02의 결정적 차이다. §01은 데이터를 관측한다 — printf를 꽂고 출력 창을 본다. §02는 데이터를 연역한다 — 코드만 읽고 분포를 머릿속에 그린다. 관측은 정확하지만 느리고(컴파일·실행·읽기 30초~1분), 연역은 근사적이지만 즉각적이다(읽는 즉시). 시험장에서 둘 다 쓴다: 먼저 연역으로 30초 안에 알고리즘 대분류를 정하고, 그 다음 §01의 루틴으로 가설을 확인한다.

main.cpp · 제약이 곧 명세인 전형적 생성부로봇청소기 채점기 발췌
static const int N = 64;            // ← 맵은 64x64 격자로 고정static const int TC = 10;            // ← 테스트 케이스 10개static int grid[N][N];               // ← 4096칸. 전체 순회해도 4096번static void make_tc() {    for (int y = 0; y < N; y++)        for (int x = 0; x < N; x++)            grid[y][x] = pseudo_rand() % 3;   // ← 셀 값 0,1,2 균등    dust = pseudo_rand() % 300 + 100;     // ← 먼지 100~399개}
이 6줄에서 읽히는 것 맵은 64×64=4096칸 — 전체를 매 호출마다 순회해도 4096번뿐이다. 셀 값은 % 3이라 0·1·2가 각 33%씩 균등하게 섞인다. 먼지 개수는 % 300 + 100이라 100~399 균등, 평균 약 250. TC는 10개. — 시드를 한 번도 안 돌렸지만 데이터의 모양이 이미 손에 잡힌다.
발상의 전환 "데이터를 모른다"는 말은 시험장에서 거짓이다. 정확히는 "데이터의 난수 시퀀스를 외우지 못한다"일 뿐이고, 그건 알 필요도 없다. 알고리즘 등급을 정하는 데 필요한 건 개별 값이 아니라 분포의 성질 — 범위, 평균, 균등성, 개수 — 이고 그건 전부 제약 상수에 적혀 있다. 제약을 읽는 것은 데이터를 읽는 것이다.

§ 2체크리스트 ① 제약 읽기 — 상수에서 분포를 즉시 연역한다

첫 번째 체크리스트는 make_tc()를 한 줄씩 훑으며 각 pseudo_rand() 표현식을 분포 명세로 번역하는 작업이다. 패턴은 네 가지뿐이고, 각각 읽는 법이 정해져 있다.

패턴 1pseudo_rand() % N — [0, N) 균등 분포

가장 흔한 형태. 값이 0부터 N-1까지 각 1/N 확률로 균등하게 나온다. 좌표(% MAP_SIZE), 인덱스(% cnt), 분류값(% 3)이 전부 이 꼴이다. 평균은 (N-1)/2, 표준편차는 N/√12 ≈ 0.289·N. 균등이라는 사실 자체가 중요하다 — "데이터가 어느 영역에 몰려 있을 것"이라는 가정을 금지한다.

패턴 2pseudo_rand() % N + K — [K, K+N) 로 평행이동된 균등

% 100 + 11~100, % 300 + 100100~399. + K는 분포의 모양은 그대로 두고 시작점만 옮긴다. 무게·비용·재고처럼 "0이 되면 안 되는 양"에 + 1이 붙는 게 전형. 범위 폭은 여전히 N, 평균은 K + (N-1)/2.

패턴 3pseudo_rand() % A + pseudo_rand() % B — 합 분포(삼각/정규 근사)

두 균등 난수의 합은 더 이상 균등이 아니다 — 가운데가 솟은 삼각형 분포가 된다. 셋 이상을 더하면 종 모양(정규 근사). 무게가 % 50 + % 50 같은 꼴이면 평균값 근처에 데이터가 몰려 있다는 뜻이고, 그건 정렬·중앙값 트릭의 공략점이 된다. +가 두 개 이상 보이면 균등 가정을 버려라.

패턴 4do { … } while (중복) — 중복 제거된 균등

좌표나 ID를 뽑은 뒤 do-while로 "이미 쓴 값이면 다시 뽑기"를 하는 코드. 결과는 여전히 거의 균등하지만 중복이 없다는 보장이 추가된다. 이게 보이면 "두 점이 같은 칸에 겹칠 일은 없다"는 전제를 풀이에 쓸 수 있다 — 충돌 처리 코드를 통째로 생략할 근거다.

main.cpp · 네 패턴이 한 함수에 모인 예물류배송 채점기 발췌
static void make_tc() {    centerCnt = pseudo_rand() % 500 + 501;       // 패턴2: 501~1000    orderCnt  = pseudo_rand() % 500000 + 500001; // 패턴2: 50만~100만    for (int i = 0; i < orderCnt; i++) {        do {            order[i].x = pseudo_rand() % MAP;       // 패턴1+4: 균등, 중복없음            order[i].y = pseudo_rand() % MAP;        } while (occupied(order[i].x, order[i].y));        order[i].stock = pseudo_rand() % 50                       + pseudo_rand() % 50 + 2;   // 패턴3: 삼각, 2~100, 평균~51    }}
한 함수에서 읽어낸 명세 센터는 501~1000개 균등, 주문은 50만~100만개 균등(이 자릿수가 알고리즘 등급을 가른다 — §3 참조), 좌표는 중복 없는 균등 산포, 재고는 2~100 삼각 분포로 평균 51 근처에 몰림. 이 네 줄을 읽는 데 15초, 그리고 풀이의 절반이 이미 결정됐다.
함정 — % 다음의 +1을 놓치는 것 % 1000~99, % 100 + 11~100이다. + 1 하나로 "값이 0일 수 있는가"가 갈린다. 0이 가능하면 0 무게·0 비용·0 거리 같은 경계 케이스를 풀이에서 다뤄야 하고, + 1이 있으면 그 코드를 생략할 수 있다. 제약을 읽을 때 % 뒤의 상수만 보지 말고 + 뒤까지 반드시 읽어라.

§ 3체크리스트 ② N값 함의 — 크기가 알고리즘 복잡도를 가둔다

두 번째 체크리스트는 가장 강력하다. 데이터의 개수(맵 크기·원소 수)를 보면, 어떤 시간복잡도까지 허용되는지가 거의 자동으로 결정된다. 시험 채점기는 보통 한 TC당 수백 ms~수 초의 예산을 준다. 현대 CPU는 1초에 약 108~109회의 단순 연산을 처리한다 — 이 수치를 기준으로 N이 복잡도 상한을 가둔다.

데이터 개수 N허용 복잡도가능한 알고리즘대표 문제
N ≤ 100O(N³), O(2N) 부분완전탐색, 플로이드–워셜, DP 전부, 백트래킹드론 배치(N≈50)
N ≤ 5,000O(N²), O(N²·logN)모든 쌍 비교, 삽입정렬, DP 2차원, 이중 그리디로봇청소기(맵 4096칸)
N ≤ 105O(N·logN), O(N√N)정렬+그리디, 이진탐색, 세그먼트 트리, 우선순위 큐택시 배정(승객 ~1만)
N ≤ 106O(N·logN) 빠듯, O(N) 안전해시·카운팅 정렬, 투 포인터, 선형 스캔, BFS/DFS물류배송(주문 50만~100만)
N ≥ 107O(N) 필수, 상수도 작게한 번 스캔, 비트 연산, 캐시 친화적 순회만대규모 스트림형 문제
표를 쓰는 법 제약에서 가장 큰 N을 찾아(보통 좌표 개수나 원소 수) 이 표의 행을 고른다. 그 행의 "허용 복잡도"보다 비싼 알고리즘은 시간 초과로 죽는다. 예: 물류배송은 주문이 최대 100만 — O(N²) 모든 쌍 비교(1012회)는 절대 불가, O(N·logN)(약 2천만회)이 한계, O(N)이 안전. 그러므로 "주문을 정렬 후 그리디"는 가능하지만 "주문끼리 다 비교"는 시작도 하면 안 된다.
불변맵 크기 vs 원소 개수를 구분하라

두 N이 흔히 동시에 등장한다. 맵 크기(MAP_SIZE)는 좌표가 사는 공간의 변, 원소 개수(orderCnt 등)는 그 공간에 놓인 점의 수. 복잡도를 가두는 건 원소 개수이지 맵 크기가 아니다. 택시의 MAP_SIZE=106는 좌표 범위일 뿐 — 승객이 1만 명이면 알고리즘은 O(승객수) 기준으로 짠다. 단 맵 크기는 배열 메모리를 결정한다: int grid[106][106]는 메모리 폭발이므로, 큰 맵에서는 격자 배열 대신 좌표 리스트로 다뤄야 한다.

핵심 N값 함의표는 "무엇을 할 수 있는가"가 아니라 "무엇을 하면 안 되는가"를 먼저 알려준다. 시험장에서 가장 비싼 실수는 30분간 O(N²) 알고리즘을 완성한 뒤 시간 초과를 보는 것이다. N을 먼저 읽으면 그 30분이 시작되기 전에 멈춘다. 표의 행을 고르는 데 5초, 그 5초가 30분을 산다.

§ 4체크리스트 ③ TC 개수 전략 — 5개냐 30개냐 100개냐

세 번째 체크리스트는 TC 상수다. 채점기는 한 시드에 대해 여러 테스트 케이스를 돌리고, 전체 TC의 합산 시간이 제한 안에 들어야 한다. TC가 몇 개냐가 "TC 하나에 쓸 수 있는 시간"을 직접 나눈다.

TC 개수TC당 시간 예산(총 5초 가정)전략적 함의
TC ≤ 5약 1초 / TCTC당 무거운 연산 가능. 한 TC가 크고 복잡한 단일 인스턴스 — 정밀한 알고리즘을 써도 된다. 초기화 비용이 커도 5번이면 흡수된다.
TC ≈ 20~30약 150~250ms / TC중간. TC마다 가벼운 초기화 + 효율적 본체. 매 TC memset 같은 큰 배열 초기화가 누적되니 초기화 비용도 예산에 넣어라.
TC ≥ 100약 50ms 이하 / TC빡빡. TC당 알고리즘은 거의 O(N)~O(N·logN)만 허용. 전역 배열 재초기화도 부담 — 필요한 칸만 되돌리는 부분 초기화가 필요할 수 있다.
main.cpp · TC 루프와 시간 분배채점기의 전형적 TC 구조
int main() {    long long totalScore = 0;    for (int tc = 0; tc < TC; tc++) {   // ← TC만큼 반복        make_tc();                       // 데이터 생성        init(...);                       // user 초기화 — 매 TC 호출        while (remainTime()) process(...); // user 본체 — 매 TC 호출        totalScore += scoreOfThisTC();   // TC 점수 누적    }    printf("SCORE: %lld\n", totalScore);  // ← 최종 합산 점수}
읽는 법 mainfor (tc < TC)를 찾으면 TC 개수가 보인다. 중요한 건 init매 TC마다 호출된다는 점 — init 안에서 큰 배열을 memset하면 그 비용에 TC를 곱한 만큼이 든다. TC=100에 memset으로 106칸을 매번 0으로 채우면 108회 — 그 자체로 1초를 먹는다. TC가 많을수록 초기화를 가볍게 하라.
실전 인용 택시 배정TC=5 — TC당 1초씩 넉넉하니 승객 1만 명에 대한 정밀한 매칭을 돌릴 여유가 있다. 로봇청소기TC=10 — 맵이 4096칸으로 작아 TC당 부담이 거의 없다. 물류배송은 TC가 적지만 한 TC의 주문이 100만 개라 TC 내부가 무겁다 — TC 개수가 적어도 인스턴스가 크면 빡빡할 수 있으니 TC 개수와 인스턴스 크기를 함께 봐야 한다.

§ 5체크리스트 ④ PASS 기준 역산 — CUT 점수에서 허용 비용을 거꾸로 푼다

네 번째 체크리스트는 가장 정량적이다. 채점기는 보통 CUT(통과 기준 점수)을 코드 어딘가에 명시한다. 그 점수와 데이터 규모를 결합하면, "한 단위 작업당 허용되는 평균 비용"을 역산할 수 있다. 그 허용 비용이 알고리즘 등급을 판정한다 — 그리디로 충분한가, 정밀 최적화가 필요한가.

PASS 역산 공식

주어진 것: 통과 기준 점수 CUT, 처리할 단위 개수 K(주문 수·승객 수·청소 칸 수 등)

① 점수가 비용 합이고 낮을수록 좋은 문제 (이동거리·낭비·지연):
    허용 평균 비용 = CUT / K
    → 내 알고리즘의 단위당 평균 비용이 이 값보다 작으면 PASS

② 점수가 처리량이고 높을수록 좋은 문제 (배송 완료 수·점수 적립):
    필요 단위 처리율 = CUT / (전체 가능 단위)
    → 내 알고리즘이 이 비율 이상을 처리하면 PASS

③ 시간 한계 역산 (TC당 시간 T초, 단위 K개):
    단위당 허용 연산 = T · 10⁸ / K
    → 이 값이 logK보다 크면 정렬·이진탐색 여유, 1에 가까우면 O(N) 외엔 불가
예제 — 로봇청소기 (N=64, TC=10, CUT=6.32M)

맵 4096칸, 먼지 평균 250개. 점수가 "총 이동 비용 합"이고 낮을수록 좋다고 하자.
전체 점수 6.32M를 TC 10으로 나누면 TC당 ≈ 632,000.
한 TC에서 청소할 먼지가 평균 250개라면 먼지당 허용 이동 = 632,000 / 250 ≈ 2,528.
그런데 맵은 64×64 — 두 점의 최대 맨해튼 거리는 2·63 = 126. 평균 쌍거리는 약 42.
판정: 먼지당 2,528은 평균거리 42의 60배 — 엄청나게 헐겁다. 단순 그리디(가장 가까운 먼지부터)로도 먼지당 평균 60~80이면 충분히 PASS. 정밀 TSP는 불필요. 역산이 "최적화 노력을 아껴라"라고 말한다.
예제 — 택시 배정 (MAP=10⁶, TC=5, CUT=432.5M)

승객 5,001~10,000명, 맵 한 변 106. 점수는 "승객 대기·이동 비용 합", 낮을수록 좋다.
432.5M / 5 TC = TC당 ≈ 86.5M. 승객 평균 7,500명이면 승객당 허용 ≈ 11,533.
맵이 106이라 평균 쌍거리는 약 0.5·10⁶ ≈ 500,000.
판정: 승객당 허용 11,533은 평균거리 50만의 1/43. 즉 승객을 가장 가까운 택시에 붙여야만 PASS — 무작위·순서대로 배정하면 즉사. 거리 기반 그리디가 필수이고, swap 개선(§03)으로 더 짜내야 안전권. 역산이 "정밀하게 풀어라"라고 말한다.
예제 — 물류배송 (센터 ~1000, 배송 100만, CUT=13B)

배송 50만~100만 건, 점수는 "총 배송 거리 합", 낮을수록 좋다.
CUT 13B(=13×109)를 배송 100만으로 나누면 배송당 허용 ≈ 13,000.
센터가 1000개로 촘촘하면, 한 배송지에서 가장 가까운 센터까지의 평균 거리는 맵 밀도에 따라 수천 단위.
판정: 배송당 13,000은 "가장 가까운 센터 배정"이면 닿지만 여유가 크지 않다 — 센터 선택 임계값(TH) 튜닝이 점수를 가른다(§03의 파라미터 스윕 사례). 역산이 "닿긴 하나 튜닝으로 마진을 확보하라"라고 말한다.
역산의 의미 PASS 역산은 알고리즘을 짜기 전에 "얼마나 잘 풀어야 하는가"를 숫자로 못박는다. 세 가지 판정만 나온다 — 헐겁다(그리디로 충분, 최적화 절약), 빡빡하다(정밀 알고리즘 + 개선 루프 필수), 닿는다(기본기 + 파라미터 튜닝). 이 판정이 §03 개선 전략에 얼마나 시간을 쏟을지를 결정한다. CUT을 못 찾으면? 채점기의 scoreOfThisTC()나 등급 판정 if문을 읽어라 — 거기 숫자가 박혀 있다.

§ 6pseudo_rand() % N 의 분포 성질 — 외워야 할 다섯 숫자

네 체크리스트의 바탕에는 한 가지 수학적 사실이 있다 — pseudo_rand() % N이산 균등 분포를 만든다. 이 분포의 성질을 외우면, 제약을 보는 즉시 평균·표준편차를 암산할 수 있다.

보조정리이산 균등 분포 U[0, N) 의 다섯 수

X = pseudo_rand() % N이 0~N-1을 각 1/N로 균등하게 가질 때:

  • 최솟값 = 0,  최댓값 = N-1,  범위 = N-1 ≈ N
  • 평균 E[X] = (N-1)/2 ≈ N/2
  • 분산 Var[X] = (N²-1)/12 ≈ N²/12
  • 표준편차 σ = √(N²/12) = N/√12 ≈ 0.289·N
  • 중앙값 ≈ 평균 (대칭 분포이므로)
표준편차 = 범위 / √12 — 이것만 외우면 된다

σ = 범위 / √12,  √12 ≈ 3.464,  따라서 σ ≈ 범위 × 0.289

예: % 300 + 100 → 범위 300, σ ≈ 300/3.464 ≈ 87, 평균 ≈ 250
즉 먼지 개수는 "250 ± 87" 정도로 흩어진다 — 대부분 163~337 사이.

왜 쓸모있나: §01의 summary() 루틴이 출력한 sd 값과 이 이론값을 비교하면, 데이터가 정말 균등인지 검증된다. 실측 σ가 이론 σ와 맞으면 균등 확정, 작으면 값이 뭉친 것(합 분포·필터링 의심).
증명 스케치두 균등 난수의 합은 왜 삼각형인가

X, Y ~ U[0, N)이 독립일 때 S = X + Y의 분포를 보자. 합이 s가 되는 (x, y) 쌍의 수는 x + y = s를 만족하는 격자점 수다. s가 0이나 2(N-1)에 가까우면 그런 쌍이 적고(모서리), s ≈ N-1이면 가장 많다(대각선이 가장 김). 그래서 가능한 쌍의 수가 가운데에서 최대인 삼각형 분포가 된다. 셋을 더하면(중심극한정리의 맛보기) 종 모양으로 매끄러워진다. — 그러므로 제약에서 +가 두 개 이상 보이면 "가운데에 몰린 데이터"를 예상하고, 그 가운데값 근처를 공략하는 정렬·중앙값 전략을 준비하라.

do-while 중복제거의 분포 효과 do { v = rand() % N; } while (used[v]);로 K개를 뽑으면, 분포는 여전히 거의 균등하지만 비복원 추출이 된다. K가 N에 비해 작으면(K ≪ N) 균등 가정 그대로 써도 되고, K가 N에 근접하면(예: N칸에서 0.9N개를 뽑음) 후반부 추출이 점점 "남은 칸"으로 몰려 약간의 편향이 생긴다. 대부분의 Expert 문제는 K ≪ N이라 균등으로 다뤄도 안전하다 — 단 "두 점이 절대 겹치지 않는다"는 보장은 확실히 챙겨라.

§ 730초 퀵레퍼런스 — 시험 시작 직후 훑는 표

시험이 시작되면 main.cpp가 화면에 뜬다. 코드를 풀이하기 전, 30초 동안 이 표 순서대로 훑는다. 각 칸의 답을 종이나 주석에 한 줄로 적어두면, 그 다섯 줄이 알고리즘 설계의 골격이 된다.

찾을 것코드에서 보는 위치적어둘 결론
0–5s가장 큰 Nstatic const int, 전역 배열 크기, cnt 변수의 % 범위"N ≈ ___ → 허용 복잡도 ___"
5–10sTC 개수mainfor (tc < TC)"TC ___개 → TC당 ___예산"
10–20s각 변수 분포make_tc()의 모든 pseudo_rand() % … + …"좌표 균등 / 무게 삼각 / 중복없음 ___"
20–25sCUT 점수scoreOfThisTC, 등급 판정 if, CUT 상수"CUT ___ → 단위당 허용 ___"
25–30s한 줄 판정위 네 줄을 종합"이 문제는 ___형 + ___알고리즘 등급"
완성된 30초 메모의 예 — 물류배송
① N: 배송 최대 100만 → O(N·logN) 한계, O(N) 안전, O(N²) 금지
② TC: 적지만 인스턴스가 큼 → TC 내부가 무거움, 초기화 가볍게
③ 분포: 좌표 중복없는 균등 산포 / 재고 삼각(평균 51) / 센터 501~1000 균등
④ CUT 13B / 배송 100만 → 배송당 허용 ≈ 13,000 (닿지만 마진 작음)
판정: 산포형 + 거리 기반 그리디 필수 + 센터 선택 임계값 튜닝으로 마진 확보
함정 — 추론을 맹신하는 것 제약 추론은 방향을 맞추는 도구이지 정답을 주는 도구가 아니다. do-while 안의 숨은 필터, if로 갈라지는 조건부 생성, 좌표를 군집 중심 주변에 뿌리는 코드 — 이런 것은 추론만으로 놓칠 수 있다. 그래서 30초 추론 뒤에는 반드시 §01의 분석 루틴(histogram, density)을 한 번 돌려 가설을 확인한다. 추론으로 시작하고, 관측으로 확정한다.
다음 단계 제약을 읽어 분포를 그렸고, PASS 역산으로 알고리즘 등급을 판정했다. 이제 그 등급의 알고리즘을 짜고 — 점수가 모자라면 고쳐야 한다. 막무가내가 아니라 체계적으로. §03 알고리즘 개선 전략은 병목을 찾고, 파라미터를 훑고, 두 코드를 겨루는 4단계 개선 방법론이다. 추론(§02)이 출발선을 정했다면, 개선(§03)이 결승선까지 데려간다.