diff --git a/brute-force/backtracking/backtracking.md b/brute-force/backtracking/backtracking.md index 133b017..edc9982 100644 --- a/brute-force/backtracking/backtracking.md +++ b/brute-force/backtracking/backtracking.md @@ -26,9 +26,10 @@ * **N-Queen 문제**: 체스판에 N개의 퀸을 배치 * **스도쿠 풀이**: 빈 칸에 숫자 채우기 * **미로 찾기**: 모든 경로 탐색 -* **중복 순열/조합 생성** (N과 M 시리즈) -* **알파벳 문제** (1987): 같은 알파벳을 두 번 지나지 않고 최대 경로 찾기 -* **부분 집합 생성** +* **순열/조합 생성**: 주어진 집합에서 특정 개수의 순열이나 조합 생성 +* **제약 조건 탐색**: 특정 조건을 만족하는 경로나 해 찾기 +* **부분 집합 생성**: 주어진 집합의 모든 부분 집합 생성 +* **문자열 조합 생성**: 문자열에서 특정 크기의 조합 생성 --- @@ -90,7 +91,10 @@ void backtrack(상태) { ➡️ 상태를 되돌리지 않으면, 한 경로의 상태가 다른 경로 탐색을 방해합니다. -> **구체적인 예시**: 알파벳 문제(1987)의 상세한 구현은 [boj-1987 문제 문서](./boj-1987/)를 참고하세요. +**사고 과정:** +- 한 경로를 탐색할 때 상태를 변경함 +- 그 경로 탐색이 끝나면 상태를 원래대로 되돌려야 함 +- 그래야 다른 경로를 탐색할 때 이전 경로의 영향이 없음 --- @@ -135,7 +139,10 @@ void dfs(정점) { * **백트래킹**: 재귀 호출 후 상태를 되돌려서 **다른 경로를 탐색**할 수 있게 함 * **일반 DFS**: 상태를 되돌리지 않아서 **한 번 방문한 정점은 다시 방문하지 않음** -> **구체적인 예시**: 알파벳 문제(1987)와 순열 사이클 문제(10451)의 상세한 비교는 각 문제 문서를 참고하세요. +**사고 과정:** +- 백트래킹은 "모든 가능한 경로를 탐색"해야 할 때 사용 +- 일반 DFS는 "그래프의 구조를 파악"할 때 사용 +- 상태를 되돌리느냐 마느냐가 핵심 차이 ### 왜 백트래킹은 보통 DFS로 구현되는가? @@ -151,7 +158,7 @@ void dfs(정점) { | **목적** | 모든 경로 탐색 (최대값, 모든 해 찾기) | 그래프 탐색 (연결 요소, 사이클) | | **대상** | 상태 공간 트리 | 그래프 (정점, 간선) | | **구현 방식** | DFS (재귀) | DFS (재귀/스택) | -| **사용 예시** | N-Queen, 스도쿠, 알파벳(1987) | 순열 사이클(10451), 연결 요소 | +| **사용 예시** | 모든 경로 탐색, 제약 조건 탐색 | 그래프 연결성, 사이클 탐지 | | **핵심 특징** | 상태 되돌리기 | 깊이 우선 탐색 | **결론**: 백트래킹은 "상태를 되돌리는 특수한 형태의 DFS"라고 이해하면 됩니다. @@ -200,7 +207,10 @@ void backtrack(상태) { * 다음 반복에서 자동으로 덮어씌워짐 * 명시적 되돌리기 불필요 (암묵적 되돌리기) -> **구체적인 예시**: 알파벳 문제(1987)와 중복 순열 문제(15651)의 상세한 구현은 각 문제 문서를 참고하세요. +**사고 과정:** +- 상태가 독립적이면 자동으로 덮어씌워지므로 되돌릴 필요 없음 +- 상태가 공유되면 명시적으로 되돌려야 함 +- 문제의 특성에 따라 판단해야 함 ### 핵심 요소 @@ -278,6 +288,7 @@ sequenceDiagram | **순열** | 각 수는 한 번만 사용 | 필요 | 불필요 | N!/(N-M)! | | **조합** | 순서 무관, 중복 없음 | 필요 (이전 수 이후만) | 불필요 | N!/(M!(N-M)!) | | **중복 조합** | 순서 무관, 중복 허용 | 필요 (현재 수 이후만) | 불필요 | (N+M-1)!/(M!(N-1)!) | +| **문자열 조합 생성** | 문자열에서 특정 크기의 조합 생성 | 불필요 (인덱스로 제어) | 필요 (남은 문자 부족 시) | C(n, r) | | **제약 조건 문제** | 특정 조건 만족해야 함 | 필요 | 필요 | 문제에 따라 다름 | ### 중복 순열의 특수성 @@ -419,7 +430,10 @@ void backtrack(상태) { * **패턴 1**: 배열 인덱스에 값 할당 → 다음 반복에서 자동 덮어씌워짐 → 암묵적 되돌리기 * **패턴 2**: 공유 상태(배열, 전역 변수) → 자동 덮어씌워지지 않음 → 명시적 되돌리기 필수 -> **구체적인 예시**: 알파벳 문제(1987)의 상세한 구현은 [boj-1987 문제 문서](./boj-1987/)를 참고하세요. +**사고 과정:** +- 상태가 독립적인지 공유되는지 판단 +- 독립적이면 자동으로 처리됨 +- 공유되면 명시적으로 되돌려야 함 ### 패턴 3: 명시적 상태 되돌리기 (StringBuilder 사용) @@ -455,10 +469,167 @@ void backtrack(int depth) { * 다음 반복에서 이전 값이 남아있으면 잘못된 결과가 됨 * 따라서 **명시적으로 `sb.setLength(...)`로 되돌려야** 함 +### 패턴 4: 인덱스 기반 조합 생성 (문자열 조합) + +```java +static void generateCombinations( + String order, int courseSize, int start, + StringBuilder current, Map countMap +) { + // 종료 조건 + if (current.length() == courseSize) { + countMap.put(current.toString(), ...); + return; + } + + // 가지치기 + if (order.length() - start < courseSize - current.length()) { + return; + } + + // 선택지 탐색 + for (int i = start; i < order.length(); i++) { + current.append(order.charAt(i)); // 선택 + generateCombinations(order, courseSize, i + 1, current, countMap); + current.setLength(current.length() - 1); // ✅ 상태 되돌리기 + } +} +``` + +**특징:** +* 인덱스(`start`)로 선택 가능한 범위 제어 +* StringBuilder로 누적 상태 관리 +* 가지치기로 불가능한 경우 조기 종료 +* 인자로 모든 정보 전달 (독립적인 호출 가능) + +**사고 과정:** +- 각 호출이 독립적이어야 하는지 판단 +- 독립적이면 인자로 전달 +- 공유되면 전역 변수 사용 + **패턴 비교:** * **패턴 1**: 배열 인덱스 → 자동 덮어씌워짐 → 암묵적 되돌리기 * **패턴 2**: boolean 배열 → 자동 덮어씌워지지 않음 → 명시적 되돌리기 필수 * **패턴 3**: StringBuilder → 자동 덮어씌워지지 않음 → 명시적 되돌리기 필수 +* **패턴 4**: 인덱스 + StringBuilder → 자동 덮어씌워지지 않음 → 명시적 되돌리기 필수 + +--- + +## 8-1️⃣ 백트래킹 함수 구성 패턴 + +### 일반적인 함수 구성 패턴들 + +백트래킹 문제마다 함수 구성이 조금씩 다르지만, **공통된 패턴**이 있습니다. + +#### 패턴 1: 깊이(depth) 기반 + +**적용 상황**: 순열/조합 생성, 고정된 개수의 선택 + +```java +static void backtrack(int depth) { + if (depth == 목표_깊이) { + // 결과 처리 + return; + } + + for (int i = 시작값; i <= 끝값; i++) { + result[depth] = i; // 선택 + backtrack(depth + 1); // 재귀 호출 + // 상태 되돌리기 불필요 (다음 반복에서 덮어씌워짐) + } +} +``` + +**사고 과정:** +- 몇 개를 선택해야 하는지가 명확할 때 +- 각 단계가 독립적일 때 +- 전역 변수로 상태 관리 + +#### 패턴 2: 위치 기반 + +**적용 상황**: 2차원 보드 탐색, 그래프 탐색 + +```java +static void backtrack(int r, int c, int count) { + if (제약_조건_위반) { + return; // 가지치기 + } + + visited[상태] = true; // 상태 변경 + + for (int i = 0; i < 방향_개수; i++) { + int nr = r + dr[i]; + int nc = c + dc[i]; + if (isValid(nr, nc)) { + backtrack(nr, nc, count + 1); + } + } + + visited[상태] = false; // ✅ 상태 되돌리기 필수! +} +``` + +**사고 과정:** +- 위치 정보가 핵심일 때 +- 공유 상태를 사용할 때 +- 상태 되돌리기가 필수 + +#### 패턴 3: 인덱스 + 누적 상태 + +**적용 상황**: 문자열 조합, 독립적인 호출 필요 + +```java +static void generateCombinations( + String source, int targetSize, int start, + StringBuilder current, Map result +) { + if (current.length() == targetSize) { + result.put(current.toString(), ...); + return; + } + + if (가지치기_조건) { + return; + } + + for (int i = start; i < source.length(); i++) { + current.append(source.charAt(i)); // 선택 + generateCombinations(source, targetSize, i + 1, current, result); + current.setLength(current.length() - 1); // ✅ 상태 되돌리기 + } +} +``` + +**사고 과정:** +- 각 호출이 독립적이어야 할 때 +- 누적 상태를 관리할 때 +- 인자로 모든 정보 전달 + +### 패턴 선택 가이드 + +**어떤 패턴을 선택할까?** + +1. **깊이 기반**: 선택할 개수가 명확하고, 각 단계가 독립적 +2. **위치 기반**: 위치 정보가 핵심이고, 공유 상태 사용 +3. **인덱스 기반**: 독립적인 호출이 필요하고, 누적 상태 관리 + +### 함수 설계 원칙 + +**사고 과정:** + +1. **상태 정보 결정** + - 현재 진행 상황은 무엇인가? (depth, 위치, 인덱스) + - 현재 상태는 무엇인가? (선택한 값들, 방문 정보) + - 목표 정보는 무엇인가? (목표 크기, 목표 위치) + - 결과는 어디에 저장할까? (전역 변수, 인자) + +2. **전역 vs 인자** + - 여러 호출에서 공유해야 하는가? → 전역 + - 각 호출이 독립적이어야 하는가? → 인자 + +3. **상태 되돌리기 판단** + - 상태가 공유되는가? → 필수 + - 상태가 독립적인가? → 불필요 --- diff --git a/brute-force/backtracking/programmers-72411/1.analysis.md b/brute-force/backtracking/programmers-72411/1.analysis.md new file mode 100644 index 0000000..45f2271 --- /dev/null +++ b/brute-force/backtracking/programmers-72411/1.analysis.md @@ -0,0 +1,1268 @@ +# 메뉴 리뉴얼 + +## 📌 문제를 쉽게 이해하기 + +### 🍽️ 문제 상황 + +레스토랑을 운영하는 스카피가 있습니다. 손님들이 어떤 메뉴들을 **함께** 주문하는지 분석해서, 인기 있는 메뉴 조합을 코스요리로 만들고 싶어합니다. + +**예를 들어:** +- 손님 1: A, B, C 메뉴 주문 +- 손님 2: A, C 메뉴 주문 +- 손님 3: C, D, E 메뉴 주문 + +이 경우 "A와 C"를 함께 주문한 손님이 2명이므로, "AC" 코스요리를 만들 수 있습니다! + +### 🎯 문제의 핵심 + +**"어떤 메뉴 조합이 가장 많이 함께 주문되었는가?"** + +--- + +## 🔍 문제를 단계별로 이해하기 + +### 1단계: 입력 이해하기 + +**입력 1: `orders` (손님들의 주문 내역)** +``` +orders = ["ABCFG", "AC", "CDE", "ACDE", "BCFG", "ACDEH"] +``` +- 각 문자열은 한 손님이 주문한 메뉴들입니다 +- "ABCFG" = 손님 1이 A, B, C, F, G 메뉴를 주문 +- "AC" = 손님 2가 A, C 메뉴를 주문 +- ... (나머지도 동일) + +**입력 2: `course` (만들고 싶은 코스요리의 메뉴 개수)** +``` +course = [2, 3, 4] +``` +- 2개 메뉴로 구성된 코스요리 찾기 +- 3개 메뉴로 구성된 코스요리 찾기 +- 4개 메뉴로 구성된 코스요리 찾기 + +### 2단계: 조합이란? (완전히 이해하기) + +**조합 = 메뉴들을 순서 없이 선택하는 것** + +#### 🎯 조합을 쉽게 이해하는 방법 + +손님 1이 **"ABC"** 메뉴를 주문했다고 가정해봅시다. + +**2개 조합 만들기:** +- A와 B를 선택 → "AB" +- A와 C를 선택 → "AC" +- B와 C를 선택 → "BC" + +**총 3개의 조합이 나옵니다!** + +#### 📝 더 큰 예시: "ABCFG"에서 2개 조합 + +손님 1이 **"ABCFG"** (A, B, C, F, G)를 주문했다면: + +**2개 조합을 만드는 방법:** +1. A를 선택 → B, C, F, G 중 하나 더 선택 + - AB, AC, AF, AG (4개) +2. A를 선택하지 않음 → B를 선택 → C, F, G 중 하나 더 선택 + - BC, BF, BG (3개) +3. A, B를 선택하지 않음 → C를 선택 → F, G 중 하나 더 선택 + - CF, CG (2개) +4. A, B, C를 선택하지 않음 → F와 G 선택 + - FG (1개) + +**총 4 + 3 + 2 + 1 = 10개의 조합!** + +**3개 조합도 마찬가지:** +- ABC, ABF, ABG, ACF, ACG, AFG, BCF, BCG, BFG, CFG (총 10개) + +**4개 조합:** +- ABCF, ABCG, ABFG, ACFG, BCFG (총 5개) + +#### ⚠️ 중요한 점 + +1. **순서는 중요하지 않음**: "AC"와 "CA"는 같은 조합입니다! +2. **중복 없음**: 같은 메뉴를 두 번 선택하지 않습니다. +3. **알파벳 순으로 정렬**: 결과는 항상 알파벳 순으로 정렬됩니다 (예: "AC"가 맞고 "CA"는 아님) + +### 3단계: 빈도 카운트하기 (쉽게 이해하기) + +각 조합이 **몇 명의 손님**에게서 나왔는지 세어봅니다. + +#### 🔍 "AC" 조합이 몇 번 나왔는지 세어보기 + +**손님들의 주문:** +- 손님 1: "ABCFG" (A, B, C, F, G) +- 손님 2: "AC" (A, C) +- 손님 3: "CDE" (C, D, E) +- 손님 4: "ACDE" (A, C, D, E) +- 손님 5: "BCFG" (B, C, F, G) +- 손님 6: "ACDEH" (A, C, D, E, H) + +**"AC" 조합 확인하기:** +- 손님 1: A 있음 ✅, C 있음 ✅ → **"AC" 조합 가능!** +- 손님 2: A 있음 ✅, C 있음 ✅ → **"AC" 조합 가능!** +- 손님 3: A 없음 ❌ → "AC" 조합 불가능 +- 손님 4: A 있음 ✅, C 있음 ✅ → **"AC" 조합 가능!** +- 손님 5: A 없음 ❌ → "AC" 조합 불가능 +- 손님 6: A 있음 ✅, C 있음 ✅ → **"AC" 조합 가능!** + +**결과**: "AC"는 **4명의 손님** (손님 1, 2, 4, 6)에게서 나왔습니다! 🎉 + +#### 💡 핵심 포인트 + +**조합이 나왔다 = 해당 손님의 주문에 그 조합의 모든 메뉴가 포함되어 있다** + +예: "AC" 조합이 나왔다 = 손님의 주문에 A도 있고 C도 있다 + +### 4단계: 조건 확인하기 + +**조건 1**: 최소 2명 이상의 손님에게서 나온 조합만 유효 +- "AC"는 4명 → ✅ 유효 +- "AB"는 1명만 → ❌ 제외 + +**조건 2**: 각 코스 크기별로 **가장 많이** 주문된 조합 선택 +- 크기 2: "AC"가 4번으로 최대 → ✅ 선택 +- 크기 3: "CDE"가 3번으로 최대 → ✅ 선택 +- 크기 4: "BCFG"와 "ACDE"가 각각 2번으로 최대 → ✅ 둘 다 선택 + +### 5단계: 정렬하기 + +결과를 알파벳 순으로 정렬합니다. +``` +["AC", "ACDE", "BCFG", "CDE"] +``` + +--- + +## 📥 입력 조건 (간단 정리) + +* `orders`: 손님들의 주문 내역 + * 손님 수: 2명 이상 20명 이하 + * 각 손님이 주문한 메뉴 개수: 2개 이상 10개 이하 + * 메뉴는 알파벳 대문자로 표시 (A, B, C, ...) + * 같은 메뉴를 중복 주문하지 않음 + +* `course`: 만들고 싶은 코스요리의 메뉴 개수 + * 예: [2, 3, 4] = 2개짜리, 3개짜리, 4개짜리 코스요리 찾기 + +--- + +## 📤 출력 조건 (간단 정리) + +* 각 코스요리 메뉴 구성을 문자열로 반환 +* 알파벳 순으로 정렬 +* 예: ["AC", "ACDE", "BCFG", "CDE"] + +--- + +## 📥 입력 조건 + +* `orders`: 배열의 크기는 2 이상 20 이하 + * 각 원소는 크기가 2 이상 10 이하인 문자열 + * 각 문자열은 알파벳 대문자로만 이루어져 있음 + * 각 문자열에는 같은 알파벳이 중복해서 들어있지 않음 +* `course`: 배열의 크기는 1 이상 10 이하 + * 각 원소는 2 이상 10 이하인 자연수가 오름차순으로 정렬되어 있음 + * 배열에는 같은 값이 중복해서 들어있지 않음 +* `orders`와 `course` 매개변수는 return 하는 배열의 길이가 1 이상이 되도록 주어짐 + +--- + +## 📤 출력 조건 + +* 각 코스요리 메뉴의 구성을 문자열 형식으로 배열에 담아 사전 순으로 오름차순 정렬해서 반환 +* 배열의 각 원소에 저장된 문자열 또한 알파벳 오름차순으로 정렬되어야 함 +* 반환 타입: `String[]` + +--- + +## 💡 문제를 어떻게 풀까요? + +### 🤔 문제의 핵심 + +**"각 손님의 주문에서 가능한 모든 조합을 만들어서, 가장 많이 나온 조합을 찾자!"** + +### 🛠️ 해결 방법 (쉽게 설명) + +**1단계: 각 손님의 주문에서 조합 만들기** +- 손님 1이 "ABCFG"를 주문했다면 +- 2개 조합: AB, AC, AF, AG, BC, BF, BG, CF, CG, FG +- 3개 조합: ABC, ABF, ABG, ACF, ACG, AFG, BCF, BCG, BFG, CFG +- 4개 조합: ABCF, ABCG, ABFG, ACFG, BCFG + +**2단계: 모든 조합의 빈도 세기** +- "AC"가 몇 번 나왔는지 세기 +- "BCFG"가 몇 번 나왔는지 세기 +- ... (모든 조합에 대해) + +**3단계: 조건에 맞는 조합만 선택** +- 최소 2명 이상의 손님에게서 나온 조합만 +- 각 코스 크기별로 가장 많이 나온 조합만 + +**4단계: 정렬해서 반환** + +### 📋 해결 과정 요약 + +``` +1. 각 손님의 주문을 알파벳 순으로 정렬 (예: "ABCFG" → "ABCFG") +2. 각 주문에서 course 배열에 있는 크기의 조합 생성 + - 백트래킹으로 모든 조합 만들기 +3. 각 조합이 몇 번 나왔는지 카운트 (Map 사용) +4. 각 코스 크기별로: + - 최소 2명 이상의 손님에게서 나온 조합만 고려 + - 가장 많이 나온 조합 선택 (같으면 모두 선택) +5. 결과를 알파벳 순으로 정렬 +``` + +--- + +## 🎯 왜 백트래킹을 사용해야 할까요? + +### 🤔 문제의 핵심 난제 + +**"ABCFG"에서 2개 조합을 어떻게 만들까요?** + +손으로 직접 만들어보면: +- AB, AC, AF, AG, BC, BF, BG, CF, CG, FG + +하지만 **프로그램으로 자동으로** 만들려면 어떻게 해야 할까요? + +### ❌ 다른 방법들의 한계 + +#### 방법 1: 반복문으로 직접 만들기 + +```java +// 이렇게 하면 어떨까? +for (int i = 0; i < order.length(); i++) { + for (int j = i + 1; j < order.length(); j++) { + // 2개 조합 만들기 + } +} +``` + +**문제점:** +- ✅ 2개 조합은 가능 +- ❌ 3개 조합은? → 반복문 3중첩 필요 +- ❌ 4개 조합은? → 반복문 4중첩 필요 +- ❌ **코스 크기가 동적으로 변할 수 있어서 불가능!** + +`course = [2, 3, 4]`인데, 반복문을 몇 중첩해야 할지 모릅니다! + +#### 방법 2: 비트마스크 사용하기 + +```java +// 모든 경우를 비트로 표현 +for (int mask = 0; mask < (1 << order.length()); mask++) { + if (Integer.bitCount(mask) == courseSize) { + // 조합 만들기 + } +} +``` + +**문제점:** +- ✅ 동적 크기 조합 생성 가능 +- ❌ **불필요한 경우도 모두 탐색** (예: 크기가 2가 아닌 경우도 탐색) +- ❌ **비효율적**: 2^10 = 1024가지 모두 확인해야 함 +- ❌ 가지치기(조기 종료)가 어려움 + +#### 방법 3: DP(동적 프로그래밍) 사용하기 + +```java +// DP로 조합 개수만 구할 수 있음 +int[][] dp = new int[n+1][r+1]; +dp[i][j] = i개 중 j개를 선택하는 조합의 개수 +``` + +**문제점:** +- ❌ **조합의 개수만 구할 수 있고, 실제 조합을 만들 수 없음!** +- ❌ 이 문제는 **모든 조합을 실제로 생성**해야 함 (빈도 카운트를 위해) +- ❌ DP는 최적값을 구하는 데 유용하지만, **모든 경우를 나열하는 데는 부적합** +- ❌ 메모이제이션의 이점이 없음 (각 주문마다 독립적으로 조합 생성 필요) + +**왜 DP가 부적합한가?** + +1. **문제의 목적이 다름** + - DP: 최적값(최대, 최소, 개수 등)을 구하는 데 유용 + - 이 문제: **모든 조합을 실제로 생성**해야 함 + +2. **중복되는 부분 문제가 없음** + - 각 주문("ABCFG", "AC", ...)은 독립적 + - 한 주문의 조합 생성이 다른 주문의 조합 생성에 도움이 되지 않음 + - DP의 핵심인 "메모이제이션"의 이점이 없음 + +3. **모든 조합을 생성해야 함** + - DP로는 "ABCFG에서 2개 조합이 몇 개인가?" (10개)만 알 수 있음 + - 하지만 우리는 **"AB", "AC", "AF", ... 실제 조합들**이 필요함 + - 각 조합을 Map에 저장해서 빈도를 카운트해야 함 + +**예시:** +```java +// DP로는 이렇게만 가능 +int count = dp[5][2]; // "ABCFG"에서 2개 조합의 개수 = 10 + +// 하지만 우리에게 필요한 것은: +// "AB", "AC", "AF", "AG", "BC", "BF", "BG", "CF", "CG", "FG" +// 이 실제 조합들을 만들어서 Map에 저장해야 함! +``` + +#### 방법 4: 수학 공식 사용하기 + +조합 공식 C(n, r) = n! / (r! × (n-r)!)로 개수는 알 수 있지만... + +**문제점:** +- ❌ **어떤 조합인지는 알 수 없음!** +- ❌ 실제 조합을 만들어야 하는데 공식만으로는 불가능 + +### ✅ 백트래킹이 최적인 이유 + +#### 1️⃣ 동적 크기 조합 생성 가능 + +**백트래킹은 재귀를 사용하므로, 조합 크기가 변해도 코드가 동일합니다!** + +```java +// 실제 함수 시그니처 +static void generateCombinations( + String order, // 주문 문자열 (예: "ABCFG") + int courseSize, // 만들 조합의 크기 (예: 2, 3, 4) + int start, // 현재 선택할 수 있는 시작 인덱스 + StringBuilder current, // 현재까지 선택한 문자들 (예: "A", "AB") + Map countMap // 조합의 빈도를 카운트하는 Map +) + +// 크기가 2든 3든 4든 같은 함수 사용 +generateCombinations("ABCFG", 2, 0, new StringBuilder(), countMap); // 2개 조합 +generateCombinations("ABCFG", 3, 0, new StringBuilder(), countMap); // 3개 조합 +generateCombinations("ABCFG", 4, 0, new StringBuilder(), countMap); // 4개 조합 +``` + +**각 인자의 의미:** + +1. **`order`** (String): 조합을 만들 원본 주문 문자열 + - 예: "ABCFG" = 손님이 주문한 메뉴들 + - **왜 전체 문자열을 전달하나요?** → 인덱스로 접근하기 위해! + +2. **`courseSize`** (int): 만들고 싶은 조합의 크기 + - 예: 2 = 2개 메뉴로 구성된 조합 (AB, AC, ...) + - 예: 3 = 3개 메뉴로 구성된 조합 (ABC, ABF, ...) + +3. **`start`** (int): 현재 단계에서 선택할 수 있는 시작 위치 + - 예: 0 = 처음부터 선택 가능 + - 예: 2 = 2번째 문자부터 선택 가능 (이미 앞에서 선택했으므로) + - **핵심**: `order.substring(start)`와 같은 효과지만 더 효율적! + +4. **`current`** (StringBuilder): 현재까지 선택한 문자들을 저장 + - 예: "" = 아직 아무것도 선택 안 함 + - 예: "A" = A를 선택함 + - 예: "AB" = A와 B를 선택함 + +5. **`countMap`** (Map): 생성된 조합의 빈도를 카운트하는 맵 + - 예: {"AC": 4, "BC": 2, ...} = "AC" 조합이 4번 나왔음 + +--- + +### 🤔 왜 "남은 문자들"을 인자로 전달하지 않았을까요? + +**질문**: 왜 `generateCombinations(String remainingChars, ...)` 같이 남은 문자들만 전달하지 않았나요? + +**대안 1: 남은 문자들을 전달하는 방법** +```java +// 이렇게 할 수도 있었음 +generateCombinations("BCFG", 2, new StringBuilder(), countMap); // A 선택 안 함 +generateCombinations("CFG", 1, new StringBuilder("A"), countMap); // A 선택함 +``` + +**대안 2: 현재 방법 (인덱스 사용)** +```java +// 실제 구현 +generateCombinations("ABCFG", 2, 1, new StringBuilder(), countMap); // start=1부터 +generateCombinations("ABCFG", 2, 1, new StringBuilder("A"), countMap); // A 선택함 +``` + +**왜 인덱스를 사용하는 방법을 선택했을까요?** + +#### ✅ 인덱스 사용의 장점 + +1. **메모리 효율성** + ```java + // 방법 1: 부분 문자열 생성 (비효율적) + String remaining = order.substring(start); // 새로운 문자열 객체 생성! + generateCombinations(remaining, ...); + + // 방법 2: 인덱스 사용 (효율적) + generateCombinations(order, courseSize, start, ...); // 기존 문자열 재사용! + ``` + - 부분 문자열을 만들면 **새로운 메모리 할당**이 필요함 + - 인덱스 사용은 **기존 문자열을 재사용**하므로 메모리 효율적 + +2. **성능** + ```java + // 부분 문자열 생성: O(n) 시간 소요 + String remaining = order.substring(start); // 복사 작업 필요 + + // 인덱스 접근: O(1) 시간 소요 + char c = order.charAt(start); // 직접 접근 + ``` + - 부분 문자열 생성은 **복사 작업**이 필요해 느림 + - 인덱스 접근은 **직접 접근**이므로 빠름 + +3. **간단함** + ```java + // 방법 1: 매번 부분 문자열 생성 + generateCombinations(order.substring(1), ...); + generateCombinations(order.substring(2), ...); + + // 방법 2: 인덱스만 증가 + generateCombinations(order, courseSize, start + 1, ...); + ``` + - 인덱스만 증가시키면 되므로 **코드가 간단**함 + +#### 📊 비교 예시 + +**"ABCFG"에서 조합을 만들 때:** + +**방법 1: 부분 문자열 전달** +```java +generateCombinations("ABCFG", ...) + ├─ A 선택 → generateCombinations("BCFG", ...) // 새 문자열 생성! + │ ├─ B 선택 → generateCombinations("CFG", ...) // 또 새 문자열 생성! + │ └─ B 선택 안 함 → generateCombinations("CFG", ...) // 또 새 문자열 생성! + └─ A 선택 안 함 → generateCombinations("BCFG", ...) // 또 새 문자열 생성! +``` +- **문제**: 매번 새로운 문자열 객체 생성 → 메모리 낭비! + +**방법 2: 인덱스 사용 (현재 방법)** +```java +generateCombinations("ABCFG", 2, 0, ...) + ├─ A 선택 → generateCombinations("ABCFG", 2, 1, "A", ...) // 같은 문자열 재사용! + │ ├─ B 선택 → generateCombinations("ABCFG", 2, 2, "AB", ...) // 같은 문자열 재사용! + │ └─ B 선택 안 함 → generateCombinations("ABCFG", 2, 2, "A", ...) // 같은 문자열 재사용! + └─ A 선택 안 함 → generateCombinations("ABCFG", 2, 1, "", ...) // 같은 문자열 재사용! +``` +- **장점**: 같은 문자열을 재사용하므로 메모리 효율적! + +#### 🎯 결론 + +**"주어진 선택지"를 인자로 전달하지 않은 이유:** + +1. ✅ **메모리 효율성**: 부분 문자열을 만들지 않고 기존 문자열 재사용 +2. ✅ **성능**: 문자열 복사 없이 인덱스로 직접 접근 +3. ✅ **간단함**: 인덱스만 증가시키면 되므로 코드가 간단 + +**→ `order` 전체와 `start` 인덱스를 사용하는 것이 더 효율적입니다!** + +--- + +## 🔄 백트래킹 함수 구성 패턴: 일반적인 방법과 비교 + +### 📋 백트래킹 함수의 일반적인 패턴들 + +백트래킹 문제마다 함수 구성이 조금씩 다르지만, **공통된 패턴**이 있습니다. + +#### 패턴 1: 깊이(depth) 기반 (가장 일반적) + +**예시: 중복 순열 문제 (boj-15651)** + +```java +static void backtrack(int depth) { + // 종료 조건 + if (depth == M) { + // 결과 처리 + return; + } + + // 선택지 탐색 + for (int i = 1; i <= N; i++) { + result[depth] = i; // 선택 + backtrack(depth + 1); // 재귀 호출 + // 상태 되돌리기 불필요 (다음 반복에서 덮어씌워짐) + } +} +``` + +**특징:** +- `depth`: 현재까지 선택한 개수 +- 전역 변수 사용 (`result[]`, `N`, `M`) +- 상태 되돌리기 불필요 (배열 인덱스에 직접 할당) + +#### 패턴 2: 위치 기반 (2차원 보드 탐색) + +**예시: 알파벳 문제 (boj-1987)** + +```java +static void backtrack(int r, int c, int count) { + // 종료 조건 (가지치기) + if (visited[알파벳]) { + return; + } + + // 상태 변경 + visited[알파벳] = true; + + // 선택지 탐색 (상하좌우) + for (int i = 0; i < 4; i++) { + int nr = r + dr[i]; + int nc = c + dc[i]; + if (isValid(nr, nc)) { + backtrack(nr, nc, count + 1); + } + } + + // ✅ 상태 되돌리기 필수! + visited[알파벳] = false; +} +``` + +**특징:** +- `r, c`: 현재 위치 +- `count`: 현재까지의 경로 길이 +- 전역 변수 사용 (`visited[]`, `board[][]`) +- **상태 되돌리기 필수** (공유 상태) + +#### 패턴 3: 인덱스 + 누적 상태 (이 문제) + +**예시: 메뉴 리뉴얼 (programmers-72411)** + +```java +static void generateCombinations( + String order, // 전체 문자열 + int courseSize, // 목표 크기 + int start, // 시작 인덱스 + StringBuilder current, // 현재까지 선택한 문자들 + Map countMap // 결과 저장 +) { + // 종료 조건 + if (current.length() == courseSize) { + countMap.put(combination, ...); + return; + } + + // 가지치기 + if (order.length() - start < courseSize - current.length()) { + return; + } + + // 선택지 탐색 + for (int i = start; i < order.length(); i++) { + current.append(order.charAt(i)); // 선택 + generateCombinations(order, courseSize, i + 1, current, countMap); + current.setLength(...); // ✅ 상태 되돌리기 필수! + } +} +``` + +**특징:** +- `start`: 현재 선택할 수 있는 시작 위치 +- `current`: 누적 상태 (StringBuilder) +- `countMap`: 결과 저장 (인자로 전달) +- **상태 되돌리기 필수** (StringBuilder는 누적 방식) + +### 🔍 패턴 비교표 + +| 패턴 | 주요 인자 | 상태 저장 | 상태 되돌리기 | 사용 예시 | +|------|----------|----------|--------------|----------| +| **깊이 기반** | `depth` | 전역 배열 (`result[]`) | 불필요 | 중복 순열, 순열, 조합 | +| **위치 기반** | `r, c, count` | 전역 배열 (`visited[]`) | **필수** | 보드 탐색, 미로 찾기 | +| **인덱스 기반** | `start, current` | 인자로 전달 (`StringBuilder`) | **필수** | 문자열 조합, 부분 집합 | + +### 💡 이 문제가 인덱스 기반 패턴을 사용하는 이유 + +#### 1️⃣ 문자열 조합 생성의 특성 + +**문제:** +- 각 주문마다 독립적으로 조합 생성 +- 여러 주문에 대해 반복 호출 필요 +- 전역 변수 사용 시 충돌 가능 + +**해결:** +- 인자로 모든 정보 전달 → **독립적인 호출** 가능 +- 각 주문마다 깨끗한 상태로 시작 + +#### 2️⃣ StringBuilder 사용의 필요성 + +**왜 StringBuilder를 사용하나요?** +```java +// 방법 1: String 사용 (비효율적) +String current = ""; +current = current + order.charAt(i); // 새 문자열 객체 생성! + +// 방법 2: StringBuilder 사용 (효율적) +StringBuilder current = new StringBuilder(); +current.append(order.charAt(i)); // 기존 객체 수정 +``` + +**문제:** +- String은 불변(immutable)이라 매번 새 객체 생성 +- StringBuilder는 가변(mutable)이라 효율적 +- 하지만 **누적 방식**이므로 상태 되돌리기 필수! + +#### 3️⃣ 결과 저장 방식 + +**다른 문제들:** +```java +// 전역 변수 사용 +static List result = new ArrayList<>(); +backtrack(...); +``` + +**이 문제:** +```java +// 인자로 전달 +Map countMap = new HashMap<>(); +generateCombinations(..., countMap); +``` + +**이유:** +- 각 주문마다 같은 Map에 저장해야 함 +- 빈도 카운트를 위해 공유 필요 +- 전역 변수보다 명시적 전달이 더 명확함 + +### 🎯 일반적인 백트래킹 함수 설계 원칙 + +#### 원칙 1: 상태 정보 결정 + +**질문: "어떤 정보가 필요할까?"** + +1. **현재 진행 상황**: `depth`, `start`, `count` 등 +2. **현재 상태**: `current`, `visited[]`, `result[]` 등 +3. **목표 정보**: `courseSize`, `target` 등 +4. **결과 저장**: `result`, `countMap` 등 + +#### 원칙 2: 전역 vs 인자 + +**전역 변수 사용:** +- ✅ 여러 재귀 호출에서 공유 필요 +- ✅ 상태 되돌리기가 간단한 경우 +- 예: `result[]`, `visited[]` + +**인자로 전달:** +- ✅ 각 호출이 독립적이어야 함 +- ✅ 여러 번 호출해야 하는 경우 +- 예: `countMap`, `current` (StringBuilder) + +#### 원칙 3: 상태 되돌리기 판단 + +**필요한 경우:** +- 공유 상태: `visited[]`, `StringBuilder`, 전역 변수 +- 다음 반복에서 자동으로 덮어씌워지지 않음 + +**불필요한 경우:** +- 독립 상태: 배열 인덱스에 직접 할당 (`result[depth] = i`) +- 다음 반복에서 자동으로 덮어씌워짐 + +### 📊 이 문제의 함수 구성이 일반적인가? + +**✅ 일반적인 패턴을 따르고 있습니다!** + +1. ✅ **종료 조건**: 목표 크기 달성 시 +2. ✅ **가지치기**: 불가능한 경우 조기 종료 +3. ✅ **선택지 탐색**: for 루프로 모든 선택지 탐색 +4. ✅ **상태 변경**: StringBuilder에 추가 +5. ✅ **재귀 호출**: 다음 단계로 진행 +6. ✅ **상태 되돌리기**: StringBuilder에서 제거 + +**차이점:** +- 인자로 많은 정보를 전달 (전역 변수 대신) +- → **더 명시적이고 독립적인 호출** 가능 +- → **여러 주문에 대해 반복 호출**하기에 적합 + +### 🎓 결론 + +**이 문제의 함수 구성은:** +- ✅ 백트래킹의 일반적인 패턴을 따름 +- ✅ 문자열 조합 생성에 적합한 변형 +- ✅ 인자 전달 방식으로 독립적인 호출 가능 +- ✅ **표준적인 백트래킹 구현입니다!** + +**다른 백트래킹 문제와의 차이는:** +- 문제 특성에 맞게 **인자 구성이 다를 뿐** +- 핵심 구조(종료 조건, 선택지 탐색, 상태 되돌리기)는 동일 + +**핵심 포인트:** +- `courseSize`만 바꾸면 2개 조합, 3개 조합, 4개 조합을 모두 만들 수 있음! +- 반복문은 크기가 변할 때마다 코드를 바꿔야 하지만, 백트래킹은 같은 함수 사용 + +**실제 동작 예시:** + +```java +// "ABCFG"에서 2개 조합 만들기 +generateCombinations("ABCFG", 2, 0, "", {}) + +// 재귀 호출 과정: +// 1. A 선택 → generateCombinations("ABCFG", 2, 1, "A", {}) +// - B 선택 → "AB" 완성! ✅ countMap에 추가 +// - C 선택 → "AC" 완성! ✅ countMap에 추가 +// - F 선택 → "AF" 완성! ✅ countMap에 추가 +// - G 선택 → "AG" 완성! ✅ countMap에 추가 +// 2. A 선택 안 함 → generateCombinations("ABCFG", 2, 1, "", {}) +// - B 선택 → generateCombinations("ABCFG", 2, 2, "B", {}) +// - C 선택 → "BC" 완성! ✅ +// - F 선택 → "BF" 완성! ✅ +// - G 선택 → "BG" 완성! ✅ +// - ... (계속) +``` + +**각 단계에서 인자가 어떻게 변하는지:** +- `start`: 0 → 1 → 2 → ... (다음 선택할 위치로 이동) +- `current`: "" → "A" → "AB" → ... (선택한 문자들이 누적) +- `courseSize`: 2 (고정, 목표 크기) +- `countMap`: 조합이 완성될 때마다 카운트 증가 + +#### 2️⃣ 가지치기로 효율적 탐색 + +**불가능한 경우를 미리 제외할 수 있습니다!** + +예: "ABC"에서 4개 조합을 만들려고 할 때 +- 남은 문자: 3개 +- 필요한 문자: 4개 +- **3 < 4이므로 불가능!** → 즉시 종료 + +반복문이나 비트마스크는 이런 최적화가 어렵습니다. + +#### 3️⃣ 체계적이고 빠짐없는 탐색 + +**백트래킹은 트리 구조로 모든 경우를 체계적으로 탐색합니다.** + +"ABC"에서 2개 조합을 만드는 과정: + +``` + 시작: "" + / | \ + A 선택 B 선택 C 선택 + / \ | + B 선택 C 선택 C 선택 + | | | + "AB" "AC" "BC" +``` + +**모든 조합을 빠짐없이 만들 수 있습니다!** + +#### 4️⃣ 상태 되돌리기로 깔끔한 구현 + +**한 경로를 탐색한 후, 상태를 되돌려서 다른 경로를 탐색합니다.** + +```java +current.append(order.charAt(i)); // 선택 +generateCombinations(...); // 재귀 호출 +current.setLength(...); // 선택 취소 (상태 되돌리기) +``` + +이렇게 하면 **같은 메모리 공간을 재사용**할 수 있어 효율적입니다! + +### 📊 방법 비교표 + +| 방법 | 동적 크기 | 실제 조합 생성 | 효율성 | 구현 난이도 | 가지치기 | 결론 | +|------|----------|--------------|--------|------------|---------|------| +| **반복문** | ❌ 불가능 | ✅ 가능 | - | 쉬움 | - | ❌ 사용 불가 | +| **비트마스크** | ✅ 가능 | ✅ 가능 | 낮음 (불필요한 경우도 탐색) | 중간 | 어려움 | ⚠️ 비효율적 | +| **DP** | ✅ 가능 | ❌ **불가능**
(개수만 구함) | - | 중간 | - | ❌ **목적 불일치** | +| **백트래킹** | ✅ 가능 | ✅ 가능 | 높음 (가지치기 가능) | 중간 | 쉬움 | ✅ **최적!** | + +### 🎯 결론 + +**백트래킹을 선택하는 이유:** + +1. ✅ **동적 크기 조합 생성**: `course` 배열의 크기가 변해도 대응 가능 +2. ✅ **효율적 탐색**: 가지치기로 불필요한 경우 제외 +3. ✅ **체계적 탐색**: 모든 조합을 빠짐없이 생성 +4. ✅ **깔끔한 구현**: 상태 되돌리기로 메모리 효율적 + +**→ 이 문제에는 백트래킹이 가장 적합한 알고리즘입니다!** + +--- + +### 🔍 백트래킹이 실제로 어떻게 동작하는가? + +**"ABCFG"에서 2개 조합을 만드는 과정을 시각화해봅시다:** + +#### 단계별 탐색 과정 + +``` +1단계: A를 선택할까? + ├─ [A 선택] → 2단계로 + └─ [A 선택 안 함] → 2단계로 + +2단계: B를 선택할까? + ├─ [A 선택 + B 선택] → "AB" 완성! ✅ + ├─ [A 선택 + B 선택 안 함] → 3단계로 + ├─ [A 선택 안 함 + B 선택] → 3단계로 + └─ [A 선택 안 함 + B 선택 안 함] → 3단계로 + +3단계: C를 선택할까? + ├─ [A 선택 + B 선택 안 함 + C 선택] → "AC" 완성! ✅ + ├─ [A 선택 + B 선택 안 함 + C 선택 안 함] → 4단계로 + ├─ [A 선택 안 함 + B 선택 + C 선택] → "BC" 완성! ✅ + └─ ... (계속) +``` + +#### 트리 구조로 보기 + +``` + 시작: "" + / \ + A 선택 (1개) A 선택 안 함 + / \ / \ + B 선택(2개) B 선택안함 B 선택(1개) B 선택안함 + "AB"✅ / \ "BC"✅ / \ + C 선택 C 선택안함 C 선택 C 선택안함 + "AC"✅ / \ "CF"✅ / \ + F 선택 F 선택안함 F 선택안함 + "AF"✅ / \ ... + G 선택 G 선택안함 + "AG"✅ ... +``` + +#### 핵심 포인트 + +1. **각 단계에서 선택/선택 안 함 두 가지 경우를 모두 탐색** +2. **원하는 크기(2개)를 만들면 즉시 저장하고 돌아감** +3. **불가능한 경우(남은 문자 부족)는 조기 종료** +4. **상태를 되돌려서 다른 경로 탐색 가능** + +#### 실제 코드 동작 (의사코드) + +```java +generateCombinations("ABCFG", 2, 0, "", {}) + ├─ A 선택 → generateCombinations("ABCFG", 2, 1, "A", {}) + │ ├─ B 선택 → "AB" 완성! ✅ (카운트 +1) + │ ├─ C 선택 → "AC" 완성! ✅ (카운트 +1) + │ ├─ F 선택 → "AF" 완성! ✅ (카운트 +1) + │ └─ G 선택 → "AG" 완성! ✅ (카운트 +1) + │ + ├─ A 선택 안 함 → generateCombinations("ABCFG", 2, 1, "", {}) + │ ├─ B 선택 → generateCombinations("ABCFG", 2, 2, "B", {}) + │ │ ├─ C 선택 → "BC" 완성! ✅ (카운트 +1) + │ │ ├─ F 선택 → "BF" 완성! ✅ (카운트 +1) + │ │ └─ G 선택 → "BG" 완성! ✅ (카운트 +1) + │ │ + │ ├─ B 선택 안 함 → generateCombinations("ABCFG", 2, 2, "", {}) + │ │ ├─ C 선택 → generateCombinations("ABCFG", 2, 3, "C", {}) + │ │ │ ├─ F 선택 → "CF" 완성! ✅ (카운트 +1) + │ │ │ └─ G 선택 → "CG" 완성! ✅ (카운트 +1) + │ │ │ + │ │ └─ C 선택 안 함 → generateCombinations("ABCFG", 2, 3, "", {}) + │ │ └─ F 선택 → generateCombinations("ABCFG", 2, 4, "F", {}) + │ │ └─ G 선택 → "FG" 완성! ✅ (카운트 +1) +``` + +**결과: AB, AC, AF, AG, BC, BF, BG, CF, CG, FG (총 10개) ✅** + +#### 왜 이 방법이 좋은가? + +1. **체계적**: 모든 경우를 빠짐없이 탐색 +2. **효율적**: 불가능한 경우는 조기 종료 +3. **유연함**: 크기가 변해도 같은 로직 사용 +4. **명확함**: 코드만 봐도 동작을 이해할 수 있음 + +### 📋 해결 과정 요약 + +``` +1. 각 손님의 주문을 알파벳 순으로 정렬 (예: "ABCFG" → "ABCFG") +2. 각 주문에서 course 배열에 있는 크기의 조합 생성 + - 백트래킹으로 모든 조합 만들기 +3. 각 조합이 몇 번 나왔는지 카운트 (Map 사용) +4. 각 코스 크기별로: + - 최소 2명 이상의 손님에게서 나온 조합만 고려 + - 가장 많이 나온 조합 선택 (같으면 모두 선택) +5. 결과를 알파벳 순으로 정렬 +``` + +--- + +## ✨ 예시로 완전히 이해하기 + +### 예시 1: 문제에서 제공한 예제 (단계별 상세 설명) + +**입력** +``` +orders = ["ABCFG", "AC", "CDE", "ACDE", "BCFG", "ACDEH"] +course = [2, 3, 4] +``` + +**손님별 주문 정리:** +- 손님 1: A, B, C, F, G +- 손님 2: A, C +- 손님 3: C, D, E +- 손님 4: A, C, D, E +- 손님 5: B, C, F, G +- 손님 6: A, C, D, E, H + +--- + +#### 🔍 단계 1: 각 손님의 주문에서 조합 만들기 + +**손님 1 ("ABCFG")에서 크기 2인 조합:** +- AB, AC, AF, AG, BC, BF, BG, CF, CG, FG + +**손님 2 ("AC")에서 크기 2인 조합:** +- AC + +**손님 3 ("CDE")에서 크기 2인 조합:** +- CD, CE, DE + +**손님 4 ("ACDE")에서 크기 2인 조합:** +- AC, AD, AE, CD, CE, DE + +**손님 5 ("BCFG")에서 크기 2인 조합:** +- BC, BF, BG, CF, CG, FG + +**손님 6 ("ACDEH")에서 크기 2인 조합:** +- AC, AD, AE, AH, CD, CE, CH, DE, DH, EH + +--- + +#### 📊 단계 2: 각 조합의 빈도 세기 + +**크기 2인 조합 빈도표:** +- "AC": 손님 1✅, 손님 2✅, 손님 4✅, 손님 6✅ → **4번** 🏆 +- "AB": 손님 1✅ → 1번 (제외, 2명 미만) +- "BC": 손님 1✅, 손님 5✅ → 2번 +- "CD": 손님 3✅, 손님 4✅, 손님 6✅ → 3번 +- "CE": 손님 3✅, 손님 4✅, 손님 6✅ → 3번 +- "DE": 손님 3✅, 손님 4✅, 손님 6✅ → 3번 +- "BCFG": (크기 4이므로 여기서는 제외) +- ... (기타 조합들) + +**크기 3인 조합 빈도표:** +- "CDE": 손님 3✅, 손님 4✅, 손님 6✅ → **3번** 🏆 +- "ACD": 손님 4✅, 손님 6✅ → 2번 +- "ACE": 손님 4✅, 손님 6✅ → 2번 +- ... (기타 조합들) + +**크기 4인 조합 빈도표:** +- "BCFG": 손님 1✅, 손님 5✅ → **2번** 🏆 +- "ACDE": 손님 4✅, 손님 6✅ → **2번** 🏆 +- ... (기타 조합들) + +--- + +#### ✅ 단계 3: 조건에 맞는 조합 선택 + +**크기 2:** +- 최대 빈도: 4번 +- "AC"가 4번으로 최대 → ✅ 선택 + +**크기 3:** +- 최대 빈도: 3번 +- "CDE"가 3번으로 최대 → ✅ 선택 + +**크기 4:** +- 최대 빈도: 2번 +- "BCFG"와 "ACDE"가 각각 2번으로 최대 → ✅ 둘 다 선택 + +--- + +#### 📝 단계 4: 정렬하기 + +선택된 조합: ["AC", "CDE", "BCFG", "ACDE"] + +알파벳 순으로 정렬: **["AC", "ACDE", "BCFG", "CDE"]** + +**최종 출력** +``` +["AC", "ACDE", "BCFG", "CDE"] +``` + +### 예시 2: 문제에서 제공한 예제 (간단 설명) + +**입력** +``` +orders = ["ABCDE", "AB", "CD", "ADE", "XYZ", "XYZ", "ACD"] +course = [2, 3, 5] +``` + +**손님별 주문:** +- 손님 1: A, B, C, D, E +- 손님 2: A, B +- 손님 3: C, D +- 손님 4: A, D, E +- 손님 5: X, Y, Z +- 손님 6: X, Y, Z +- 손님 7: A, C, D + +**핵심 결과:** +- **크기 2**: "AD"가 3번으로 최대 (손님 1, 3, 4) +- **크기 3**: "ACD", "ADE", "XYZ"가 각각 2번으로 최대 +- **크기 5**: "ABCDE"는 1번만 (손님 1만) → 제외 + +**출력** +``` +["ACD", "AD", "ADE", "CD", "XYZ"] +``` + +### 예시 3: 문제에서 제공한 예제 (간단 설명) + +**입력** +``` +orders = ["XYZ", "XWY", "WXA"] +course = [2, 3, 4] +``` + +**손님별 주문:** +- 손님 1: X, Y, Z +- 손님 2: X, W, Y +- 손님 3: W, X, A + +**핵심 결과:** +- **크기 2**: "WX"와 "XY"가 각각 2번으로 최대 +- **크기 3**: 모든 조합이 1번만 → 제외 (2명 미만) +- **크기 4**: 4개 이상 주문한 손님 없음 → 제외 + +**출력** +``` +["WX", "XY"] +``` + +--- + +## 💭 문제 해결 접근 방법: 어떻게 generateCombinations를 떠올릴 수 있을까? + +### 🤔 처음 문제를 봤을 때 + +**문제**: "각 손님의 주문에서 가능한 모든 조합을 만들어서, 가장 많이 나온 조합을 찾기" + +처음에는 막막할 수 있습니다. 하지만 **단계별로 생각**하면 해결책이 보입니다! + +### 📋 단계별 사고 과정 + +#### 1단계: 문제를 작은 단위로 분해하기 + +**큰 문제**: "모든 조합을 만들어서 빈도 카운트하기" + +**작은 문제들로 나누기:** +1. 한 손님의 주문에서 조합 만들기 +2. 모든 손님에 대해 반복하기 +3. 각 조합의 빈도 세기 +4. 조건에 맞는 조합 선택하기 + +**→ 핵심은 "1번: 한 손님의 주문에서 조합 만들기"입니다!** + +#### 2단계: 핵심 작업 파악하기 + +**질문**: "ABCFG"에서 2개 조합을 어떻게 만들까? + +**생각 과정:** +- A를 선택하거나 선택하지 않음 +- B를 선택하거나 선택하지 않음 +- C를 선택하거나 선택하지 않음 +- ... (각 문자마다 선택/선택 안 함) + +**→ 각 문자마다 "선택" 또는 "선택 안 함" 두 가지 경우가 있음!** + +#### 3단계: 패턴 인식하기 + +**유사한 문제들:** +- "N과 M" 문제: 1부터 N까지의 수에서 M개를 선택하는 모든 경우 +- "부분 집합" 문제: 주어진 집합의 모든 부분 집합 찾기 +- **이 문제**: 주어진 문자열에서 특정 크기의 조합 찾기 + +**→ 모두 "선택/선택 안 함"을 반복하는 패턴!** + +#### 4단계: 재귀 함수로 추상화하기 + +**핵심 아이디어:** +``` +현재 문자를 선택하거나 선택하지 않음 +→ 다음 문자로 넘어가기 +→ 원하는 크기가 되면 저장 +``` + +**함수로 표현하면:** +```java +// 의사코드 +함수(현재 위치, 현재까지 선택한 문자들, 목표 크기) { + if (목표 크기 달성) { + 결과 저장 + return + } + + if (더 이상 선택할 수 없음) { + return + } + + // 현재 문자 선택 + 현재 문자 추가 + 함수(다음 위치, 선택한 문자들, 목표 크기) + 현재 문자 제거 // 상태 되돌리기 + + // 현재 문자 선택 안 함 + 함수(다음 위치, 선택한 문자들, 목표 크기) +} +``` + +**→ 이것이 바로 `generateCombinations` 함수의 아이디어입니다!** + +#### 5단계: 구체적인 함수 시그니처 설계하기 + +**필요한 정보:** +1. 원본 문자열 (`order`) +2. 목표 크기 (`courseSize`) +3. 현재 위치 (`start`) +4. 현재까지 선택한 문자들 (`current`) +5. 결과를 저장할 곳 (`countMap`) + +**→ 함수 시그니처 완성!** +```java +generateCombinations(String order, int courseSize, int start, + StringBuilder current, Map countMap) +``` + +### 🎯 핵심 사고 과정 요약 + +``` +1. 문제 분해 + "모든 조합 만들기" → "한 주문에서 조합 만들기" + +2. 패턴 인식 + "각 문자마다 선택/선택 안 함" → 재귀 패턴! + +3. 추상화 + "선택 → 다음 단계 → 되돌리기" → 백트래킹 함수 + +4. 구체화 + 필요한 정보를 매개변수로 → 함수 시그니처 완성 +``` + +### 💡 실전 팁: 이런 생각을 하면 됩니다! + +**문제를 볼 때 자문하기:** +1. ✅ "이 문제의 핵심 작업이 뭘까?" +2. ✅ "이 작업을 작은 단위로 나눌 수 있을까?" +3. ✅ "각 단계에서 선택지가 몇 개인가?" (2개면 백트래킹 가능!) +4. ✅ "반복되는 패턴이 있나?" (있으면 재귀 함수로!) +5. ✅ "이전에 본 유사한 문제가 있나?" (N과 M, 부분 집합 등) + +**예시:** +- "조합 만들기" → 각 문자마다 선택/선택 안 함 (2가지) → 백트래킹! +- "최단 경로" → 각 위치에서 다음 위치 선택 (여러 가지) → BFS/DFS! +- "최적값 찾기" → 중복되는 부분 문제 → DP! + +### 📚 연습 방법 + +1. **문제를 읽고 핵심 작업 파악하기** + - "무엇을 해야 하는가?" + +2. **작은 단위로 분해하기** + - "큰 문제를 작은 문제들로 나눌 수 있는가?" + +3. **패턴 찾기** + - "선택지가 몇 개인가?" + - "반복되는 구조가 있는가?" + +4. **함수로 추상화하기** + - "이 작업을 함수로 만들면?" + - "필요한 정보는 무엇인가?" + +**→ 이 과정을 반복하면 자연스럽게 `generateCombinations` 같은 함수를 떠올릴 수 있습니다!** + +### 🎬 실제 사고 과정 예시 + +**문제를 처음 봤을 때:** + +``` +"ABCFG"에서 2개 조합을 만들어야 해... +``` + +**1단계: 작은 단위로 분해** +``` +"ABCFG"에서 2개 조합 만들기 +→ A를 포함하는 조합 + A를 포함하지 않는 조합 +``` + +**2단계: 패턴 인식** +``` +A를 포함하는 조합: +- A + (B, C, F, G 중 1개) → AB, AC, AF, AG + +A를 포함하지 않는 조합: +- (B, C, F, G 중 2개) → BC, BF, BG, CF, CG, FG +``` + +**3단계: 재귀 패턴 발견** +``` +"ABCFG"에서 2개 조합 += A 선택 + "BCFG"에서 1개 조합 ++ A 선택 안 함 + "BCFG"에서 2개 조합 + +"BCFG"에서 1개 조합 += B 선택 + "CFG"에서 0개 조합 (완성!) ++ B 선택 안 함 + "CFG"에서 1개 조합 + +... (반복) +``` + +**4단계: 함수로 추상화** +``` +함수(문자열, 목표 크기, 현재 위치, 현재 조합) { + if (목표 크기 달성) → 저장 + if (불가능) → 종료 + + 현재 문자 선택 → 재귀 호출 + 현재 문자 선택 안 함 → 재귀 호출 +} +``` + +**→ `generateCombinations` 함수 완성!** + +### 🔑 핵심 인사이트 + +**"각 단계에서 선택지가 2개(선택/선택 안 함)라면 → 백트래킹!"** + +이 문제의 경우: +- 각 문자마다 **선택하거나 선택하지 않음** (2가지) +- 목표 크기까지 반복 +- **→ 백트래킹이 자연스러운 해결책!** + +--- + +## 📝 한 줄 요약 + +**"각 손님의 주문에서 가능한 모든 조합을 만들어서, 가장 많이 나온 조합을 찾는 문제"** + +### 🎯 핵심 포인트 + +1. **조합 만들기**: 각 손님의 주문에서 가능한 모든 조합 생성 + - 예: "ABC" → AB, AC, BC (크기 2인 조합) + +2. **빈도 세기**: 각 조합이 몇 명의 손님에게서 나왔는지 카운트 + - 예: "AC"가 손님 1, 2, 4, 6에게서 나옴 → 4번 + +3. **조건 확인**: + - 최소 2명 이상의 손님에게서 나온 조합만 유효 + - 각 코스 크기별로 가장 많이 나온 조합 선택 + +4. **정렬**: 결과를 알파벳 순으로 정렬 + +### 🛠️ 알고리즘 선택: 백트래킹 + +**왜 백트래킹인가?** + +| 요구사항 | 반복문 | 비트마스크 | DP | 백트래킹 | +|---------|--------|-----------|-----|---------| +| **동적 크기 조합 생성** | ❌ 불가능
(2중첩, 3중첩... 몇 중첩?) | ✅ 가능 | ✅ 가능 | ✅ 가능 | +| **실제 조합 생성** | ✅ 가능 | ✅ 가능 | ❌ **불가능**
(개수만 구함) | ✅ 가능 | +| **효율적 탐색**
(가지치기) | - | ❌ 어려움
(모든 경우 탐색) | - | ✅ 쉬움
(불가능한 경우 제외) | +| **구현 명확성** | ⚠️ 중첩이 많아지면 복잡 | ⚠️ 비트 연산으로 복잡 | 중간 | ✅ 재귀로 직관적 | +| **메모리 효율** | - | - | - | ✅ 상태 되돌리기로 효율적 | +| **결론** | ❌ **사용 불가** | ⚠️ **비효율적** | ❌ **목적 불일치** | ✅ **최적!** | + +**백트래킹의 핵심 장점:** + +1. ✅ **동적 크기 대응**: `course = [2, 3, 4]`처럼 크기가 변해도 같은 코드로 처리 +2. ✅ **실제 조합 생성**: 모든 조합을 실제로 만들어서 Map에 저장 가능 (DP는 개수만 구함) +3. ✅ **효율적 탐색**: 가지치기로 불가능한 경우를 미리 제외 +4. ✅ **완전 탐색**: 모든 조합을 빠짐없이 체계적으로 생성 +5. ✅ **구현의 명확성**: 재귀를 통한 직관적이고 이해하기 쉬운 코드 + +**→ 이 문제의 모든 요구사항을 만족하는 유일한 방법입니다!** + +**특히 DP와의 차이:** +- **DP**: "ABCFG에서 2개 조합이 몇 개인가?" → 10개 (개수만 알 수 있음) +- **백트래킹**: "ABCFG에서 2개 조합을 모두 만들어라" → AB, AC, AF, AG, BC, BF, BG, CF, CG, FG (실제 조합 생성) +- **이 문제는 실제 조합을 만들어서 빈도를 카운트해야 하므로 DP는 사용 불가!** + +### ⏱️ 시간 복잡도 + +- 각 손님의 주문에서 조합 생성: 최대 2^10 = 1024가지 +- 손님 수: 최대 20명 +- 코스 크기 종류: 최대 10개 +- **전체**: 충분히 빠름 (제약 조건이 작음) + diff --git a/brute-force/backtracking/programmers-72411/2.algorithm.md b/brute-force/backtracking/programmers-72411/2.algorithm.md new file mode 100644 index 0000000..b96c802 --- /dev/null +++ b/brute-force/backtracking/programmers-72411/2.algorithm.md @@ -0,0 +1,192 @@ +# 알고리즘: 백트래킹(Backtracking) + +> 📖 **전체 알고리즘 설명**: [백트래킹(Backtracking) 정리](../backtracking.md) + +이 문제는 **백트래킹(Backtracking)** 알고리즘을 사용하여 해결합니다. +상위 폴더의 [백트래킹 정리 문서](../backtracking.md)에서 알고리즘의 전체 이론과 개념을 확인할 수 있습니다. + +--- + +## 이 문제에서의 백트래킹 적용 + +### 문제 특성 + +* **조합 생성**: 각 손님의 주문에서 가능한 모든 조합을 생성해야 함 +* **제약 조건**: + - 조합의 크기는 `course` 배열에 있는 값만 고려 + - 최소 2명 이상의 손님으로부터 주문된 조합만 유효 +* **빈도 카운트**: 각 조합이 몇 번 주문되었는지 카운트 +* **최대값 선택**: 각 코스 크기별로 가장 많이 주문된 조합 선택 + +--- + +## 이 문제의 특이사항 + +### 1️⃣ 조합 생성 (Combination Generation) + +이 문제는 **각 주문에서 가능한 모든 조합을 생성**해야 합니다: + +* 각 주문은 알파벳 대문자로 이루어진 문자열 +* 각 주문에서 크기 `course[i]`인 모든 조합을 생성 +* 조합은 순서가 중요하지 않지만, 결과는 알파벳 순으로 정렬되어야 함 + +**예시**: 주문 "ABCFG"에서 크기 2인 조합 생성 +``` +AB, AC, AF, AG, BC, BF, BG, CF, CG, FG +``` + +### 2️⃣ 빈도 카운트 및 최대값 선택 + +* **빈도 카운트**: `Map`를 사용하여 각 조합의 빈도 카운트 +* **최소 조건**: 최소 2명 이상의 손님으로부터 주문된 조합만 유효 +* **최대값 선택**: 각 코스 크기별로 가장 많이 주문된 조합 선택 +* **동일 최대값**: 최대 빈도가 같은 조합이 여러 개면 모두 포함 + +### 3️⃣ 정렬 요구사항 + +* **문자열 내부 정렬**: 각 조합은 알파벳 순으로 정렬되어야 함 (예: "AC"는 "CA"가 아님) +* **결과 배열 정렬**: 최종 결과 배열은 사전 순으로 정렬되어야 함 + +### 4️⃣ 시간 복잡도 + +* **조합 생성**: 각 주문에서 크기 `c`인 조합을 생성하는 경우의 수는 C(M, c) (M은 주문 길이) +* **최악의 경우**: 각 주문마다 모든 크기의 조합을 생성 +* **전체 시간 복잡도**: O(2^M × N × C) + - M: 주문의 최대 길이 (최대 10) + - N: 주문 개수 (최대 20) + - C: course 배열 크기 (최대 10) + +제약 조건에 따라 최악의 경우 2^10 × 20 × 10 = 204,800번의 연산이 필요하지만, 실제로는 조합 생성 시 가지치기를 통해 더 적은 연산만 수행됩니다. + +### 5️⃣ 공간 복잡도 + +* **조합 저장**: 생성된 조합들을 `Map`에 저장 +* **최악의 경우**: 각 주문마다 모든 크기의 조합을 생성 +* **전체 공간 복잡도**: O(2^M × N) + +--- + +## 핵심 구현 패턴 + +### 방법 1: 백트래킹을 사용한 조합 생성 ✅ 권장 + +**핵심 로직:** +```java +// 각 주문에서 가능한 모든 조합 생성 +void generateCombinations(String order, int courseSize, int start, StringBuilder current, Map countMap) { + // 종료 조건: 원하는 크기의 조합을 만들었을 때 + if (current.length() == courseSize) { + String combination = current.toString(); + countMap.put(combination, countMap.getOrDefault(combination, 0) + 1); + return; + } + + // 가지치기: 남은 문자로는 원하는 크기를 만들 수 없을 때 + if (order.length() - start < courseSize - current.length()) { + return; + } + + // 각 문자를 선택하거나 선택하지 않는 두 가지 경우 + for (int i = start; i < order.length(); i++) { + current.append(order.charAt(i)); // 선택 + generateCombinations(order, courseSize, i + 1, current, countMap); + current.setLength(current.length() - 1); // 선택 취소 (상태 되돌리기) + } +} +``` + +**특징:** +* 백트래킹을 사용하여 조합 생성 +* 상태 되돌리기: `StringBuilder`를 사용하므로 명시적으로 상태를 되돌려야 함 +* 가지치기: 남은 문자로는 원하는 크기를 만들 수 없을 때 조기 종료 + +### 방법 2: 비트마스크를 사용한 조합 생성 + +**핵심 로직:** +```java +// 비트마스크를 사용하여 조합 생성 +void generateCombinationsWithBitmask(String order, int courseSize, Map countMap) { + int n = order.length(); + for (int mask = 0; mask < (1 << n); mask++) { + if (Integer.bitCount(mask) != courseSize) continue; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + if ((mask & (1 << i)) != 0) { + sb.append(order.charAt(i)); + } + } + String combination = sb.toString(); + countMap.put(combination, countMap.getOrDefault(combination, 0) + 1); + } +} +``` + +**특징:** +* 비트마스크를 사용하여 모든 부분 집합 생성 +* 조합 크기가 `courseSize`와 일치하는 경우만 처리 +* 구현이 간단하지만, 불필요한 경우도 탐색하므로 비효율적일 수 있음 + +### 구현 선택 가이드 + +| 버전 | 핵심 특징 | 권장 상황 | +|------|----------|----------| +| **백트래킹 버전** | 가지치기로 효율적 탐색 | ✅ **일반적인 경우 (권장)** | +| **비트마스크 버전** | 구현 간단, 모든 경우 탐색 | 주문 길이가 매우 작은 경우 | + +**결론**: 주문 길이가 최대 10이므로 **백트래킹 버전이 더 효율적이며 권장**합니다. + +--- + +## 다른 백트래킹 문제와의 차이점 + +| 특징 | 일반 백트래킹 (순열/조합) | 이 문제 (72411) | +|------|------------------------|----------------| +| 탐색 대상 | 1부터 N까지의 수 | 주문 문자열의 문자들 | +| 선택지 개수 | N개 (1부터 N까지) | 주문 길이만큼 | +| 결과 저장 | 배열에 저장 | Map에 빈도 카운트 | +| 정렬 요구사항 | 사전 순 출력 | 알파벳 순 정렬 + 사전 순 정렬 | +| 추가 처리 | 결과 출력 | 빈도 카운트 + 최대값 선택 | + +--- + +## 상태 공간 트리 예시 + +`order = "ABC"`, `courseSize = 2`인 경우의 상태 공간 트리: + +``` + [""] + / | \ + / | \ + [A] [B] [C] + / \ / \ + / \ / \ + [AB] [AC] [BC] [C] +``` + +각 노드는 현재까지 선택한 문자들을 나타내며, 리프 노드가 크기 2인 조합입니다. + +--- + +## 최적화 고려사항 + +### 현재 구현의 특징 + +* **가지치기**: 남은 문자로는 원하는 크기를 만들 수 없을 때 조기 종료 +* **상태 되돌리기**: `StringBuilder`를 사용하므로 명시적으로 상태를 되돌려야 함 +* **빈도 카운트**: `Map`을 사용하여 효율적으로 빈도 카운트 + +### 추가 최적화 가능성 + +* **문자 정렬**: 각 주문을 미리 알파벳 순으로 정렬하여 조합 생성 시 자동으로 정렬됨 +* **조기 종료**: 최대 빈도를 미리 계산하여 불필요한 조합 생성 방지 (하지만 구현이 복잡해짐) +* **메모이제이션**: 동일한 주문 패턴에 대해 메모이제이션 가능하지만, 이 문제에서는 효과가 제한적 + +--- + +## 참고 + +* 전체 백트래킹 알고리즘 설명: [../backtracking.md](../backtracking.md) +* 문제 분석: [1.analysis.md](./1.analysis.md) +* 알고리즘 적용 근거: [3.reasoning.md](./3.reasoning.md) + diff --git a/brute-force/backtracking/programmers-72411/3.reasoning.md b/brute-force/backtracking/programmers-72411/3.reasoning.md new file mode 100644 index 0000000..3d673a2 --- /dev/null +++ b/brute-force/backtracking/programmers-72411/3.reasoning.md @@ -0,0 +1,410 @@ +# Main.java 설계 근거 정리 + +이 문서는 **프로그래머스 72411 메뉴 리뉴얼 문제**에 대한 최종 해답 코드인 `Main.java`가 +앞서 정리한 **백트래킹 개념, 알고리즘 논의와 어떻게 정확히 대응되는지**를 설명한다. + +--- + +## 1️⃣ 알고리즘 선택 근거: 백트래킹(Backtracking) + +### 문제 특성과의 적합성 + +* 문제 요구사항 + * 각 손님의 주문에서 가능한 모든 조합을 생성해야 함 + * 각 조합의 빈도를 카운트하여 가장 많이 주문된 조합 찾기 + * `course` 배열에 있는 크기의 조합만 고려 + * 최소 2명 이상의 손님으로부터 주문된 조합만 유효 +* 제약 조건 + * 주문 길이: 2 이상 10 이하 + * 주문 개수: 2 이상 20 이하 + * course 배열 크기: 1 이상 10 이하 + +이 문제는 **조합 생성 문제**로, 다음과 같은 특성이 있다: + +* 각 주문에서 가능한 모든 조합을 생성해야 함 +* 조합의 크기는 `course` 배열에 있는 값만 고려 +* 최소 2명 이상의 손님으로부터 주문된 조합만 유효 +* 결과는 알파벳 순으로 정렬되어야 함 + +➡️ 이러한 조건을 가장 직관적으로 만족하는 방법이 **백트래킹(Backtracking)** 이다. + +--- + +## 2️⃣ 코드 구조와 알고리즘 논의의 1:1 대응 + +### ① 조합 생성 함수 구조 + +```java +static void generateCombinations(String order, int courseSize, int start, StringBuilder current, Map countMap) { + // 종료 조건: 원하는 크기의 조합을 만들었을 때 + if (current.length() == courseSize) { + String combination = current.toString(); + countMap.put(combination, countMap.getOrDefault(combination, 0) + 1); + return; + } + + // 가지치기: 남은 문자로는 원하는 크기를 만들 수 없을 때 + if (order.length() - start < courseSize - current.length()) { + return; + } + + // 각 문자를 선택하거나 선택하지 않는 두 가지 경우 + for (int i = start; i < order.length(); i++) { + current.append(order.charAt(i)); // 선택 + generateCombinations(order, courseSize, i + 1, current, countMap); + current.setLength(current.length() - 1); // 선택 취소 (상태 되돌리기) + } +} +``` + +* 알고리즘 문서에서 설명한 **백트래킹 기본 템플릿**과 정확히 일치한다 +* `current.length() == courseSize`: 종료 조건 - 원하는 크기의 조합을 만들었을 때 +* `current.append(order.charAt(i))`: 선택 - 현재 문자를 조합에 추가 +* `current.setLength(current.length() - 1)`: 선택 취소 - 상태 되돌리기 +* `generateCombinations(..., i + 1, ...)`: 다음 문자로 재귀 호출 + +➡️ **상태 공간 트리를 깊이 우선으로 탐색**하는 백트래킹 구조가 코드에 그대로 반영되어 있다. + +--- + +### ② 가지치기 최적화 + +```java +// 가지치기: 남은 문자로는 원하는 크기를 만들 수 없을 때 +if (order.length() - start < courseSize - current.length()) { + return; +} +``` + +* **가지치기 원리**: 남은 문자 개수가 필요한 문자 개수보다 적으면 조합을 만들 수 없음 +* **효과**: 불가능한 경로를 조기 종료하여 탐색 효율 향상 +* **예시**: `order = "ABC"`, `courseSize = 2`, `start = 2`, `current.length() = 0`인 경우 + * 남은 문자: 1개 ("C") + * 필요한 문자: 2개 + * 1 < 2이므로 조기 종료 + +➡️ 이는 **가지치기 최적화**가 코드에 구현된 것이다. 불가능한 경로를 조기 종료하여 탐색 효율을 높입니다. + +--- + +### ③ 상태 되돌리기 (Backtracking) + +```java +current.append(order.charAt(i)); // 선택 +generateCombinations(order, courseSize, i + 1, current, countMap); +current.setLength(current.length() - 1); // 선택 취소 (상태 되돌리기) +``` + +* **상태 되돌리기 원리**: `StringBuilder`는 누적 방식으로 동작하므로, 재귀 호출 후 명시적으로 상태를 되돌려야 함 +* **필요성**: 다른 경로를 탐색할 때 이전 경로의 상태가 남아있으면 안 됨 +* **구현**: `current.setLength(current.length() - 1)`로 마지막 문자 제거 + +➡️ 알고리즘 문서에서 설명한 **"상태를 되돌리는 특수한 형태의 DFS"**가 코드에 구현되어 있다. + +--- + +### ④ 빈도 카운트 및 최대값 선택 + +```java +// 각 주문에서 조합 생성 및 빈도 카운트 +for (String order : orders) { + char[] chars = order.toCharArray(); + Arrays.sort(chars); // 알파벳 순으로 정렬 + String sortedOrder = new String(chars); + + for (int courseSize : course) { + if (sortedOrder.length() >= courseSize) { + generateCombinations(sortedOrder, courseSize, 0, new StringBuilder(), countMap); + } + } +} + +// 각 코스 크기별로 최대 빈도 찾기 +for (int courseSize : course) { + int maxCount = 0; + List candidates = new ArrayList<>(); + + for (Map.Entry entry : countMap.entrySet()) { + String combination = entry.getKey(); + int count = entry.getValue(); + + if (combination.length() == courseSize && count >= 2) { + if (count > maxCount) { + maxCount = count; + candidates.clear(); + candidates.add(combination); + } else if (count == maxCount) { + candidates.add(combination); + } + } + } + + result.addAll(candidates); +} +``` + +* **빈도 카운트**: `Map`를 사용하여 각 조합의 빈도 카운트 +* **최소 조건**: `count >= 2`로 최소 2명 이상의 손님으로부터 주문된 조합만 유효 +* **최대값 선택**: 각 코스 크기별로 가장 많이 주문된 조합 선택 +* **동일 최대값**: 최대 빈도가 같은 조합이 여러 개면 모두 포함 + +➡️ 알고리즘 문서에서 설명한 **"빈도 카운트 및 최대값 선택"**이 코드에 구현되어 있다. + +--- + +### ⑤ 정렬 요구사항 + +```java +// 각 주문을 알파벳 순으로 정렬 +char[] chars = order.toCharArray(); +Arrays.sort(chars); +String sortedOrder = new String(chars); + +// 결과를 사전 순으로 정렬 +Collections.sort(result); +``` + +* **문자열 내부 정렬**: 각 주문을 미리 알파벳 순으로 정렬하여 조합 생성 시 자동으로 정렬됨 +* **결과 배열 정렬**: 최종 결과 배열을 사전 순으로 정렬 + +➡️ 알고리즘 문서에서 설명한 **"정렬 요구사항"**이 코드에 구현되어 있다. + +--- + +## 3️⃣ 재귀 호출의 의미 + +### 상태 공간 트리 탐색 + +재귀 호출 `generateCombinations(order, courseSize, i + 1, current, countMap)`는 다음과 같은 의미를 가진다: + +1. **현재 문자를 선택한 상태**에서 +2. **다음 문자로 진행**하여 +3. **상태 공간 트리의 다음 레벨을 탐색** + +### 예시: order="ABC", courseSize=2인 경우 + +``` +generateCombinations("ABC", 2, 0, "", {}) + ├─ 선택: A + │ └─ generateCombinations("ABC", 2, 1, "A", {}) + │ ├─ 선택: B → "AB" (크기 2 달성) ✅ + │ └─ 선택: C → "AC" (크기 2 달성) ✅ + ├─ 선택: B + │ └─ generateCombinations("ABC", 2, 2, "B", {}) + │ └─ 선택: C → "BC" (크기 2 달성) ✅ + └─ 선택: C + └─ generateCombinations("ABC", 2, 3, "C", {}) + └─ 가지치기: 남은 문자 없음 → 조기 종료 +``` + +➡️ 이는 알고리즘 문서에서 설명한 **상태 공간 트리**를 그대로 탐색하는 과정이다. + +--- + +## 4️⃣ 시간 복잡도와 코드의 대응 + +### 조합 생성 시간 복잡도 + +* 각 주문에서 크기 `c`인 조합을 생성하는 경우의 수는 C(M, c) (M은 주문 길이) +* 최악의 경우: 각 주문마다 모든 크기의 조합을 생성 +* 전체 시간 복잡도: **O(2^M × N × C)** + - M: 주문의 최대 길이 (최대 10) + - N: 주문 개수 (최대 20) + - C: course 배열 크기 (최대 10) + +### 코드에서의 대응 + +```java +// 각 주문마다 모든 코스 크기의 조합 생성 +for (String order : orders) { // O(N) + for (int courseSize : course) { // O(C) + if (sortedOrder.length() >= courseSize) { + generateCombinations(...); // O(2^M) + } + } +} +``` + +* 각 주문마다 모든 코스 크기의 조합을 생성 +* 가지치기를 통해 불가능한 경로는 조기 종료 +* 전체 시간 복잡도: **O(2^M × N × C)** (최악의 경우, 실제로는 가지치기로 더 적음) + +--- + +## 5️⃣ 공간 복잡도 분석 + +### 조합 저장 공간 + +* 생성된 조합들을 `Map`에 저장 +* 최악의 경우: 각 주문마다 모든 크기의 조합을 생성 +* 전체 공간 복잡도: **O(2^M × N)** + +### 재귀 호출 스택 + +* 최대 재귀 깊이: M (주문의 최대 길이) +* 각 재귀 호출마다 스택 프레임 생성 +* 스택 공간: **O(M)** + +### 전체 공간 복잡도 + +* **O(2^M × N)** - 조합 저장 공간이 지배적 + +--- + +## 6️⃣ 다른 접근 방법과의 비교 + +### 방법 1: 백트래킹 (현재 방법) ✅ + +**장점:** +* 구현이 간단하고 직관적 +* 가지치기를 통해 불가능한 경로 조기 종료 +* 코드가 간결하고 이해하기 쉬움 + +**단점:** +* 재귀 호출로 인한 스택 오버플로우 가능성 (하지만 M ≤ 10이므로 문제 없음) + +### 방법 2: 비트마스크 + +```java +void generateCombinationsWithBitmask(String order, int courseSize, Map countMap) { + int n = order.length(); + for (int mask = 0; mask < (1 << n); mask++) { + if (Integer.bitCount(mask) != courseSize) continue; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + if ((mask & (1 << i)) != 0) { + sb.append(order.charAt(i)); + } + } + String combination = sb.toString(); + countMap.put(combination, countMap.getOrDefault(combination, 0) + 1); + } +} +``` + +**장점:** +* 구현이 간단함 +* 스택 오버플로우 걱정 없음 + +**단점:** +* 불필요한 경우도 탐색하므로 비효율적 +* 비트마스크 개념 필요 +* 시간 복잡도는 동일 (O(2^M)) + +### 방법 3: 반복문을 사용한 조합 생성 + +**단점:** +* 구현이 복잡함 +* 동적 크기의 조합을 생성하기 어려움 +* 이 문제에서는 백트래킹이 더 적합 + +➡️ **백트래킹이 이 문제에 가장 적합한 방법이다.** + +--- + +## 7️⃣ 핵심 설계 원칙 + +1. **단순성**: 복잡한 최적화 없이 알고리즘의 핵심만 구현 +2. **명확성**: 코드만 봐도 알고리즘의 동작을 이해할 수 있음 +3. **효율성**: 문제 제약 조건 내에서 충분히 효율적 +4. **일관성**: 알고리즘 문서의 설명과 코드가 1:1로 대응 + +--- + +## 8️⃣ 구현 세부사항 + +### 문자열 정렬 + +```java +char[] chars = order.toCharArray(); +Arrays.sort(chars); +String sortedOrder = new String(chars); +``` + +* 각 주문을 미리 알파벳 순으로 정렬 +* 조합 생성 시 자동으로 알파벳 순으로 정렬됨 +* 결과 정렬 요구사항을 만족 + +### 빈도 카운트 + +```java +countMap.put(combination, countMap.getOrDefault(combination, 0) + 1); +``` + +* `Map`을 사용하여 각 조합의 빈도 카운트 +* `getOrDefault`를 사용하여 초기값 0 처리 + +### 최대값 선택 + +```java +if (count > maxCount) { + maxCount = count; + candidates.clear(); + candidates.add(combination); +} else if (count == maxCount) { + candidates.add(combination); +} +``` + +* 각 코스 크기별로 최대 빈도 찾기 +* 동일한 최대값이면 모두 포함 + +--- + +## 9️⃣ 특수 케이스 처리 + +### 케이스 1: 주문 길이가 코스 크기보다 작은 경우 + +```java +if (sortedOrder.length() >= courseSize) { + generateCombinations(sortedOrder, courseSize, 0, new StringBuilder(), countMap); +} +``` + +* 주문 길이가 코스 크기보다 작으면 조합 생성 불가 +* 조기 종료하여 불필요한 연산 방지 + +### 케이스 2: 최소 2명 이상의 손님 조건 + +```java +if (combination.length() == courseSize && count >= 2) { + // 최대값 선택 로직 +} +``` + +* 최소 2명 이상의 손님으로부터 주문된 조합만 유효 +* `count >= 2` 조건으로 필터링 + +### 케이스 3: 동일한 최대값이 여러 개인 경우 + +```java +if (count > maxCount) { + maxCount = count; + candidates.clear(); + candidates.add(combination); +} else if (count == maxCount) { + candidates.add(combination); +} +``` + +* 최대 빈도가 같은 조합이 여러 개면 모두 포함 +* `candidates` 리스트에 모두 추가 + +--- + +## 🔟 코드 최적화 고려사항 + +### 현재 구현의 특징 + +* **가지치기**: 남은 문자로는 원하는 크기를 만들 수 없을 때 조기 종료 +* **상태 되돌리기**: `StringBuilder`를 사용하므로 명시적으로 상태를 되돌려야 함 +* **빈도 카운트**: `Map`을 사용하여 효율적으로 빈도 카운트 +* **정렬**: 각 주문을 미리 정렬하여 조합 생성 시 자동으로 정렬됨 + +### 추가 최적화 가능성 + +* **조기 종료**: 최대 빈도를 미리 계산하여 불필요한 조합 생성 방지 (하지만 구현이 복잡해짐) +* **메모이제이션**: 동일한 주문 패턴에 대해 메모이제이션 가능하지만, 이 문제에서는 효과가 제한적 +* 현재 구현이 문제 제약 조건 내에서 충분히 효율적 + diff --git a/brute-force/backtracking/programmers-72411/Main.java b/brute-force/backtracking/programmers-72411/Main.java new file mode 100644 index 0000000..58e2867 --- /dev/null +++ b/brute-force/backtracking/programmers-72411/Main.java @@ -0,0 +1,97 @@ +import java.util.*; + +class Solution { + /** + * 메뉴 리뉴얼 문제 해결 + * + * 접근 방법: 백트래킹을 사용하여 각 주문에서 가능한 모든 조합을 생성하고, + * 빈도를 카운트하여 가장 많이 주문된 조합을 찾는다. + */ + public String[] solution(String[] orders, int[] course) { + // 각 조합의 빈도를 카운트하는 Map + Map countMap = new HashMap<>(); + + // 각 주문에서 가능한 모든 조합 생성 및 빈도 카운트 + for (String order : orders) { + // 각 주문을 알파벳 순으로 정렬 (조합 생성 시 자동으로 정렬됨) + char[] chars = order.toCharArray(); + Arrays.sort(chars); + String sortedOrder = new String(chars); + + // 각 코스 크기별로 조합 생성 + for (int courseSize : course) { + // 주문 길이가 코스 크기보다 작으면 조합 생성 불가 + if (sortedOrder.length() >= courseSize) { + generateCombinations(sortedOrder, courseSize, 0, new StringBuilder(), countMap); + } + } + } + + // 각 코스 크기별로 최대 빈도 찾기 + List result = new ArrayList<>(); + for (int courseSize : course) { + int maxCount = 0; + List candidates = new ArrayList<>(); + + // 해당 코스 크기의 조합 중 최대 빈도 찾기 + for (Map.Entry entry : countMap.entrySet()) { + String combination = entry.getKey(); + int count = entry.getValue(); + + // 최소 2명 이상의 손님으로부터 주문된 조합만 유효 + if (combination.length() == courseSize && count >= 2) { + if (count > maxCount) { + maxCount = count; + candidates.clear(); + candidates.add(combination); + } else if (count == maxCount) { + candidates.add(combination); + } + } + } + + result.addAll(candidates); + } + + // 결과를 사전 순으로 정렬 + Collections.sort(result); + + return result.toArray(new String[0]); + } + + /** + * 백트래킹을 사용하여 주문에서 가능한 모든 조합을 생성 + * + * @param order 주문 문자열 (알파벳 순으로 정렬됨) + * @param courseSize 생성할 조합의 크기 + * @param start 현재 선택할 수 있는 시작 인덱스 + * @param current 현재까지 선택한 문자들 + * @param countMap 조합의 빈도를 카운트하는 Map + */ + static void generateCombinations(String order, int courseSize, int start, StringBuilder current, Map countMap) { + // 종료 조건: 원하는 크기의 조합을 만들었을 때 + if (current.length() == courseSize) { + String combination = current.toString(); + countMap.put(combination, countMap.getOrDefault(combination, 0) + 1); + return; + } + + // 가지치기: 남은 문자로는 원하는 크기를 만들 수 없을 때 + if (order.length() - start < courseSize - current.length()) { + return; + } + + // 각 문자를 선택하거나 선택하지 않는 두 가지 경우 + for (int i = start; i < order.length(); i++) { + // 선택: 현재 문자를 조합에 추가 + current.append(order.charAt(i)); + + // 재귀 호출: 다음 문자로 진행 + generateCombinations(order, courseSize, i + 1, current, countMap); + + // 선택 취소: 상태 되돌리기 (백트래킹의 핵심) + current.setLength(current.length() - 1); + } + } +} +