해 구성하기 (Constructive)
정의
해 구성하기 (Constructive Algorithm) 는 답의 존재를 증명하는 대신, 명시적으로 답을 만드는 문제 유형.
“가능한가?” → YES/NO 가 아니라, “가능하다면 그 예시를 하나 만들어라” 가 출력.
수학의 구성적 증명 (constructive proof) 과 동일. 존재성 정리 (존재한다는 증명) 보다 구체 알고리즘을 요구.
문제 상황과 동기
문제: “조건 C 를 만족하는 수열/배치/그래프를 하나 만들어라. 없으면 -1.”
- naive: 모든 가능한 배치를 브루트포스 (O(N!) 또는 O(2^N)).
- constructive: 조건 분석 → 그리디 / 재귀 / 역산으로 O(N)~O(N log N) 구성.
핵심 통찰: “불가능 조건”을 먼저 제거하고, 가능한 경우 단계적 구성.
자주 등장하는 PS 위치:
- Balanced parentheses 생성
- Matching / Pairing 문제
- Magic square / Latin square 구성
- 그래프 간선 추가로 조건 만족
실무:
- 테스트 케이스 자동 생성 (fuzzer, random testing)
- 스케줄링 (과제 배치, 시프트)
시각화
핵심 아이디어
invariant: 단계마다 조건을 유지하며 확장.
대표 패턴:
- 그리디 구성: 각 단계마다 지역 최적 선택.
- 재귀 분할: 작은 부분 문제 해결 후 합치기.
- 역산: 목표 상태에서 거꾸로 역추적.
- 대칭/패턴: 규칙적 배치 (예: 대각선, 회전).
- 불변량 유지: 각 단계에서 조건 검증.
알고리즘
constructive(conditions):
if impossible(conditions):
return -1
result = []
while not complete(result):
next_element = choose_next(result, conditions)
result.append(next_element)
if violates(result, conditions):
backtrack or fail
return result
구현
예제 1: Balanced Parentheses 생성
N쌍의 괄호로 올바른 괄호 문자열 하나 만들기.
관찰: 여는 괄호 N개, 닫는 괄호 N개. 항상 (왼쪽 누적 ≥ 오른쪽 누적).
// N쌍 괄호, 가장 간단한 구성: (((...)))
#include <bits/stdc++.h>
using namespace std;
int main() {
int n; cin >> n;
string s(n, '(');
s += string(n, ')');
cout << s << "\n";
}3((()))예제 2: 1~N 순열, 인접 차이 ≥ 2
N개 수 1,2,…,N 을 배치하되, 인접한 두 수의 차이가 2 이상이어야 함.
관찰: N=1,2 는 불가능 (1,2 인접하면 차이=1). N≥3 이면 홀수-짝수 분리 배치 (1,3,5,…,2,4,6,…) 가 해.
// 홀수-짝수 구성, O(N)
#include <bits/stdc++.h>
using namespace std;
int main() {
int n; cin >> n;
if (n <= 2) {
cout << "-1\n";
return 0;
}
vector<int> ans;
for (int i = 1; i <= n; i += 2) ans.push_back(i);
for (int i = 2; i <= n; i += 2) ans.push_back(i);
for (int v : ans) cout << v << " ";
cout << "\n";
}2-1예제 3: Magic Square 3×3 (합=15)
3×3 격자에 1~9 를 배치, 모든 행/열/대각선 합=15.
관찰: 중앙=5, 나머지 대칭 배치. 2,4,6,8 모서리, 1,3,7,9 코너.
// 3×3 매직 스퀘어 고정 배치, O(1)
#include <bits/stdc++.h>
using namespace std;
int main() {
int grid[3][3] = {
{2, 7, 6},
{9, 5, 1},
{4, 3, 8}
};
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++)
cout << grid[i][j] << " ";
cout << "\n";
}
}2 7 6
9 5 1
4 3 8복잡도
| 항목 | 값 |
|---|---|
| 시간 | 보통 O(N) ~ O(N log N) (그리디 구성) |
| 공간 | O(N) (결과 배열) |
| 구현 난이도 | 중간 (조건 검증 로직) |
변형
| 유형 | 설명 |
|---|---|
| 그리디 구성 | 각 단계마다 최선 선택 (가장 작은 수, 가장 빠른 시간) |
| 재귀 구성 | 작은 문제 해결 → 합치기 (분할정복) |
| 역산 | 목표 상태에서 거꾸로 추적 (예: 최종 수열 → 초기 배열) |
| 대칭/회전 | 규칙적 패턴 (Magic square, Latin square) |
| 조합론적 구성 | 조합/순열 생성 (next_permutation, 비트마스크) |
함정
1. 불가능 조건 누락
“항상 가능” 이라 가정했는데 반례 존재. 불가능 케이스 먼저 처리 (if N < threshold: return -1).
2. 부분 조건 위반
각 단계에서 조건 만족했지만, 최종 결과가 전체 조건 위반. 예: 괄호 생성 중 중간에 닫는 괄호가 여는 괄호보다 많음.
3. 중복 제거 미흡
같은 원소를 여러 번 사용 (순열에서 중복). set 또는 used[] 배열로 관리.
4. 오프바이원 (off-by-one)
1-indexed vs 0-indexed, inclusive vs exclusive. 배열 범위 검증 필수.
BOJ 연습 문제
| 번호 | 제목 | 정답률 | 링크 |
|---|---|---|---|
| BOJ 9086 | 문자열 | - | kokoa-lab |
| BOJ 1305 | 광고 | - | kokoa-lab |
| BOJ 1748 | 수 이어 쓰기 1 | - | kokoa-lab |
| BOJ 14503 | 로봇 청소기 | - | kokoa-lab |
참고
이 글의 용어 (4개)
- 그리디 (Greedy)algorithm
- 정의 그리디 (Greedy) 알고리즘은 매 단계마다 국소 최적 (locally optimal) 선택을 하여 전역 최적해 (globally optimal solution) 에 도달…
- 백트래킹 (Backtracking)algorithm
- 정의 백트래킹 (Backtracking) 은 DFS + 가지치기 (pruning) 로 모든 경우를 탐색하되, 불가능한 후보를 조기 제거하여 탐색 공간을 극적으로 줄이는 알고리즘.…
- 애드 혹 (Ad Hoc)algorithm
- 정의 애드 혹 (Ad Hoc) 은 범용 알고리즘이 아닌, 그 문제만의 관찰·트릭·창의적 사고로 푸는 문제 유형. 라틴어로 "이 목적을 위한" 이라는 뜻. 정형화된 자료구조나 DP…
- 케이스 워크 (Case Work)algorithm
- 정의 케이스 워크 (Case Work) 는 문제를 여러 조건/경우로 분류하여 각각 해결하는 패턴. if-else, switch-case, 의사결정 트리 등으로 구현. 복잡한 조건…
💬 댓글