토스 보증 문제

네, Java로 작성한 해결책입니다.

핵심 아이디어는 보증 관계를 Map으로 만들어두고, 각 신청자로부터 시작해 보증인의 사슬을 거슬러 올라가 최종 보증인이 '초기 승인자' 목록에 포함되는지 확인하는 것입니다.

핵심 로직

  1. 자료구조 선택:

    • 보증인 맵 (guarantorMap): Map<Integer, Integer>를 사용해 key: 보증받는 사람, value: 보증하는 사람 형태로 저장합니다. 특정 고객의 보증인을 O(1) 시간 복잡도로 빠르게 찾기 위함입니다.

    • 초기 승인자 집합 (approvalSet): Set를 사용해 초기 승인자 목록을 저장합니다. 특정 고객이 초기 승인자인지 O(1) 시간 복잡도로 빠르게 확인하기 위함입니다.

  2. 탐색 및 판별:

    • 각 신청자에 대해 while 루프를 순회하며 보증인의 사슬을 따라 올라갑니다.

    • 사슬을 따라가는 도중 만나는 고객이 approvalSet에 포함되어 있다면, 해당 신청자는 최종적으로 승인될 수 있으므로 true를 반환합니다.

    • 만약 사슬의 끝에 도달했는데도 (더 이상 보증인이 없는데도) approvalSet에 포함된 고객을 만나지 못했다면 거절을 의미하는 false를 반환합니다.

  3. 최적화 (메모이제이션, Memoization):

    • [4, 2] 신청자를 심사할 때 4 -> 2 -> 1 사슬을 확인합니다. 이때 2번 고객이 승인 가능하다는 것을 알게 됩니다. 나중에 2번 고객을 또 심사한다면 같은 과정을 반복해야 하므로 비효율적입니다.

    • Map<Integer, Boolean> 형태의 memo 캐시를 두어, 한번 확인한 고객의 승인 여부(true/false)를 저장합니다. 다음번에 동일한 고객에 대한 심사 요청이 오면, 계산 없이 즉시 캐시된 결과를 반환하여 중복 계산을 피합니다.

전체 코드

 1 import java.util.*;
 2
 3 class Solution {
 4
 5     /**
 6      * 대출 승인 여부를 판별하는 메인 함수
 7      * @param relations 보증 관계 배열 [[보증받는자, 보증인], ...]
 8      * @param initial_approvals 초기 승인자 배열
 9      * @param applicants 대출 신청자 배열
10      * @return 각 신청자의 승인 여부를 담은 boolean 배열
11      */
12     public boolean[] solution(int[][] relations, int[] initial_approvals, int[] applicants) {
13         // 1. 자료구조 초기화
14         Map<Integer, Integer> guarantorMap = new HashMap<>();
15         for (int[] rel : relations) {
16             int guaranteed = rel[0];
17             int guarantor = rel[1];
18             guarantorMap.put(guaranteed, guarantor);
19         }
20
21         Set<Integer> approvalSet = new HashSet<>();
22         for (int approved : initial_approvals) {
23             approvalSet.add(approved);
24         }
25
26         // 3. 최적화를 위한 메모이제이션 캐시
27         Map<Integer, Boolean> memo = new HashMap<>();
28
29         // 2. 각 신청자에 대해 승인 여부 확인
30         boolean[] results = new boolean[applicants.length];
31         for (int i = 0; i < applicants.length; i++) {
32             results[i] = isApproved(applicants[i], guarantorMap, approvalSet, memo);
33         }
34
35         return results;
36     }
37
38     /**
39      * 특정 신청자가 대출 승인을 받을 수 있는지 재귀적으로 확인하는 헬퍼 함수
40      * @param applicant 현재 확인하려는 고객
41      * @param guarantorMap 보증 관계 맵
42      * @param approvalSet 초기 승인자 집합
43      * @param memo 중복 계산을 피하기 위한 캐시
44      * @return 승인 여부
45      */
46     private boolean isApproved(int applicant, Map<Integer, Integer> guarantorMap, Set<Integer> approvalSet,
   Map<Integer, Boolean> memo) {
47         // 이미 계산된 결과가 있으면 즉시 반환 (메모이제이션)
48         if (memo.containsKey(applicant)) {
49             return memo.get(applicant);
50         }
51
52         // 현재 고객부터 시작하여 보증인 사슬을 따라 올라감
53         int current = applicant;
54         Set<Integer> visited = new HashSet<>(); // 순환 방지를 위한 방문 기록 (문제 조건상 불필요하나 안전장치)
55
56         while (true) {
57             // 기저 사례 1: 현재 고객이 초기 승인자이면, 승인
58             if (approvalSet.contains(current)) {
59                 memo.put(applicant, true); // 원본 신청자의 결과를 캐시에 저장
60                 return true;
61             }
62
63             // 이미 확인했던 중간 고객의 결과가 캐시에 있다면 사용
64             if (memo.containsKey(current)) {
65                 boolean result = memo.get(current);
66                 memo.put(applicant, result); // 원본 신청자의 결과를 캐시에 저장
67                 return result;
68             }
69
70             // 방문 기록 추가 (무한 루프 방지)
71             if (visited.contains(current)) {
72                  memo.put(applicant, false);
73                  return false;
74             }
75             visited.add(current);
76
77             // 다음 보증인 탐색
78             Integer guarantor = guarantorMap.get(current);
79
80             // 기저 사례 2: 보증인이 더 이상 없으면, 거절
81             if (guarantor == null) {
82                 memo.put(applicant, false); // 원본 신청자의 결과를 캐시에 저장
83                 return false;
84             }
85
86             // 보증인을 따라 한 단계 위로 올라감
87             current = guarantor;
88         }
89     }
90
91     public static void main(String[] args) {
92         Solution sol = new Solution();
93
94         // 예시 1
95         int[][] relations1 = {{2, 1}, {3, 1}, {4, 2}};
96         int[] initial_approvals1 = {1};
97         int[] applicants1 = {3, 4, 5, 1};
98         boolean[] results1 = sol.solution(relations1, initial_approvals1, applicants1);
99         // 예상 결과: [true, true, false, true]

100 System.out.println("예시 1 결과: " + Arrays.toString(results1)); 101 102 // 예시 2 103 int[][] relations2 = {{2, 1}, {5, 4}, {6, 5}}; 104 int[] initial_approvals2 = {1, 4}; 105 int[] applicants2 = {2, 6, 7, 4}; 106 boolean[] results2 = sol.solution(relations2, initial_approvals2, applicants2); 107 // 예상 결과: [true, true, false, true] 108 System.out.println("예시 2 결과: " + Arrays.toString(results2)); 109 } 110 }

실행 결과

1 예시 1 결과: [true, true, false, true] 2 예시 2 결과: [true, true, false, true]

╭───────────────────────────────────╮ │ > 그럼 이건 어떤 유형 문제인데? │ ╰───────────────────────────────────╯

✦ 좋은 질문입니다. 이 문제는 여러 알고리즘 유형의 특징을 함께 가지고 있습니다.

핵심적인 순서대로 나열하면 다음과 같습니다.

  1. 그래프 탐색 (Graph Traversal) 이것이 가장 중요한 핵심 유형입니다.

  • 이유: 고객들을 정점(Vertex), 보증 관계를 간선(Edge)으로 하는 방향 그래프(Directed Graph)로 문제를 모델링할 수 있습니다. '대출 승인' 여부를 찾는 과정은, 특정 정점에서 시작하여 간선을 거꾸로 따라가며 '초기 승인자'라는 특정 속성을 가진 루트 정점(Root Node)에 도달할 수 있는지 경로를 탐색하는 과정과 같습니다.

  • 세부 유형: 문제 조건에 '순환이 없다'고 명시되어 있으므로 DAG(Directed Acyclic Graph, 방향성 비순환 그래프)에서의 탐색 문제입니다.

  1. 동적 계획법 (Dynamic Programming) / 메모이제이션 (Memoization) 이것은 문제를 효율적으로 풀기 위해 적용된 기법입니다.

  • 이유: 4 -> 2 -> 1의 보증 관계에서 4와 2를 모두 심사해야 하는 경우를 생각해 보세요. 4를 심사할 때 2의 승인 여부를 이미 확인했는데, 나중에 2를 또 심사하면 같은 과정을 불필요하게 반복하게 됩니다. 이처럼 부분 문제의 결과가 중복되어 사용될 수 있습니다.

  • 적용: memo라는 캐시(Map)를 사용해 한 번 계산한 고객의 승인 여부 결과를 저장했습니다. 이를 통해 중복 계산을 피하고 시간 복잡도를 크게 줄일 수 있습니다. 이것이 바로 동적 계획법의 핵심 아이디어인 메모이제이션입니다.

한마디로 정의하면?

메모이제이션 기법을 적용한 DAG(방향성 비순환 그래프)에서의 경로 탐색 문제입니다.

코딩 테스트에서는 그래프 탐색으로 분류되며, 효율성(시간 초과 방지)을 만족시키기 위해 동적 계획법(메_모이제이션)을 함께 사용해야 하는 문제입니다.

╭─────────────────────────────────────╮ │ > 프로그래머스에 비슷한 문제있음? │ ╰─────────────────────────────────────╯

✦ 네, 프로그래머스에 지금 푸신 문제와 아주 유사한 원리를 사용하는 문제들이 있습니다. 특히 트리/그래프 구조를 DFS로 탐색하며 동적 계획법(메모이제이션)을 적용하는 유형이 그렇습니다.

가장 비슷한 문제 ⭐⭐⭐

[2021 카카오 블라인드 채용] 매출 하락 최소화 (Level 4)

  • 어떤 점이 비슷한가요?

    1. 트리(DAG) 구조: 회사의 조직도가 트리 구조로 주어집니다. 이는 우리가 다룬 보증 관계(DAG)와 사실상 같습니다.

    2. DFS 탐색: 각 직원(노드)을 기준으로 DFS를 통해 팀(서브트리)을 순회해야 합니다.

    3. DP/메모이제이션: 각 직원이 워크숍에 참석할 경우와 불참할 경우의 최소 매출액을 계산하고, 그 결과를 DP 테이블(메모이제이션)에 저장해야 합니다. 상위 노드는 하위 노드의 계산된 DP 값을 이용해 자신의 값을 결정합니다. 이는 우리가 memo 맵에 승인 여부를 저장한 것과 동일한 원리입니다.

  • 어떤 점이 다른가요?

    • 우리가 푼 문제는 true/false만 판별하면 되지만, 이 문제는 '최소 비용'을 계산해야 하므로 DP 로직이 조금 더 복잡합니다. 하지만 근본적인 접근 방식(DFS + DP)은 거의 같습니다. 이 문제를 풀 수 있다면 우리가 다룬 유형은 확실히 마스터하신 겁니다.

Last updated