문제 풀이/알고리즘

[백준] (5052) 전화번호 목록

얌얌념념 2022. 11. 30. 11:19

링크 : https://www.acmicpc.net/problem/5052

사용 언어 : JAVA


 


내 생각

초기 생각

트라이 구조를 활용하자.

트라이(trie) 자료구조란?

노드에 전체 문자열 다 표기

 

동적 집합이나 연관 배열을 저장하는 데 사용되는 트리 자료 구조이다. 주로 문자열이 키인 경우가 많다.

쉽게 말해 그냥 문자열을 트리 구조로 표현(할 때 주로 사용)하는 것인데, 노드의 모든 자손은 노드에 연관된 문자열의 공통 접두사를 공유한다는 특징이 있다. 루트는 빈 문자열에 연관된다.

 

이 자료구조를 어떻게 활용할 수 있을까?

하나의 Node 객체에 담겨야하는 데이터는 두가지라고 생각했다.

  • boolean: 이 노드를 마지막으로 갖는 문자열이 있는가?
  • Node[10]: 자식 노드 (자식으로 올 수 있는게 0~9, 총 10가지이므로 그냥 크기 10짜리 배열을 생각했다)
더보기

Node[10]으로 만드니까 확실히 메모리 리소스를 많이 사용해야 했다.

나는 노드마다 데이터(문자)를 저장하고 그 문자가 있는지 일일이 찾아야하는게 귀찮아서 아예 배열의 인덱스로 찾아가도록 만든건데
음.. Node 배열 말고 List나 Set을 사용하면 더 나으려나 모르겠다ㅎ

트라이의 루트를 Node root로 두고 N가지 전화번호를 모두 삽입을 하는데, 삽입을 하는 과정에서 일관성에 어긋나면 바로 "NO"를 출력하고 아무 일 없이 모두 삽입이 완료되면 "YES"를 출력한다.

삽입과정

각 문자열의 0번째~n-1번째 문자열을 루트에서부터 내려오며 차례로 삽입하는데, 부모와 겹치지 않는 문자만을 삽입한다.

첫번째 전화번호가 123, 두번째 전화번호가 124일 때를 예로 들자.

맨 처음 root의 자식 배열 child[]은 다 null 일 것이다. 첫번째 전화번호에서 0번째 문자(1)을 삽입하는데, root의 child[1]=null이다. 이 경우 child[1]에 new Node를 할당해서 그 노드에서 다시 다음 문자를 삽입할 수 있는지 탐색한다.

첫번째 전화번호를 다 삽입하고 두번째 전화번호를 삽입할 때는 어떻게 될까.

cur = root일 때, cur.child[ second[0]==1 ] 존재
cur = cur.child[ second[0] ], cur.child[ second[1]==2 ] 존재
cur = cur.child[ second[1] ], cur.child[ second[2]==4 ] 존재 X => cur.child[4]에 새로운 노드를 할당해준다. 또 이번이 마지막 노드이므로 방금 할당한 새 노드에 마지막 표기를 해준다.

 

노드에 접두사(root~부모)를 뺀 나머지만 표기

최종적으로 저런 모습이 되게끔 만들었다.

그리고 문자열을 삽입하는 과정에서 일관성을 해치지 않는지 확인해줘야 했다.

이 문제의 경우 조건이 어떤 문자열 A를 접두사로 가지는 문자열 B가 있으면 일관성 X 인데, 이는

  1. root에서부터 차례로 내려오며 노드를 탐색할 때, 현재 탐색 중인 노드를 마지막으로 갖는 문자열이 있으면 일관성 X
  2. 현재 부모 노드에 삽입해야하는 노드가 문자열의 마지막 노드일 때, 이미 누가 그 자리에 Node 객체를 할당해 놓은 경우 일관성 X

정도로 나타낼 수 있다. (문자열을 길이 순으로 정렬해 둔다면 1번만 확인하면 된다.)

여하튼 저 두가지에서 걸린다면 바로 "NO" 출력 후 다음 테스트 케이스로 넘어간다.

 

최종 코드

더보기
import java.util.*;

public class Main_BJ_5052_전화번호_목록 {

    static int N;
    static Node root;

    // 트라이 자료구조 사용
    // 노드마다 1)이 노드를 끝으로 하는 문자열 유무, 2) 10개 노드 자식 을 가짐
    // 전화번호 하나씩 트라이에 추가할 때 마다 root에서부터 타고 내려가서
    // 방문하는 노드가 마지막인 문자열 있는지 확인,
    // 내가 마지막 노드인데 이미 누가 왔던 노드인지 확인 하면서 새로운 노드 추가해 감
    // 중간에 에러 안나면 정상적으로 insert 된 것
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int T = Integer.parseInt(br.readLine());
        StringBuilder sb = new StringBuilder();

        ArrayList<String> address;
        String yes = "YES", no = "NO", ret;

        for(int t=0; t<T; t++){
            N = Integer.parseInt(br.readLine());
            address = new ArrayList<>();

            // br.readLine()으로 하나씩 로직 돌릴거면서
            // 중간에 return으로 멋대로 끊지 말아야 한다
            // 아니면 처음부터 입력값 다 받고난 뒤에 돌려야 한다 멍청인가봐
            for(int i=0; i<N; i++) {
                address.add(br.readLine());
            }

            root = new Node();
            ret = yes;

            for (String a: address){
                if(!insert(a.toCharArray())){
                    ret = no;
                    break;
                }
            }
            sb.append(ret+"\n");
        }

        System.out.println(sb);
    }

    private static boolean insert(char[] address) {
        Node cur = root;
        int num;
        for(int a=0, n=address.length; a<n; a++){
            num = address[a] - '0';
            if(cur.isEnd)   return false;
            if(cur.child[num]==null) cur.child[num] = new Node();
            else if(a==n-1) return false;
            cur = cur.child[num];
        }
        cur.isEnd = true;
        return true;
    }

    static class Node{
        boolean isEnd;
        Node[] child;

        public Node(){
            isEnd = false;
            child = new Node[10];
        }
    }
}

 

++

다른 분의 코드를 보니 우선순위 큐를 사용한 풀이가 있었다.

public void main() throws IOException {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

    int t = Integer.parseInt(br.readLine());
    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < t; i++) {
        int n = Integer.parseInt(br.readLine());

        // 힙 만들어서 사전 순으로 정렬
        PriorityQueue<String> pq = new PriorityQueue<>();

        for (int j = 0; j < n; j++) {
            pq.add(br.readLine());
        }

        // 사전 상에서 가장 빠른 문자열
        String last = pq.poll();
        String ans = "YES";

        // 큐가 빌 때까지 하나씩 뽑으면서 바로 옆의 문자열끼리 비교
        // abc, abcd, abd, ... 이런 순으로 정렬되니까 옆문자열끼리는
        //   1. 접두사 포함 관계
        //   2. 중간부터 다름
        // 둘 중 하나임. 여기서 1번 관계가 있는지 찾아내야함
        while(!pq.isEmpty()){
            String now = pq.poll();
            if (now.startsWith(last)){
                ans = "NO";
                break;
            }
            last = now;
        }
        sb.append(ans+"\n");
    }
    System.out.print(sb);
}

이 방식이 훨씬 간단하고 빠른 것 같다


느낀점

  • 문자열 문제라 무조건 트라이만 떠올렸었는데 다른 자료구조를 이용해도 충분히 빠르게 풀 수 있다는 것을 느꼈다.