Skip to content

Conversation

@minor7295
Copy link
Owner

@minor7295 minor7295 commented Jan 13, 2026

프로그래머스 72411 - 메뉴 리뉴얼 문제 풀이 추가

Summary

프로그래머스 72411 메뉴 리뉴얼 문제를 백트래킹 알고리즘으로 해결하고, 문제 분석부터 알고리즘 선택, 구현까지의 전체 사고 과정을 문서화했습니다. 각 손님의 주문에서 가능한 모든 조합을 생성하여 가장 많이 주문된 조합을 찾는 문제를 백트래킹으로 모델링하여 해결했습니다.

주요 구현 내용:

  • 문제 분석: 각 손님의 주문에서 가능한 모든 조합을 생성하고 빈도를 카운트하는 문제로 모델링
  • 알고리즘 선택: 백트래킹 알고리즘 채택 (동적 크기 조합 생성, 실제 조합 생성 필요)
  • 구현: O(2^M × N × C) 시간 복잡도로 모든 조합을 생성하고 빈도를 카운트하는 해답 코드 작성
    • 시간 복잡도 근거: 각 주문에서 가능한 모든 조합 생성 (최대 2^10 = 1024가지)
    • 다른 방법 대비: 반복문(동적 크기 불가), 비트마스크(비효율적), DP(조합 생성 불가) 대비 백트래킹이 최적

추가된 파일:

  • brute-force/backtracking/programmers-72411/1.analysis.md: 문제 분석 및 해결 접근 방법 (1269줄)
  • brute-force/backtracking/programmers-72411/2.algorithm.md: 백트래킹 적용 및 문자열 조합 특이사항 (193줄)
  • brute-force/backtracking/programmers-72411/3.reasoning.md: 알고리즘 선택 근거 및 설계 과정 (411줄)
  • brute-force/backtracking/programmers-72411/Main.java: 백트래킹 기반 조합 생성 및 빈도 카운트 (96줄)
  • brute-force/backtracking/backtracking.md: 백트래킹 문서 보완 (문자열 조합 생성 유형 추가, 함수 구성 패턴 추가)

주요 판단 및 구현 세부사항

1. 백트래킹 알고리즘 선택 근거

배경 및 문제 상황:
각 손님의 주문에서 가능한 모든 조합을 생성해야 하며, 조합의 크기는 course 배열에 따라 동적으로 변합니다. 또한 실제 조합을 만들어서 빈도를 카운트해야 합니다.

해결 방안 및 방식 선택:

백트래킹을 선택한 이유:

  • 동적 크기 조합 생성: course = [2, 3, 4]처럼 크기가 변해도 같은 코드로 처리 가능
  • 실제 조합 생성: 모든 조합을 실제로 만들어서 Map에 저장 가능 (DP는 개수만 구함)
  • 효율적 탐색: 가지치기로 불가능한 경우를 미리 제외
  • 완전 탐색: 모든 조합을 빠짐없이 체계적으로 생성

다른 방법과의 비교:

방법 동적 크기 실제 조합 생성 효율성 결론
반복문 ❌ 불가능
(2중첩, 3중첩... 몇 중첩?)
✅ 가능 - ❌ 사용 불가
비트마스크 ✅ 가능 ✅ 가능 낮음 (불필요한 경우도 탐색) ⚠️ 비효율적
DP ✅ 가능 불가능
(개수만 구함)
- 목적 불일치
백트래킹 ✅ 가능 ✅ 가능 높음 (가지치기 가능) 최적!

핵심 판단:

  • 반복문: 동적 크기 조합 생성 불가능 (몇 중첩이 필요한지 모름)
  • 비트마스크: 불필요한 경우도 모두 탐색하여 비효율적
  • DP: 조합의 개수만 구할 수 있고, 실제 조합을 만들 수 없음
  • 백트래킹: 모든 요구사항을 만족하는 유일한 방법

구현 코드:

static void generateCombinations(
    String order, int courseSize, int start, 
    StringBuilder current, Map<String, Integer> countMap
) {
    // 종료 조건
    if (current.length() == courseSize) {
        countMap.put(current.toString(), countMap.getOrDefault(current.toString(), 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);  // ✅ 상태 되돌리기
    }
}

고민한 점:

  • 왜 백트래킹을 선택해야 하는지에 대한 명확한 근거가 필요했습니다. 다른 방법들의 한계를 구체적으로 분석하여 백트래킹이 최적인 이유를 명확히 했습니다.
  • 특히 DP와의 차이점을 강조했습니다. DP는 조합의 개수만 구할 수 있지만, 이 문제는 실제 조합을 만들어서 빈도를 카운트해야 하므로 DP는 사용 불가능합니다.

2. 문제 해결 접근 방법: generateCombinations 함수 설계

배경 및 문제 상황:
"ABCFG"에서 2개 조합을 어떻게 만들까요? 처음에는 막막할 수 있지만, 단계별로 생각하면 해결책이 보입니다.

해결 방안 및 사고 과정:

1단계: 문제를 작은 단위로 분해

  • 큰 문제: "모든 조합을 만들어서 빈도 카운트하기"
  • 작은 문제: "한 손님의 주문에서 조합 만들기"

2단계: 핵심 작업 파악

  • 각 문자마다 "선택" 또는 "선택 안 함" 두 가지 경우
  • 원하는 크기가 되면 저장

3단계: 패턴 인식

  • "N과 M" 문제, "부분 집합" 문제와 유사한 패턴
  • 모두 "선택/선택 안 함"을 반복하는 패턴

4단계: 재귀 함수로 추상화

현재 문자를 선택하거나 선택하지 않음
→ 다음 문자로 넘어가기
→ 원하는 크기가 되면 저장

5단계: 구체적인 함수 시그니처 설계

  • 필요한 정보: 원본 문자열, 목표 크기, 현재 위치, 현재까지 선택한 문자들, 결과 저장

최종 선택:
인덱스 기반 패턴을 사용하여 함수를 설계했습니다. 전체 문자열과 인덱스를 사용하는 것이 부분 문자열을 만드는 것보다 메모리 효율적입니다.

구현 코드:

static void generateCombinations(
    String order,           // 전체 문자열 (인덱스로 접근)
    int courseSize,         // 목표 크기
    int start,              // 현재 선택할 수 있는 시작 위치
    StringBuilder current,  // 현재까지 선택한 문자들
    Map<String, Integer> countMap  // 결과 저장
)

고민한 점:

  • 왜 "남은 문자들"을 인자로 전달하지 않았는지에 대한 질문이 있었습니다. 부분 문자열을 만들면 새로운 메모리 할당이 필요하지만, 인덱스를 사용하면 기존 문자열을 재사용할 수 있어 더 효율적입니다.
  • 함수 설계 원칙을 명확히 했습니다: 상태 정보 결정, 전역 vs 인자, 상태 되돌리기 판단.

3. 빈도 카운트 및 최대값 선택

배경 및 문제 상황:
각 조합이 몇 명의 손님에게서 나왔는지 카운트하고, 각 코스 크기별로 가장 많이 주문된 조합을 선택해야 합니다.

해결 방안:
Map<String, Integer>를 사용하여 각 조합의 빈도를 카운트하고, 각 코스 크기별로 최대 빈도를 찾아 선택합니다.

구현 코드:

// 각 주문에서 조합 생성 및 빈도 카운트
Map<String, Integer> 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<String> result = new ArrayList<>();
for (int courseSize : course) {
    int maxCount = 0;
    List<String> candidates = new ArrayList<>();
    
    for (Map.Entry<String, Integer> 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);
}

핵심 포인트:

  • 최소 2명 이상의 손님에게서 나온 조합만 유효
  • 각 코스 크기별로 가장 많이 주문된 조합 선택
  • 동일한 최대값이면 모두 포함
  • 결과를 알파벳 순으로 정렬

고민한 점:

  • 각 주문을 미리 알파벳 순으로 정렬하여 조합 생성 시 자동으로 정렬되도록 했습니다.
  • Map을 사용하여 효율적으로 빈도를 카운트했습니다.

4. 문서화 구조: 문제 해결 접근 방법 강조

배경 및 문제 상황:
단순히 코드를 보는 것이 아니라, 문제를 보고 "아, 이렇게 접근해야겠다"는 생각을 어떻게 할 수 있는지 알고 싶어하는 학습자가 있습니다.

해결 방안:
1.analysis.md에 "문제 해결 접근 방법" 섹션을 추가하여, generateCombinations 같은 함수를 떠올리는 사고 과정을 단계별로 설명했습니다.

추가된 내용:

  1. 단계별 사고 과정

    • 문제를 작은 단위로 분해하기
    • 핵심 작업 파악하기
    • 패턴 인식하기
    • 재귀 함수로 추상화하기
    • 구체적인 함수 시그니처 설계하기
  2. 실전 팁

    • 문제를 볼 때 자문할 질문들
    • 핵심 작업 파악 방법
    • 패턴 인식 방법
    • 함수로 추상화하는 방법
  3. 실제 사고 과정 예시

    • "ABCFG"에서 2개 조합을 만드는 과정을 단계별로 보여줌
    • 어떻게 재귀 패턴을 발견하는지 설명
  4. 핵심 인사이트

    • "각 단계에서 선택지가 2개(선택/선택 안 함)라면 → 백트래킹!"

고민한 점:

  • 문제 해결 접근 방법을 단계별로 설명하여, 학습자가 비슷한 문제를 봤을 때도 같은 방식으로 접근할 수 있도록 했습니다.
  • 구체적인 예시를 통해 사고 과정을 시각화했습니다.

구현 세부사항

1. 백트래킹 조합 생성 함수 구현

핵심 로직:

  1. 종료 조건: current.length() == courseSize일 때 완전한 조합을 찾은 것이므로 Map에 저장하고 return
  2. 가지치기: 남은 문자로는 원하는 크기를 만들 수 없을 때 조기 종료
  3. 선택지 탐색: start부터 order.length()까지 각 문자를 선택
  4. 상태 변경: current.append(order.charAt(i))로 선택
  5. 재귀 호출: 다음 문자로 진행하기 위해 generateCombinations(..., i + 1, ...) 호출
  6. 상태 되돌리기: current.setLength(...)로 선택 취소

관련 코드:

static void generateCombinations(String order, int courseSize, int start, 
                                 StringBuilder current, Map<String, Integer> 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);  // ✅ 상태 되돌리기
    }
}

2. 인덱스 사용의 효율성

왜 부분 문자열을 전달하지 않았을까?

방법 1: 부분 문자열 전달 (비효율적)

generateCombinations("BCFG", 2, ...);  // 새 문자열 객체 생성!

방법 2: 인덱스 사용 (효율적)

generateCombinations("ABCFG", 2, 1, ...);  // 기존 문자열 재사용!

장점:

  1. 메모리 효율성: 부분 문자열을 만들면 새로운 메모리 할당이 필요하지만, 인덱스 사용은 기존 문자열을 재사용
  2. 성능: 부분 문자열 생성은 O(n) 시간 소요, 인덱스 접근은 O(1) 시간 소요
  3. 간단함: 인덱스만 증가시키면 되므로 코드가 간단

3. 시간 복잡도 분석

조합 생성 시간 복잡도:

  • 각 주문에서 크기 c인 조합을 생성하는 경우의 수는 C(M, c) (M은 주문 길이)
  • 최악의 경우: 각 주문마다 모든 크기의 조합을 생성
  • 전체 시간 복잡도: O(2^M × N × C)
    • M: 주문의 최대 길이 (최대 10)
    • N: 주문 개수 (최대 20)
    • C: course 배열 크기 (최대 10)

제약 조건의 영향:

  • 최악의 경우: 2^10 × 20 × 10 = 204,800번의 연산
  • 가지치기를 통해 실제로는 더 적은 연산만 수행
  • 충분히 빠름

4. 공간 복잡도 분석

조합 저장 공간:

  • 생성된 조합들을 Map에 저장
  • 최악의 경우: 각 주문마다 모든 크기의 조합을 생성
  • 전체 공간 복잡도: O(2^M × N)

재귀 호출 스택:

  • 최대 재귀 깊이: M (주문의 최대 길이)
  • 각 재귀 호출마다 스택 프레임 생성
  • 스택 공간: O(M)

전체 공간 복잡도: O(2^M × N) (조합 저장 공간이 지배적)

@minor7295 minor7295 marked this pull request as draft January 13, 2026 21:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants