반응형
Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |
Tags
- LocalDate
- localtime
- 분할정복 알고리즘
- spring security 설정
- 자바의정석
- 백트래킹
- over()
- 티스토리챌린지
- 혼공얄코
- hackerrank
- spring security
- 둘만의 암호
- 오버라이딩
- 오블완
- BFS
- 자바의 정석
- 분할정복
- 멀티프로세싱
- greedy
- 멀티태스킹
- Arrays
- 리눅스
- Comparable
- SQL Mapper
- 오버로딩
- 둘만의 암호 자바
- Comparator
- 프로그래머스
- java
- 프로그래머스 둘만의 암호
Archives
- Today
- Total
쉽게 쉽게
[알고리즘] 분할정복(Divide & Conquer)이란? (백준 2630 , 1992, 1780, 1629 ,10830) 본문
반응형
📌 핵심 요약
- 분할정복 3단계 — Divide(분할) → Conquer(정복) → Combine(합치기), 기저 조건이 반드시 존재해야 한다
- 분할정복 거듭제곱 — b가 짝수면 절반으로 쪼개고, 홀수면 하나 떼어내서 O(log b)에 계산한다
- 행렬 거듭제곱 — 숫자 거듭제곱과 동일한 분할정복 구조, 곱셈만 행렬 곱셈으로 교체하면 된다
1 분할정복(Divide & Conquer)이란?
분할정복은 복잡한 문제를 더 작은 동일한 구조의 하위 문제로 쪼개서 해결하는 알고리즘 설계 기법이다. 재귀(Recursion)를 기반으로 동작하며, 세 단계로 구성된다.
| 단계 | 이름 | 설명 |
|---|---|---|
| 1 | Divide (분할) | 문제를 더 작은 동일 구조의 하위 문제로 나눈다 |
| 2 | Conquer (정복) | 하위 문제를 재귀적으로 푼다. 더 이상 나눌 수 없으면(기저 조건) 직접 답을 반환한다 |
| 3 | Combine (합치기) | 하위 문제의 결과를 모아 원래 문제의 답을 만든다 |
기저 조건(Base Case)이란?
재귀 호출을 멈추는 조건이다. 예를 들어 1×1 격자, 지수가 1인 경우처럼 더 이상 쪼갤 수 없는 최소 단위에서 바로 답을 반환한다. 기저 조건이 없으면 무한 재귀로 스택 오버플로우가 발생한다.분할정복 코드 패턴
분할정복 코드는 항상 아래 구조를 따른다.
void solve(int problem) {
// 1단계: 기저 조건 — 더 이상 쪼갤 수 없는 최소 단위
if (isSolvable(problem)) {
directSolve(problem);
return;
}
// 2단계: Divide — 문제를 작은 단위로 분할
int[] subProblems = divide(problem);
// 3단계: Conquer — 재귀 호출로 각각 해결
for (int sub : subProblems) solve(sub);
// 4단계: Combine — 결과 합치기
combine(subProblems);
}
분할정복 거듭제곱 호출 트리
A^10을 예시로 보면, b가 짝수면 절반으로 쪼개고 홀수면 하나 떼어내어 재귀 호출 횟수를 O(log b)로 줄인다.
matPow(A, 10) ← 짝수: 절반으로 쪼갬
└─▶ matPow(A, 5) ← 홀수: 하나 떼어냄
├─▶ matPow(A, 4) ← 짝수: 절반으로 쪼갬
│ └─▶ matPow(A, 2)
│ └─▶ matPow(A, 1) ← 기저 조건: A 반환
│ 반환: A
│ 반환: A × A = A²
│ 반환: A² × A² = A⁴
└─▶ × A ← 홀수라 떼어낸 항
반환: A⁴ × A = A⁵
반환: A⁵ × A⁵ = A¹⁰ ✅
| 방법 | A^10 계산 | 곱셈 횟수 | A^100억 계산 |
|---|---|---|---|
| 단순 반복 | A×A×…×A | 9번 | 100억 번 |
| 분할정복 | A¹→A²→A⁴→A⁵→A¹⁰ | 4번 | 약 37번 |
2 분할정복 문제 풀이
분할정복의 전형적인 백준 문제들을 풀이한다. 쿼드트리(4분할), 구구단(9분할), 거듭제곱(지수 분할), 행렬 거듭제곱 유형을 모두 다룬다.
BOJ #2630
색종이 만들기
N×N 격자에서 모든 칸이 같은 색이면 그 색 종이 1장으로 카운트하고, 다르면 4등분해서 재귀적으로 반복한다. 흰 종이와 파란 종이의 수를 출력한다.
풀이 과정
- 구역 내 모든 칸을 순회해 첫 번째 칸의 색과 비교한다. 다른 색 발견 즉시
break outer로 이중 루프 탈출 - 전부 같은 색이면 해당 색 카운트 +1 후
return - 다르면
size/2로 4등분 — 좌상 → 우상 → 좌하 → 우하 순서로 재귀 호출
import java.io.*;
import java.util.*;
public class Main {
static int white = 0, blue = 0;
static int[][] arr;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine().trim());
arr = new int[n][n];
for (int i = 0; i < n; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
for (int j = 0; j < n; j++)
arr[i][j] = Integer.parseInt(st.nextToken());
}
check(0, 0, n);
System.out.println(white);
System.out.println(blue);
}
static void check(int row, int col, int size) {
boolean all = true;
int first = arr[row][col];
outer:
for (int i = row; i < row + size; i++){
for (int j = col; j < col + size; j++)
if (arr[i][j] != first) {
all = false;
break outer;
}
}
if (all) { if (first == 1) blue++; else white++; return; }
int h = size / 2;
check(row, col, h); // 좌상
check(row, col + h, h); // 우상
check(row + h, col, h); // 좌하
check(row + h, col + h, h); // 우하
}
}
4분할 순서 주의
재귀 호출 순서는 반드시 좌상 → 우상 → 좌하 → 우하여야 한다. row+half는 아래쪽 이동이므로 두 번째에 오면 좌하가 되어버린다.BOJ #1992
쿼드트리
2630과 동일한 4분할 구조지만, 결과를 숫자 카운트가 아니라 쿼드트리 형식의 문자열로 출력한다. 4분할이 발생할 때마다 괄호로 감싼다.
2630과의 차이점
- 입력이
"0001"처럼 공백 없이 붙어있어line.charAt(j) - '0'으로 파싱해야 한다 - 4분할 진입 전
sb.append('('), 완료 후sb.append(')')로 괄호를 감싼다 - 전부 같은 색이면
sb.append(0)또는sb.append(1)로 바로 추가하고 return
static void check(int row, int col, int size) {
boolean all = true;
int first = arr[row][col];
outer:
for (int i = row; i < row + size; i++)
for (int j = col; j < col + size; j++)
if (arr[i][j] != first) { all = false; break outer; }
if (all) { sb.append(first); return; }
int h = size / 2;
sb.append('(');
check(row, col, h);
check(row, col + h, h);
check(row + h, col, h);
check(row + h, col + h, h);
sb.append(')');
}
BOJ #1780
종이의 개수
2630의 9분할 버전. -1, 0, 1 세 가지 값이 있고, 모두 같으면 해당 카운트 +1, 다르면 3×3으로 9등분해서 재귀 호출한다.
2630과의 차이점
- 입력에 -1이 있어
charAt이 아닌StringTokenizer로 파싱해야 한다 - 4분할이 아닌 9분할 —
size/3으로 나누고 이중 for문으로 9개 구역 재귀 호출 - 카운트 변수가
zero, one, minus_one세 가지
// 9분할 재귀 호출
int third = size / 3;
for (int di = 0; di < 3; di++)
for (int dj = 0; dj < 3; dj++)
check(row + di * third, col + dj * third, third);
BOJ #1629
곱셈
A^B mod C를 구하는 문제. B가 최대 약 21억이라 단순 반복 곱셈은 시간 초과. 지수를 절반씩 쪼개는 분할정복으로 O(log B)에 해결한다.
풀이 과정
- A^B = A^(B/2) × A^(B/2) (짝수) / A^B = A^(B/2) × A^(B/2) × A (홀수)
- 중간 곱셈 결과가 int 범위를 초과하므로 모든 변수를
long으로 선언 - 매 곱셈마다 mod 연산 적용 —
(half * half) % mod
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
long a = Long.parseLong(st.nextToken());
long b = Long.parseLong(st.nextToken());
long c = Long.parseLong(st.nextToken());
System.out.println(pow(a, b, c));
}
static long pow(long a, long b, long mod) {
if (b == 1) return a % mod; // 기저 조건
long half = pow(a, b / 2, mod); // Divide
long result = (half * half) % mod; // Combine
if (b % 2 == 1) result = (result * a) % mod; // 홀수 처리
return result;
}
}
long을 써야 하는 이유
A와 C가 최대 2,147,483,647(int 최대값)이므로 A × A는 약 4.6 × 10¹⁸이 될 수 있다. int 범위를 훨씬 초과하므로 반드시 long을 사용해야 한다.BOJ #10830
행렬 제곱
N×N 정사각행렬 A와 지수 B가 주어졌을 때 A^B mod 1000 을 구하는 문제. B가 최대 1,000억이라 행렬을 직접 B번 곱하는 건 불가능하고, 1629번과 동일한 분할정복 구조를 행렬에 적용한다.
풀이 과정
- 행렬 곱셈 함수 multiply() 구현 — 3중 for문으로 C[i][j] 계산, 매 원소마다 % MOD 적용
- 분할정복 matPow(): b가 짝수면 half×half, 홀수면 prev×A — 재귀로 b를 절반씩 줄여나감
- 입력할 때부터 % MOD 적용, B는 최대 1,000억이므로 반드시 long으로 선언
import java.io.*;
import java.util.*;
public class Main {
static int n;
static final long MOD = 1000;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
n = Integer.parseInt(st.nextToken());
long b = Long.parseLong(st.nextToken()); // 최대 1,000억 → long!
// 1단계: 행렬 입력 (입력부터 % MOD)
long[][] arr = new long[n][n];
for (int i = 0; i < n; i++) {
st = new StringTokenizer(br.readLine());
for (int j = 0; j < n; j++)
arr[i][j] = Long.parseLong(st.nextToken()) % MOD;
}
// 2단계: 분할정복으로 A^B 계산
long[][] result = matPow(arr, b);
// 3단계: 출력
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
sb.append(result[i][j] % MOD);
if (j < n - 1) sb.append(' ');
}
sb.append('\n');
}
System.out.print(sb);
}
// 분할정복 행렬 거듭제곱
static long[][] matPow(long[][] arr, long b) {
if (b == 1) return arr;
if (b % 2 == 0) {
long[][] half = matPow(arr, b / 2);
return multiply(half, half);
} else {
long[][] prev = matPow(arr, b - 1);
return multiply(prev, arr);
}
}
// 행렬 곱셈
static long[][] multiply(long[][] X, long[][] Y) {
long[][] C = new long[n][n];
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) {
for (int t = 0; t < n; t++)
C[i][j] += X[i][t] * Y[t][j];
C[i][j] %= MOD;
}
return C;
}
}
3 유형별 정리
| 문제 | 분할 방식 | 기저 조건 | 핵심 포인트 |
|---|---|---|---|
| 2630 색종이 | 4분할 (size/2) | 전부 같은 색 | white/blue 카운트 |
| 1992 쿼드트리 | 4분할 (size/2) | 전부 같은 색 | 괄호 ( ) 감싸기 |
| 1780 종이의 개수 | 9분할 (size/3) | 전부 같은 값 | -1/0/1 세 카운트 |
| 1629 곱셈 | 지수 절반 (b/2) | b == 1 | long 필수, 홀수 처리 |
| 10830 행렬 제곱 | 지수 절반 (b/2) | b == 1 | multiply() 함수 분리, B는 long |
공통 실수 패턴
① 4분할 순서 오류 (좌상→우상→좌하→우하 순서 지킬 것) ② static 메서드에서 지역변수 배열 접근 불가 (클래스 필드로 선언) ③ 입력 파싱 방식 혼동 (공백 구분은 StringTokenizer, 붙어있으면 charAt) ④ 지수가 클 때 long 미사용 (int 최대값 약 21억 초과 주의)| 잘못된 내용이 있다면 지적부탁드립니다. 방문해주셔서 감사합니다. |

반응형