현이의 개발 이야기

[프로그래머스/Java] 도넛과 막대 그래프 - Lv 2 문제 풀이


https://school.programmers.co.kr/learn/courses/30/lessons/258711?language=java

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

구현 중심의 문제로, 제시된 조건만 파악하면 쉽게 풀리는 문제였다.

 

문제에서 파악해야 하는 핵심 조건이 무엇이었는지를 알아보고, 이를 해결하기 위한 구현 과정을 살펴보자.


문제 풀이

문제에서는 도넛 묘양, 막대 모양, 8자 모양의 세 종류의 그래프를 제시합니다. 그리고 이 세 그래프에 속하지 않은 하나의 노드가 추가되어 모든 그래프를 연결합니다.

그래프 별 노드의 특징

먼저, 각 그래프에 속한 노드와 새로 추가한 노드의 특징을 살펴봅시다.

도넛 모양 그래프의 노드

도넛 모양 그래프의 모든 노드는 다음의 특징을 갖습니다.

  • 하나의 나가는 간선과 하나의 들어오는 간선이 있습니다.
  • 간선을 따라가다보면 자기 자신을 만나게 됩니다.

막대 모양 그래프의 노드

막대 그래프의 모든 노드는 다음의 특징을 갖습니다.

  • 최대 하나의 나가는 간선과, 최대 하나의 들어오는 간선이 있습니다.
  • 간선을 따라가다보면 막다른 노드에 도달합니다.

8자 모양 그래프의 노드

8자 모양 그래프의 모든 노드는 다음의 특징을 갖습니다.

  • 하나의 들어오는 간선과 나가는 간선 또는 두 개의 들어오는 간선과 나가는 간선이 있습니다.
  • 간선을 따라가다보면 두 개의 들어오는 간선과 나가는 간선을 갖는 노드를 만나게 됩니다.

새로 삽입된 노드

새로 삽입된 노드는 두 개 이상의 그래프를 연결하기 때문에 다음의 특징을 갖습니다.

  • 들어오는 간선이 없습니다.
  • 두 개 이상의 나가는 간선이 있습니다.

삽입된 노드 찾아 그래프 분리하기

위에서 살펴 본 노드의 특징을 활용하여 삽입된 노드를 찾아, 그래프들을 종류별로 분리할 수 있습니다.

삽입된 노드 찾기

새로 삽입된 노드의 특징 중 하나인 들어오는 간선이 없는 노드를 생각해 봅시다.

막대 모양 그래프의 시작 노드가 이러한 특징을 가질 수 있습니다. 그러나 나가는 간선이 두 개 이상이어야 한다는 점을 생각하면 이를 만족하는 노드는 새로 삽입된 노드 뿐입니다.

따라서 모든 노드를 순회하며, 들어오는 간선이 없고, 나가는 간선이 두 개 이상인 노드를 찾는다면, 해당 노드가 삽입된 노드임을 알 수 있습니다.

그래프 분리하기

삽입된 노드를 찾은 후에, 해당 노드와 연결된 간선들을 모두 끊으면 종류별로 명확히 분리된 그래프를 얻을 수 있습니다.

위 그림에서 알 수 있듯이, 삽입된 노드를 제거하면 왼쪽부터 8자 모양 그래프, 막대 모양 그래프, 8자 모양 그래프임을 쉽게 알 수 있습니다.

그래프 구분하기

그래프를 분리했으니 이제 각 그래프가 어떤 모양인지를 검사해야 합니다.

 

이 과정은 앞서 살펴본 노드의 특징을 활용하면 쉽게 할 수 있습니다.

 

임의의 한 노드로부터 출발하여 간선을 따라가다가,

  • 막다른 노드에 도달한다면: 막대 모양 그래프
  • 나가는 간선과 들어오는 간선의 수가 모두 2인 노드에 도달한다면: 8자 모양 그래프
  • 출발 노드로 돌아온다면: 도넛 모양 그래프

여기서 주의할 점은, 8자 모양 그래프에 대한 검사를 도넛 모양 그래프 검사보다 우선 해야 한다는 것입니다.

8자 모양 그래프도 한 노드로부터 간선을 따라가다보면 자기 자신이 나오게 됩니다. 하지만 그 이전에 8자 모양 그래프의 중심 노드를 거쳐야 하기 때문에 이를 우선 검사하여 8자 모양 그래프와 도넛 모양 그래프를 구분합니다.

코드

이를 코드로 구현하면 다음과 같습니다.

import java.util.*;
import java.util.function.Function;

class Solution {
    enum NodeType {
        DONUT,
        LINEAR,
        EIGHT,
    }
    
    static class Node {
        List<Node> outs = new ArrayList<>();
        List<Node> ins = new ArrayList<>();
        NodeType type;
    }
    
    private static Map<Integer, Node> constructNodes(int[][] edges) {
        Map<Integer, Node> nodes = new HashMap<>();
        
        for (int[] edge : edges) {
            int u = edge[0];
            int v = edge[1];
            
            if (!nodes.containsKey(u)) nodes.put(u, new Node());
            if (!nodes.containsKey(v)) nodes.put(v, new Node());
            
            Node from = nodes.get(u);
            Node to = nodes.get(v);
            
            from.outs.add(to);
            to.ins.add(from);
        }
        
        return nodes;
    }
    
    private static int findInsertedNodeKey(Map<Integer, Node> nodes) {
        for (int key : nodes.keySet()) {
            Node node = nodes.get(key);
            if (node.ins.size() == 0 && node.outs.size() >= 2) {
                return key;
            }
        }
        
        // Unreachable
        return -1;
    }
    
    private static int removeInsertedNode(Map<Integer, Node> nodes) {
        int insertedKey = findInsertedNodeKey(nodes);
        Node inserted = nodes.remove(insertedKey);
        
        for (Node node : inserted.ins) {
            node.outs.remove(inserted);
        }
        for (Node node : inserted.outs) {
            node.ins.remove(inserted);
        }
        return insertedKey;
    }
    
    private static Node getUnvisitedNext(Node node, List<Node> direction) {
        for (Node next : direction) {
            if (next.type == null) return next;
        }
        return null;
    }
    
    private static void mark(Node node, NodeType type, Function<Node, List<Node>> getDirection) {
        do {
            node.type = type;
            node = getUnvisitedNext(node, getDirection.apply(node));
        } while (node != null);
    }
    
    private static NodeType check(Node node) {
        Node initial = node;
        while (true) {
            if (node.outs.size() == 0) {
                mark(node, NodeType.LINEAR, n -> n.ins);
                return NodeType.LINEAR;
            } else if (node.ins.size() == 2 && node.outs.size() == 2) {
                mark(getUnvisitedNext(node, node.outs), NodeType.EIGHT, n -> n.outs);
                return NodeType.EIGHT;
            } 
            
            node = getUnvisitedNext(node, node.outs);
            if (node == null) {
                // Unreachable
                break;
            }
            
            if (node == initial) {
                mark(node, NodeType.DONUT, n -> n.outs);
                return NodeType.DONUT;
            }   
        };
        return null;
    }
    
    public int[] solution(int[][] edges) {
        Map<Integer, Node> nodes = constructNodes(edges);
        int insertedKey = removeInsertedNode(nodes);
        
        int donut = 0;
        int linear = 0;
        int eight = 0;
        
        for (Node node : nodes.values()) {
            if (node.type != null) continue;
            
            switch (check(node)) {
                case LINEAR:
                    linear += 1;
                    break;
                case EIGHT:
                    eight += 1;
                    break;
                case DONUT:
                    donut += 1;
                    break;
            }
        }
        
        return new int[] { insertedKey, donut, linear, eight };
    }
}
코딩 테스트를 준비하고 있다면? 더 좋은 코드를 작성하고 싶다면?
79개 문제 풀이, 코딩전문역량인증시험(PCCP) 대비까지!

합격에 한 걸음 더 가까워지는 실전형 코딩 테스트 문제 풀이 가이드
취업과 이직을 위한 프로그래머스 코딩 테스트 문제 풀이 전략 : 자바편