문제 해결은 정답을 한 번에 떠올리는 능력이 아니라, 막연한 문제를 질문과 실험을 통해 점차 해결 가능한 형태로 바꾸는 과정이다. 문제를 풀 때는 다음 네 단계를 반복한다.
- 문제를 이해한다.
- 계획을 세운다.
- 계획을 실행한다.
- 결과를 검토하고 배운 점을 확장한다.
이 단계는 엄격한 순서가 아니다. 계획을 세우다가 문제를 잘못 이해했다는 것을 알 수 있고, 실행 중에 더 나은 계획이 필요해질 수도 있다. 그때는 이전 단계로 돌아가 관점을 바꾼다.
여기서 계획은 문제에 알고리즘 이름을 붙이는 일과 다르다. “이건 DP다”, “이건 그리디다”와 같은 분류는 탐색의 출발점이 될 수 있지만, 실제 계획의 내용을 대신하지는 못한다. 익숙한 유형을 너무 빨리 대입하면 문제의 고유한 조건을 놓치거나, 경험과 맞지 않는 문제 앞에서 오히려 사고가 멈출 수 있다. 패턴은 결론이 아니라 검증해야 할 가설로 사용하고, 끝까지 문제의 조건에서 상태·연산·정당성을 다시 만들어야 한다.
문제 해결 훈련에는 서로 다른 두 목표가 있다. 새로운 문제의 해결법을 찾아내는 능력과, 이미 해결할 수 있는 문제를 제한 시간 안에 빠르고 정확하게 처리하는 능력은 겹치는 부분이 있지만 동일하지 않다.
| 목표 | 주로 훈련하는 것 | 적합한 환경 |
|---|---|---|
| SOLVE | 새로운 관찰, 모델링, 증명, 알고리즘을 발견하는 능력 | 아카이브 문제, 시간 압박이 적은 탐색, 해설 복기 |
| FAST | 문제 선택, 시간 배분, 구현 속도와 정확성 | 대회, 버추얼 대회, 타이머를 사용한 반복 풀이 |
대회 참가만 반복하면 이미 풀 수 있는 문제를 빠르게 처리하는 능력은 늘어도, 새로운 유형을 처음부터 해결하는 능력이 충분히 자라지 않을 수 있다. 반대로 어려운 문제만 오래 붙잡으면 실전 속도와 구현 정확성을 점검할 기회가 부족해진다. 따라서 두 목표를 의식적으로 나누어 연습하고, 각 훈련의 성공 기준도 다르게 둔다.
1. 문제를 이해한다
문제를 읽자마자 해결 방법을 찾으려 하지 말고, 먼저 문제의 구조를 분리한다.
익숙한 문제처럼 보여도 첫 독해에서 유형을 확정하지 않는다. “어디서 본 문제인가?”라는 질문은 도움이 되지만, 그보다 먼저 “이 문제에서만 중요한 조건은 무엇인가?”를 묻는다. 문제를 자신의 경험에 억지로 끼워 맞추는 대신, 작은 사례와 경계 조건으로 그 분류가 실제로 맞는지 확인한다.
- 미지수: 무엇을 알아내거나 만들어야 하는가?
- 데이터: 이미 주어진 값, 조건, 제약은 무엇인가?
- 관계: 미지수와 데이터는 어떤 조건으로 연결되어 있는가?
- 목표: 계산, 판정, 최적화, 구성, 증명 중 무엇을 요구하는가?
다음 질문에 답할 수 있으면 문제를 자신의 것으로 다시 표현할 수 있다.
- 문제에 사용된 모든 단어와 기호를 이해했는가?
- 문제를 자신의 말로 한두 문장으로 설명할 수 있는가?
- 입력과 출력의 예를 직접 작은 값으로 만들어 볼 수 있는가?
- 그림, 표, 그래프, 상태 변화로 나타내면 더 명확해지는가?
- 주어진 조건은 목표를 결정하기에 충분한가?
- 입력의 최솟값, 최댓값, 빈 입력, 중복, 경계값에서는 무엇이 일어나는가?
추상적인 정의가 이해되지 않으면 그것을 실행 가능한 절차로 번역해 본다. 어떤 대상을 판정하는 조건을 작은 함수로 쓰거나, 주어진 연산을 수행하는 간단한 시뮬레이터를 만들어 보면 정의 사이에 숨은 상태와 순서가 드러난다. 이 코드는 최종 풀이가 아니어도 된다. 직접 조작할 수 있는 모델을 만든 뒤 문제를 다시 읽으면, 처음부터 완벽한 해법을 위에서 쪼개려 할 때보다 필요한 작업을 더 정확히 파악할 수 있다.
이 단계의 산출물은 아직 알고리즘이 아니다. 무엇을 구해야 하는지, 무엇이 주어졌는지, 어떤 조건을 만족해야 하는지에 대한 정확한 문장이다. 문제를 정확히 이해하지 못한 채 세운 계획은 구현이 아무리 깔끔해도 다른 문제를 풀게 만든다.
2. 계획을 세운다
계획을 세우는 일은 네 단계 중 가장 어렵다. 좋은 아이디어는 처음부터 완성된 형태로 떠오르기보다, 여러 관점과 실패한 시도를 거치며 점차 구체화된다. 따라서 막혔다는 것은 계획을 세우는 과정이 진행 중이라는 뜻일 수 있다.
계획은 최상위 흐름에서 시작해 세부로 내려간다. 먼저 입력 해석 → 핵심 상태 또는 연산 계산 → 결과 구성처럼 주 함수의 뼈대를 한 줄로 적고, 각 단계를 다시 하위 문제로 나눈다. 이 흐름이 있으면 문제 전체를 한 번에 해결하려는 대신, 이미 해결된 부분과 아직 남은 핵심 간극을 구분할 수 있다. 알고리즘 이름은 이 흐름을 요약하는 표지일 뿐이며, 실제로는 어떤 정보를 유지하고 어떤 순서로 변환하는지가 계획이다.
계획은 한 층으로 끝나지 않는다. 상위 단계마다 필요한 입력·출력, 최악의 비용, 가장 가능성 높은 오류를 따로 적어 두면 “계획 안의 계획”이 생긴다. 위에서 하위 문제를 나누는 방식이 막히면 방향을 바꿔도 된다. 문제 설명에 절차가 자세히 주어졌거나 필요한 원시 연산이 분명하다면, 그 작은 함수부터 구현해 아래에서 위로 조립할 수 있다. 이 경로는 선형일 필요가 없으며, 작은 동작을 확인한 뒤 다시 문제 전체를 읽으면 상위 구조가 더 잘 보이기도 한다.
관점을 바꾸는 질문
한 가지 접근을 오래 붙잡기보다 다음 질문을 순서 없이 시도한다.
- 더 단순한 관련 문제로 바꾸면 무엇이 보이는가?
- 입력의 크기를 줄이거나 일부 조건을 고정하면 어떤 패턴이 나타나는가?
- 작은 사례를 손으로 계산하거나 완전 탐색으로 구현하면 어떤 규칙을 추측할 수 있는가?
- 문제를 배열, 그래프, 상태와 전이, 수식, 기하학적 모델 중 다른 형태로 표현할 수 있는가?
- 목표에서 거꾸로 출발하면 이미 알고 있는 상태에 도달하는가?
- 대칭적인 요소나 서로 바꾸어도 결과가 같은 요소가 있는가?
- 문제를 하위 문제로 분해한 뒤 다시 조합할 수 있는가?
- 최솟값·최댓값 같은 극단적인 사례에서는 어떤 구조가 강제되는가?
- 무엇이 변하고, 무엇이 보존되는가? 불가능한 경우를 증명하면 길이 줄어드는가?
- 문제를 일반화하거나 반대로 특수화했을 때 더 쉬운 문제를 얻을 수 있는가?
- 주어진 절차나 연산 자체를 작은 함수로 먼저 구현하면 전체 문제가 단순해지는가?
- 원래 표현을 다른 좌표·그래프·상태로 바꾸면 계산이 쉬워지고, 결과를 다시 되돌릴 수 있는가?
- 입력을 서로 다른 경우로 나누면 각 경우가 더 단순한 규칙을 갖는가?
- 유한한 선택·구성·경로를 모두 시도하는 기준선이 복잡도를 판단하는 데 도움이 되는가?
이 질문들은 정답을 알려 주는 공식이 아니다. 문제에 맞는 질문을 선택하고, 그 질문이 새로운 제약이나 관찰을 만들어 내는지 판단하는 연습이 필요하다. 특히 다음과 같은 변환은 프로그래밍 문제에서 자주 유용하다.
- 목표를 뒤집는다. 최소화를 최대화로, 구성을 판정이나 증명으로 바꾸어 본다.
- 연산의 순서를 바꾸거나 과정을 역방향으로 따라간다.
- 브루트 포스를 먼저 만들어 정답과 중간 상태를 관찰한다.
- 보존되는 값, 불변식, 제약 조건을 찾아 불가능한 경우를 제거한다.
- 접두사·접미사, 구간, 빈도, 집합, 해싱처럼 문제의 반복 구조를 드러내는 표현을 시도한다.
표현을 바꾸는 접근을 사용할 때는 변환 자체가 새로운 오류의 원인이 되지 않도록 변환 → 문제 풀이 → 필요하다면 역변환의 경계를 명시한다. 변환 전후에 보존되어야 하는 값과 답이 같은 이유를 적고, 테스트에서도 내부의 값이 원래 값이 아니라는 점을 구분한다. 변환이 답에 영향을 주지 않는 문제라면 역변환을 생략할 수 있지만, 그 이유 역시 계획에 남겨야 한다.
아직 풀지 못하는 가장 쉬운 경우부터 다룬다
작은 입력을 확인하는 목적은 단순히 예제를 많이 만드는 것이 아니다. 현재 해결하지 못하는 경우 중 가장 단순한 것을 찾아 그 구조를 분리하는 데 있다. 예를 들어 다음과 같이 조건을 하나씩 제거한다.
n = 1,n = 2처럼 입력 크기를 줄인다.- 모든 값이 같거나, 입력이 이미 정렬된 경우를 본다.
- 트리를 경로로, 여러 쿼리를 하나의 쿼리로 바꾼다.
- 가중치를 0 또는 1로 제한한다.
- 완전 탐색이 가능한 크기에서 정답과 중간 상태를 비교한다.
특수한 경우를 해결한 뒤에는 그 풀이에서 반복되는 상태, 불변량, 선택 기준을 찾아 일반적인 경우로 확장한다. 특수한 경우를 맞혔다는 사실 자체가 일반 풀이의 증명은 아니므로, 어떤 변환이 더 큰 입력에서도 유지되는지 별도로 확인해야 한다.
제약조건, 문제의 분류, “항상 해가 존재한다”는 보장은 풀이를 고르는 단서가 될 수 있지만 증명은 아니다. 작은 배열 크기는 완전 탐색을 시도해 볼 신호일 수 있고, 큰 수의 범위는 자료형이나 계산량을 점검하게 만들 수 있지만, 최악 입력에서 실제 비용을 계산해야 한다. 출제자의 의도를 추측하는 데 머물지 말고 그 단서를 불변식·복잡도·반례로 검증한다.
계획을 검증 가능한 문장으로 만든다
아이디어가 떠올랐다면 “될 것 같다”에서 멈추지 말고 다음을 적는다.
- 어떤 상태 또는 자료를 유지할 것인가?
- 한 번의 연산에서 그 상태가 어떻게 바뀌는가?
- 왜 이 상태만으로 이후 판단에 필요한 정보를 잃지 않는가?
- 종료 시 상태가 문제의 정답과 어떻게 연결되는가?
- 시간과 공간 복잡도는 입력 제약을 감당할 수 있는가?
- 각 하위 단계가 실패했을 때 무엇을 먼저 검사하고, 어떤 작은 테스트로 분리할 수 있는가?
- 더 직접적인 기준선 풀이를 만들어 작은 입력에서 비교할 수 있는가?
이 질문에 답하지 못하면 아직 구현 단계로 넘어갈 때가 아니다. 구현이 어렵다는 것은 종종 코드 작성 능력의 문제가 아니라 계획의 세부 사항이 충분히 명확하지 않다는 신호다.
최적화·구성·카운팅 문제에서는 가장 직접적인 완전 탐색을 먼저 후보로 둔다. 최악의 입력에서 그 비용을 추정해 충분히 빠르면 단순한 풀이 자체가 정답일 수 있고, 느리다면 작은 입력용 기준선으로 남겨 최적화한 풀이와 비교한다. 기준선은 대개 더 직관적이므로 무작위 테스트에서 두 결과를 대조하면 “더 빠른 코드가 맞는가”와 “빠르지만 다른 문제를 풀고 있는가”를 분리해 확인할 수 있다.
분해를 계속하면 더 이상 의미 있게 나누기 어려운 작은 작업이 남는다. 이를 원자 단계로 취급하려면 언어의 기본 연산이나 표준 라이브러리의 입력·출력, 복잡도, 경계 동작을 알고 있어야 한다. 자주 쓰는 언어 기법을 익혀 두면 계획에서 많은 부분을 빠르게 확정할 수 있고 구현 오류도 줄어든다. 반대로 라이브러리나 예제 코드를 이해하지 않은 채 복사하면 원자 단계가 아니라 새로운 불확실성을 추가하는 셈이므로, 사용한 기능의 계약과 비용은 확인해야 한다.
생각을 언어화한다
코딩 전에 풀이를 다른 사람이나 미래의 자신에게 설명한다고 생각한다. 다음 질문에 답하지 못하는 부분이 바로 추가로 생각해야 할 부분이다.
- 무엇을 계산하거나 결정해야 하는가?
- 왜 이 상태만 저장하면 미래의 결정에 필요한 정보가 충분한가?
- 전이는 가능한 모든 경우를 포함하는가?
- 중복 계산이나 누락은 없는가?
- 이 답이 최적인 이유를 한 문장으로 설명할 수 있는가?
- 시간·공간복잡도가 제약조건을 만족하는가?
생각을 말이나 글로 바꾸면 머릿속에서 그럴듯하게 보이던 빈틈이 드러난다. 설명은 발표를 위한 부가 작업이 아니라, 알고리즘을 검증하는 도구다.
3. 계획을 실행한다
계획을 코드나 실제 작업으로 옮긴다. 이때는 새로운 아이디어를 계속 추가하기보다 계획의 각 문장을 작은 작업으로 분해한다.
- 핵심 불변식과 상태 전이를 먼저 주석이나 의사 코드로 적는다.
- 최상위 흐름을 유지한 채 하위 단계와 원자적인 동작을 하나씩 구현하고, 각 단계가 계획과 일치하는지 확인한다.
- 대표 사례뿐 아니라 최솟값, 최댓값, 빈 경우, 중복, 한 단계만 있는 경우를 테스트한다.
- 가능하면 단순한 완전 탐색 버전과 최적화 버전의 결과를 무작위 입력으로 비교한다.
- 오류가 생기면 코드를 무작정 고치기보다, 문제 이해·계획·불변식·구현 중 어느 단계에서 어긋났는지 분류한다.
계획은 전체 방향을 제공하고, 실행은 세부 사항이 그 방향과 맞는지 확인하는 단계다. 따라서 구현 중에는 “이 코드가 왜 필요한가?”, “이 상태가 계획에서 정의한 상태와 같은가?”를 계속 묻는다.
디버깅도 구현 후에 떠올리는 응급 절차가 아니라 계획의 일부다. 오류가 나면 먼저 실패한 하위 함수의 입력·출력과 불변식을 독립적으로 확인하고, 그 다음 함수 사이의 연결과 전체 흐름을 살핀다. 상향식으로 먼저 작성한 원시 연산은 문제를 덜 이해한 시점의 가정을 담고 있을 수 있으므로, 코드가 작성된 순서가 오래된 부분부터 점검하는 것도 유용한 휴리스틱이다. 같은 코드를 여러 곳에 복사해 두면 한 곳의 수정이 다른 곳에 전파되지 않으므로, 반복되는 동작은 하나의 검증 가능한 단위로 두는 편이 수정과 재검증에 유리하다.
코딩 중 더 우아한 접근이 떠올라도 현재 계획이 틀렸다는 증거가 없다면, 시간 제한과 구현 위험을 따져 지금의 풀이를 먼저 완성하는 편이 낫다. 반대로 반례나 복잡도 분석으로 계획이 틀렸음이 확인되면, 기존 코드와 메모리를 지우기보다 재사용할 수 있는 함수·검증 사례·실패 원인을 보존한 채 방향을 전환한다. 최종 코드에서는 죽은 코드를 제거하되, 사고의 흔적까지 잃어버리면 같은 실수를 다시 조사해야 한다.
4. 검토하고 확장한다
정답을 얻었다고 문제 해결이 끝나는 것은 아니다. 결과와 그 결과에 도달한 과정을 다시 살펴봐야 다음 문제에서 재사용할 수 있는 지식이 된다.
- 결과가 문제의 맥락에서 말이 되는가?
- 주어진 데이터를 모두 사용했는가? 불필요한 가정은 없었는가?
- 계산, 논리, 경계 조건, 복잡도를 다시 확인했는가?
- 더 단순하거나 더 일반적인 풀이가 가능한가?
- 같은 문제를 다른 자료구조·순서·구현으로 풀면 정확성, 속도, 디버깅 가능성이 어떻게 달라지는가?
- 무엇이 핵심 관찰이었고, 어떤 시도는 왜 실패했는가?
- 이 풀이를 적용할 수 있는 다른 문제는 무엇인가?
풀이를 복기할 때는 “정답을 맞혔다”보다 “다음에 같은 유형을 만났을 때 어떤 신호를 알아볼 것인가”를 기록한다. 해설을 읽은 경우에도 풀이를 베껴 적는 데서 끝내지 말고, 문제의 조건에서 핵심 아이디어까지 도달하는 중간 질문을 자신의 언어로 다시 구성한다.
막혔을 때의 운영
문제마다 뛰어넘어야 하는 **간극(gap)**이 있다. 간극은 어려운 구현, 보이지 않는 관찰, 필요한 알고리즘, 혹은 여러 단계의 조합일 수 있다. 쉬운 문제만 풀면 작은 간극을 빠르게 건너는 능력은 늘지만 더 큰 간극을 넘는 연습은 부족해진다. 반대로 현재 수준과 너무 동떨어진 문제만 고르면 해설을 읽어도 연결 고리를 찾기 어려워 학습 효율과 동기가 함께 떨어질 수 있다.
따라서 연습 문제는 “조금 어렵지만 원칙적으로 도달 가능한 범위”에서 고른다. 쉬운 문제는 구현 속도와 정확성을 유지하는 데 사용하고, 어려운 문제는 새로운 사고법을 확장하는 데 사용한다.
한 가지 훈련 레시피
Codeforces 글의 저자는 무작정 “더 많이 풀라”고 하기보다, 자신의 현재 실력에서 약 60–90분 안에 풀 수 있을 것으로 예상되는 문제 범위를 정하고 반복해서 풀 것을 제안한다. 이는 모든 사람과 모든 목표에 검증된 보편 법칙이라기보다, 한 경쟁 프로그래머의 경험에서 나온 실천 레시피다. 다음처럼 적용할 수 있다.
- 현재 실력으로 60–90분 정도 고민하면 해결 가능할 것 같은 난이도 범위를 정한다.
- 그 범위에서 문제를 골라 한 문제씩 풀되, 여러 문제를 병렬로 열어 두고 사고해도 된다.
- 예상 시간보다 훨씬 오래 걸리면 해설을 읽는다. 해설을 본 사실보다, 해설에서 어떤 사고의 간극을 놓쳤는지 정리하는 것이 중요하다.
- 풀이를 이해한 뒤 직접 구현하고, 며칠 또는 몇 주 뒤 다시 풀어 본다.
- 같은 범위를 반복하며 해결 속도, 풀이를 검증하는 능력, 구현 정확성이 함께 좋아지는지 확인한다.
같은 문제를 코드를 지운 뒤 세 번 다시 구현하는 방식도 계획 감각을 훈련하는 데 활용할 수 있다. 첫 번째 시도에는 문제를 이해하고 탐색하는 시간이 함께 들어간다. 두 번째 시도에서는 이미 알고 있는 풀이를 구현하면서 반복하는 버그와 빠진 세부 사항이 드러난다. 세 번째 시도에서는 올바른 전략을 곧바로 떠올렸을 때의 구현 속도와, 상태 전이·검증 순서를 얼마나 자연스럽게 재현하는지를 확인할 수 있다. 매번 코드를 직접 다시 쓰고 시간, 반복된 실수, 계획에서 빠졌던 문장을 기록해야 효과가 있다. 다만 이 세 번 반복하기는 모든 학습에 적용해야 하는 법칙이 아니라, 특정 문제의 접근을 몸에 익히기 위한 선택적 훈련 레시피다.
문제 선택의 기준은 현재 목표에 따라 달라진다. 대회 실전 감각이 목표라면 실제 대회에서 만날 법한 문제를 제한 시간 안에 해결하는 연습이 필요하고, 특정 알고리즘이나 구현 능력이 목표라면 해당 주제의 쉬운 문제를 의도적으로 섞을 수 있다. 대회에 계속 참가하는 것만으로는 부족할 수 있으므로, 가상 대회와 업솔빙을 통해 풀지 못한 문제의 사고 과정을 복기한다.
해설을 읽는 기준
해설은 너무 빨리 보면 탐색 과정을 건너뛰고, 너무 늦게 보면 한 문제에 시간을 과도하게 사용할 수 있다. 다음 기준을 사용한다.
- 먼저 문제를 자신의 말로 다시 쓰고, 작은 사례와 몇 가지 가설을 시도한다.
- 상당한 시간 동안 진전이 없거나, 필요한 개념 자체를 모르는 경우 해설을 참고한다.
- 해설을 읽을 때는 전체 코드를 복사하지 말고, 첫 번째로 이해되지 않는 관찰까지만 확인한다.
- 해설을 닫고 핵심 아이디어부터 의사 코드로 다시 만든다.
- 구현과 검증을 마친 뒤, 원래 문제에서 어떤 단서가 그 아이디어를 암시했는지 기록한다.
이 과정의 목적은 해결한 문제의 수를 늘리는 것이 아니라, 실패한 문제에서도 재사용 가능한 패턴을 추출하는 것이다. 문제 수와 문제의 질은 대립하지 않는다. 쉬운 문제로 기본기를 유지하고, 적절한 난이도의 문제로 사고의 간극을 넓히며, 복기를 통해 경험을 지식으로 바꾼다.
알고리즘은 문제를 통해 배운다
알고리즘을 먼저 목록으로 암기한 뒤 모든 문제에 적용하려 하기보다, 문제를 해결하는 과정에서 필요한 개념을 찾아 배우는 편이 오래 남는다. 해설에서 처음 보는 알고리즘이나 자료구조가 나오면 다음 순서로 학습한다.
- 그 개념이 문제의 어떤 제약을 해결하는지 확인한다.
- 개념의 핵심 불변식과 시간복잡도만 먼저 이해한다.
- 해설을 닫고 해당 문제를 직접 구현한다.
- 같은 개념을 사용하는 유사 문제를 한두 개 더 풀어 본다.
알고리즘을 안다는 것은 기본 형태를 호출할 수 있다는 뜻보다 넓다. 어떤 불변식을 유지하는지, 시간이 어디에서 드는지, 어떤 부분을 바꾸면 결과와 복잡도가 어떻게 달라지는지 설명할 수 있어야 한다. 가능하면 코드를 보지 않고 기본 구현을 재현한 뒤 변형을 시도한다. 그러면 알고리즘 이름을 문제에 억지로 끼워 맞추는 대신, 필요한 하위 단계의 도구로 선택하고 문제에 맞게 변형할 수 있다. 여러 기법을 처음 배울 때는 특정 문제에만 쓰이는 세부 기법보다 분할 정복·동적 계획법·그리디처럼 구조를 조작하는 넓은 개념부터 익히는 편이 응용 범위를 넓히는 데 도움이 된다. 이 순서는 학습 전략이지 모든 학습자에게 검증된 법칙은 아니다.
다만 대회 준비처럼 특정 알고리즘 범위를 요구하는 목표라면 별도의 이론 학습도 필요하다. 문제 중심 학습은 알고리즘 지식을 불필요하게 만든다는 뜻이 아니라, 지식을 실제 판단과 연결하는 방식이다.
문제 해결 휴리스틱 모음
앞의 단계에서 적절한 질문이 떠오르지 않을 때 사용하는 도구 모음이다. 모든 문제에 모든 도구를 적용하지 말고, 현재의 막힘을 가장 잘 설명하는 질문부터 선택한다.
- 유추: 이미 풀어 본 문제와 구조가 비슷한가?
- 단순화: 일부 조건을 제거하거나 입력을 작게 만들면 무엇이 남는가?
- 특수화: 어떤 값이 0, 1, 최댓값이라면 어떻게 되는가?
- 일반화: 작은 사례에서 발견한 패턴을 더 넓은 경우에도 증명할 수 있는가?
- 분해와 재조합: 문제를 위에서 하위 문제로 나누거나, 원자적인 동작을 아래에서 쌓아 올린 뒤 결과를 합칠 수 있는가?
- 보조 문제: 원래 문제를 바로 풀지 않고, 해결에 도움 되는 중간 질문을 만들 수 있는가?
- 모델링과 표현 변환: 그림, 표, 그래프, 상태 머신, 수식으로 바꾸거나 계산하기 쉬운 영역으로 옮기면 구조가 드러나는가?
- 패턴과 대칭: 반복, 주기, 교환 가능한 요소, 불변량이 있는가?
- 귀납과 실험: 작은 사례를 계산하고, 관찰한 규칙을 귀납적으로 검증할 수 있는가?
- 역방향 사고: 목표에서 시작해 이미 알고 있는 조건까지 거꾸로 갈 수 있는가?
- 문제 변형: 원래 문제를 조금 바꾸어 더 쉬운 문제를 만들 수 있는가?
- 정의의 실행화: 낯선 정의를 판정 함수나 시뮬레이션 절차로 옮기면 무엇이 드러나는가?
- 경우 나누기: 입력을 몇 가지 구조적 경우로 나누면 각 경우의 규칙을 단순화할 수 있는가?
- 완전 탐색 기준선: 모든 선택을 시도하는 단순한 풀이를 만들 수 있고, 최악 비용을 감당할 수 있는가?
- 제약 읽기: 입력 제한과 보장 조건이 허용하는 풀이의 범위는 어디까지인가? 그것을 증거로 착각하고 있지는 않은가?
- 대안 비교: 같은 상위 계획을 다른 자료구조·순서·구현으로 바꾸면 정확성, 속도, 디버깅 비용이 어떻게 달라지는가?
- 모순과 불가능성: 어떤 가정이 모순을 일으키는지 보여 주면 선택지가 줄어드는가?
휴리스틱은 알고리즘 지식이나 도메인 지식을 대신하지 않는다. 같은 질문도 문제의 맥락을 모르면 엉뚱한 방향으로 이끌 수 있다. 반복적인 문제 풀이와 복기를 통해 어떤 상황에서 어떤 질문이 효과적인지 판단하는 능력을 함께 길러야 한다.
생각을 계속할지 해설을 볼지 결정한다
고정된 시간 규칙 하나로 모든 문제를 다루기보다, 사고가 실제로 전진하고 있는지 확인한다.
계속 생각할 신호
- 새로운 관찰이 계속 나온다.
- 이전 접근이 실패한 이유를 설명할 수 있다.
- 더 작은 하위 문제로 줄이거나 상태 정의를 구체화하고 있다.
- 시간복잡도나 정당성에 대한 질문이 점점 명확해진다.
멈추고 방향을 바꿀 신호
- 같은 두세 가지 아이디어만 근거 없이 반복한다.
- 자료구조나 알고리즘 이름만 나열하고 문제와의 연결을 설명하지 못한다.
- 예제를 변형하고 있지만 새로운 결론을 얻지 못한다.
- 왜 접근이 실패했는지 기록하지 않은 채 코드를 계속 고친다.
전자의 상태라면 더 생각하는 시간이 새로운 간극을 줄이는 훈련이 될 수 있다. 후자의 상태라면 다른 문제로 이동하거나, 잠시 쉬었다가 돌아오거나, 해설의 일부를 참고하는 편이 낫다. 숙련자는 해설을 한 달 이상 미루기도 하지만, 초중급자가 모든 문제에 이 전략을 적용하면 학습 속도가 지나치게 느려질 수 있다.
해설을 보더라도 전체 코드를 복사하지 않는다. 처음 이해되지 않는 관찰까지만 확인하고 해설을 닫은 뒤, 아이디어·증명·구현을 자신의 방식으로 다시 만든다. 완전히 이해한 것처럼 느껴져도 직접 구현하고 반례를 만들어 보는 과정이 필요하다.
실전 시뮬레이션과 분석
실전 속도와 정확성을 높이는 훈련에서는 실제 대회처럼 시간제한을 둔다. 여러 날에 걸친 장시간 시뮬레이션을 하더라도 중요한 것은 참가 횟수가 아니라 종료 후 분석이다.
- 대회 중에는 문제 선택, 포기, 전환 시점을 기록한다.
- 종료 후 풀지 못한 문제를 난이도와 원인별로 분류한다.
- 가장 쉬운 미해결 문제부터 업솔빙한다.
- 핵심 알고리즘을 머릿속으로 완성한 뒤 직접 구현한다.
- 다음 시뮬레이션에서 같은 실수를 피할 운영 규칙을 하나 정한다.
실전 성과는 타이핑 속도만으로 결정되지 않는다. 코딩을 시작하기 전에 알고리즘의 불확실성을 얼마나 제거했는지, 그리고 완성한 계획을 얼마나 빠르고 정확하게 구현하는지가 함께 작용한다. 따라서 문제 해결력 세션과 속도 세션을 분리하면 각 세션의 피드백이 더 선명해진다.
현실적인 훈련 구성
현재 목표에 따라 다음과 같이 구성할 수 있다.
- 문제 해결력 세션: 현재보다 약간 어려운 문제를 시간 압박 없이 풀고, 관찰과 증명을 기록한다.
- 속도·정확도 세션: 이미 익숙한 난이도의 문제를 타이머와 함께 풀며 모델링, 구현, 검증 시간을 줄인다.
- 버추얼 대회: 실제 시간과 규칙을 지키며 문제 선택과 시간 배분을 연습한다.
- 복기 세션: 해설을 읽은 문제도 직접 구현하고, 놓친 신호와 반복할 실수를 기록한다.
훈련의 성과는 해결한 문제 수나 대회 등락만으로 판단하지 않는다. 새로운 문제에서 사용할 관찰, 다시 피할 사고 오류, 재발을 막을 구현 규칙이 남았는지를 확인한다.
참고 자료
- The ‘science’ of training in competitive programming
- You can do it, too!
- How to Solve It — Wikipedia
- 28가지 문제 해결 전술
- Tourist 인터뷰 — ITMO News
- Tourist 인터뷰 — IFMO
- Petr Mitrichev 인터뷰 — Red-Green-Code
- Um_nik의 연습 방법 — Codeforces
- Benjamin Qi 인터뷰 — IOI
- How to Practice — USACO Guide
- Benq의 문제 해결 노트
- Errichto의 Codeforces 블로그
- Benq 인터뷰 — Codeforces
- Planning an Approach to a Topcoder Problem Part 1 — Topcoder
- Planning an Approach to a Topcoder Problem Part 2 — Topcoder