-
[백준-1165,13502] 단어 퍼즐 (Trie,DFS,FTP)Baekjoon 2020. 5. 19. 14:22
*답을 구하는 해법을 기술 하였고 실제 제출시 통과되는 풀이는 아닙니다.
*실제 제출시 통과되는 풀이로 업데이트 예정
문제 요약
백준에서 이 문제는 무려 다이아 문제이다. 소스 코드량에 제한이 있어 코드량 최적화가 필요한 문제이다. 대문자 알파벳이 하나씩 적혀있는 5x5의 보드와 단어 리스트 (사전)가 주어진다.
주어진 보드의 각 칸에서 8방향 탐색을 통해 사전에 있는 단어를 몇개 만들 수 있는지를 구하는 문제이다.
문제 풀이
문자열을 검색한다는 개념에서 Trie를 활용한 풀이를 기술한다.
다만 이 문제의 가장 큰 제약사항은
"소스 코드의 크기가 65536B를 넘을 수 없다"는 점이다.
제약사항을 봤을 때
이 문제를 풀기 위한 올바른 접근 방법은 코드안에 사전 데이터를 미리 넣어야 한다는 것이다.
주어진 사전 속의 단어의 개수는 24830개이고 이 텍스트 데이터들을 코드에 그대로 넣으면
이클립스 같은 경우 다음과 같은 컴파일 에러를 보게된다.
"The code of method makeDictionaryByTrie() is exceeding the 65535 bytes limit"
이것은 Java 코드를 돌려주는 JVM의 최대 메소드 사이즈가 64k로 지정이 되어 있어 이를 초과했다는 에러 메세지이다.
또한 사전 데이터를 그대로 넣으면 소스 코드가 20만~30만 바이트가 된다.
따라서 이 문제를 풀기 위해서는 주어진 사전 단어 데이터들을 제약사항에 맞게 최적화 해야하는 것이다.
본문에서는 소스 코드를 최적화 하지 않고 FTP 개념을 활용하여 원격에 있는 사전 데이터 파일을 요청 및 receive하여 추출한 뒤 구현한 Trie에 전부 등록한 후 dfs를 통해 답을 구하는 풀이를 기술한다.
Trie는 문자열의 집합을 표현하는 트리 형태의 자료구조이다.
출처 : https://en.wikipedia.org/wiki/Trie Solution
1. 사전(Dictionary) 데이터를 소스 코드에 집어 넣지 않고 다운로드 url을 통해 파일 다운로드를 하여 내용물을 추출한다.
대회 사이트 혹은 백준 문제 하단의 다운로드 도메인 주소를 url로 지정해 준다.
https://contest.usaco.org/FEB09.htm
단어 파일의 형태는 Zip으로 압축되어 있어서 자바의 다음 라이브러리를 활용한다.
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
24830개의 단어를 읽어 String[]에 저장 한 후 Trie에 등록한다.
12345678910111213141516171819private static void getDictionaryAndsetTrie() throws UnknownHostException, IOException {URL url = new URL("https://d2gd6pc034wcta.cloudfront.net/data/1165.zip");StringBuilder sb = new StringBuilder();InputStream in = new BufferedInputStream(url.openStream(), 1024*4);ZipInputStream stream = new ZipInputStream(in);byte[] buffer = new byte[1024*4];ZipEntry entry;while ((entry = stream.getNextEntry()) != null) {int read;String data ="";while ((read = stream.read(buffer, 0, 1024*4)) >= 0) {data = new String(buffer, 0, read);sb.append(data);//System.out.println(data);}}wordlist = sb.toString().split("\n");//개행 문자로 스플리트for(String data : wordlist) Trie.insert(data); //트라이에 등록}cs 2. Trie를 구성하는 각 노드는 26개(알파벳 개수) 짜리의 TrieNode 배열로 구성하였다.
아스키코드를 통해 현재 글자에서 다음 글자로 갈 수 있는 것인지 부모-자식 관계를 통해 확인하고 등록한다.
ABC라는 단어가 있다고 하자.
root에서 시작하여 한글자씩 파싱, 파스 트리를 구성한다.
1. root의 자식 26개 TrieNode 중 A가 null인지 확인. null 이라면 'A'-'A'(아스키) 인덱스에 TrieNode 생성.
2. 만들어진 A의 자식 26개 TrieNode중 B가 null인지 확인. null 이라면 'B'-'A' 인덱스에 TrieNode 생성.
3. 만들어진 B의 자식 26개 TrieNode중 C가 null인지 확인. null 이라면 'C'-'B' 인덱스에 TrieNode 생성 끝.
이후 AB라는 단어를 추가 등록할 때
1. root의 자식 26개 TrieNode 중 A가 null인지 확인. null 이아니므로 'A'-'A' 인덱스에 해당하는 자식 노드 확인
2. 확인된 A의 자식 26개 TrieNode 중 B가 null인지 확인. null이 아니므로 AB는 이미 Trie 자료구조에 존재하는 단어가 된다.
12345678910111213141516static class TrieNode{TrieNode[] children = new TrieNode[26];//26개의 알파벳 하나하나의 인덱스를 뜻하는 배열}static class Trie{ //트라이, 문자검색TrieNode root = new TrieNode(); //가장 루트는 빈문자열void insert(String word){//어떤 단어를 받아서 처리TrieNode current = root;for(int i=0;i<word.length();i++){//들어온 단어의 길이만큼 첫글자부터 확인int wordIndex = word.charAt(i)-'A'; //글자 알파벳의 인덱스if(current.children[wordIndex]==null){//글자 알파벳을 확인해 봤을 때 null이라는건 존재하지않는다는것current.children[wordIndex] =new TrieNode();//없으니까 새로 생성해준다.}current = current.children[wordIndex];// 다음 레벨로 넘어가기 위해 생성된 글자가 현재 노드가 된다}}}cs 3. 구성한 트라이 노드를 통해 이제 문자를 검색하기 위해 5x5 보드를 dfs를 통해 탐색하여 단어가 존재하는지 확인한다.
24830개의 단어를 하나씩 보면서 첫번째 글자와 일치하는 칸에서 8방향 탐색을 통해 단어의 존재를 확인한다.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647private static void find(){System.out.println(wordlist.length);//단어개수for(int i=0;i<wordlist.length;i++){ //단어를 하나씩 봄.String data = wordlist[i];findFlag = false; //찾았으면 더 이상 볼 필요가 없기 때문에 발견 flag를 둠.for(int row=0;row<5;row++) {//퍼즐 탐색for(int col=0;col<5;col++){//첫번째 글자가 같은 경우if(Trie.root.children[puzzle[row][col]-'A']!=null && data.charAt(0)==puzzle[row][col]){visit[row][col] = true;//트라이 노드를 줘서 자식을 직접 확인해봄dfs(row,col,1,Trie.root.children[puzzle[row][col]-'A'],data);visit[row][col] = false;}if(findFlag) break;}if(findFlag) break;}}}private static void dfs(int row, int col, int depth, TrieNode current,String data){if(findFlag) return;if(depth==data.length() && findFlag==false){//길이가 같다면 dfs 탐색을 통해 단어를 찾았다는 것.answer++;findFlag = true;return;}if(depth < data.length()) {for(int dir=0;dir<8;dir++) {int nr = row+dr[dir];int nc = col+dc[dir];if(rangeCheck(nr,nc)){//자식이 존재할 때if(visit[nr][nc]==false && current.children[puzzle[nr][nc]-'A']!=null && data.charAt(depth)==puzzle[nr][nc]) {visit[nr][nc] = true;dfs(nr,nc,depth+1,current.children[puzzle[nr][nc]-'A'],data);visit[nr][nc] = false;}}}}}private static boolean rangeCheck(int nr, int nc) { //영역체크if(nr>=0 && nr<5 && nc>=0 && nc<5) return true;return false;}cs Full code
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.BufferedWriter;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.OutputStreamWriter;import java.net.URL;import java.net.UnknownHostException;import java.util.StringTokenizer;import java.util.zip.ZipEntry;import java.util.zip.ZipInputStream;public class Main{static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));static char[][] puzzle = new char[5][5];static boolean[][] visit = new boolean[5][5];static StringTokenizer st;static int[] dr = {-1,-1,0,1,1,1,0,-1};static int[] dc = {0,1,1,1,0,-1,-1,-1}; //8방향static int answer;static String[] wordlist;static Trie Trie = new Trie();static boolean findFlag;static class TrieNode{TrieNode[] children = new TrieNode[26];//26개의 알파벳 하나하나의 인덱스를 뜻하는 배열}static class Trie{ //트라이, 문자검색TrieNode root = new TrieNode(); //가장 루트는 빈문자열void insert(String word){//어떤 단어를 받아서 처리TrieNode current = root;for(int i=0;i<word.length();i++){//들어온 단어의 길이만큼 첫글자부터 확인int wordIndex = word.charAt(i)-'A'; //글자 알파벳의 인덱스if(current.children[wordIndex]==null){//글자 알파벳을 확인해 봤을 때 null이라는건 존재하지않는다는것current.children[wordIndex] =new TrieNode();//없으니까 새로 생성해준다.}current = current.children[wordIndex];// 다음 레벨로 넘어가기 위해 생성된 글자가 현재 노드가 된다}}}public static void main(String[] args) throws IOException{getDictionaryAndsetTrie();setData();if(wordlist!=null) find();System.out.println(answer);}private static void find(){//System.out.println("다운로드하여 추출한 단어의 개수 "+wordlist.length);//단어개수for(int i=0;i<wordlist.length;i++){String data = wordlist[i];findFlag = false;for(int row=0;row<5;row++) {//퍼즐 탐색for(int col=0;col<5;col++){if(Trie.root.children[puzzle[row][col]-'A']!=null && data.charAt(0)==puzzle[row][col]){//갈 수 있으면visit[row][col] = true;dfs(row,col,1,Trie.root.children[puzzle[row][col]-'A'],data); //트라이 노드를 줘서 자식을 직접 확인해봄visit[row][col] = false;}if(findFlag) break;}if(findFlag) break;}}}private static void dfs(int row, int col, int depth, TrieNode current,String data){if(findFlag) return;if(depth==data.length() && findFlag==false){answer++;findFlag = true;return;}if(depth < data.length()) {for(int dir=0;dir<8;dir++) {int nr = row+dr[dir];int nc = col+dc[dir];if(rangeCheck(nr,nc)){if(visit[nr][nc]==false && current.children[puzzle[nr][nc]-'A']!=null && data.charAt(depth)==puzzle[nr][nc]) {visit[nr][nc] = true;//자식이 존재할 때dfs(nr,nc,depth+1,current.children[puzzle[nr][nc]-'A'],data);visit[nr][nc] = false;}}}}}private static boolean rangeCheck(int nr, int nc) {if(nr>=0 && nr<5 && nc>=0 && nc<5) return true;return false;}private static void getDictionaryAndsetTrie() throws UnknownHostException, IOException {/** "https://d2gd6pc034wcta.cloudfront.net/data/1165.zip"* "/TESTDATA/coggle.zip"* 66.113.2.158**/URL url = new URL("https://d2gd6pc034wcta.cloudfront.net/data/1165.zip");//URL url = new URL("http://66.113.2.158/TESTDATA/coggle.zip");StringBuilder sb = new StringBuilder();InputStream in = new BufferedInputStream(url.openStream(), 1024*4);ZipInputStream stream = new ZipInputStream(in);byte[] buffer = new byte[1024*4];ZipEntry entry;while ((entry = stream.getNextEntry()) != null) {int read;String data ="";while ((read = stream.read(buffer, 0, 1024*4)) >= 0) {data = new String(buffer, 0, read);sb.append(data);//System.out.println(data);}}wordlist = sb.toString().split("\n");for(String data : wordlist) Trie.insert(data);}private static void setData() throws IOException {for(int row=0;row<5;row++) {st = new StringTokenizer(br.readLine());for(int col=0;col<5;col++) {puzzle[row][col] = st.nextToken().charAt(0);}}}}cs 결과
1. ftp로 다운 및 단어 추출
2. testcase in, out
테스트 케이스는 이곳에서 확인할 수 있다.
https://contest.usaco.org/TESTDATA/FEB09_3.htm
feb09 CONTEST Analysis and Data: coggle
contest.usaco.org
위 사이트의 실제 대회에서 코드 제출 파일 입출력이 가능하여 사전 텍스트 파일을 읽어들인 뒤 제출하는 방식이 가능하다.
백준의 1165번은 사전 텍스트 파일을 코드에 집어넣고 풀어야 하기 때문에
본문의 풀이는 백준 제약사항에 맞는 풀이는 아니다. (본문의 코드가 채점은 되지만 3%쯤 런타임에러가 난다.)
하지만 알고리즘,자료구조 측면에서 풀이법은 동일하기 때문에 ftp를 활용한 풀이를 기술하였고
제약사항에 맞는 풀이로 추후 게시할 예정이다.
github
Jangsukwoo/Algorithm
Contribute to Jangsukwoo/Algorithm development by creating an account on GitHub.
github.com
'Baekjoon' 카테고리의 다른 글
[백준-11112] Eight Puzzle (Astar Algorithm) (1) 2020.05.12 [백준 1422] 숫자의 신 (2) 2020.04.28