BOJ

[백준 1316번/JAVA] 그룹 단어 체커

syj0522 2024. 3. 13. 21:06

문제

백준 1316번

실버는 어려워

두 번째로 맞이하는 실버5단계 문제.. 쉽지 않다.
이번 문제는 문제를 풀면서 겪었던 삽질에 대해 좀 길게 써보려고 한다.
코테를 공부할 때는 대표적인 알고리즘 코드를 외우다싶이 반복해서 비슷한 문제에 써먹는 게 최고의 방법이라지만,
내가 다 풀 수 있을 것 같은데 자꾸만 안 되는 답답한 상황에 그만 자존심을 못 버리고 삽질을 해버렸다.
지나고 보니 정답률이 53%나 되는 생각보다 간단한 문제였지만, 이렇게 느끼는 것도 삽질이 없었다면 불가능하지 않았을까 싶고, 나중에 오래 기억에 남을 테니까 오히려 좋은 거 아닌가 하는 긍정적인 생각으로 마인드셋을 또 해본다. 하하

처음 구현한 코드

이 문제를 접하고 처음 설계한 알고리즘은 맞았는데, 구현 능력이 낮아서 실패했다.
내가 알고리즘을 설계할 때 떠올렸던 생각의 흐름을 기록하고 넘어가겠다.

 

먼저 단어를 N번 입력받았으므로 가장 바깥에 반복문이 N번 실행되어야 하고,
단어마다 한 문자씩 검사해야 하므로 안쪽에 단어 길이만큼의 for문이 하나 더 위치해야 한다.

 

이전에 나온 알파벳인지 확인해야 하므로 문자 하나를 추출할 때마다 해당 알파벳이 등장했는지 아닌지 기록한다.

  • 길이 26의 int 배열 사용, 모든 요소 0으로 초기화
  • 처음 등장하는 알파벳이라면 1삽입, 이미 1이라면 이전 문자와 비교 -> 같으면 통과 / 같지 않으면 그룹 단어가 아님

여기까지 설계하고 '같으면 통과 / 같지 않으면 그룹 단어가 아님'을 어떻게 구현해야 할지 전혀 감히 안 왔다.

 

그래서 일단 에시 문자열을 하나 정해서 내가 짠 알고리즘을 손코딩해봤다.
직접 i=0, i=1, ...인 경우 어떤 코드를 거쳐야할지 생각하며 노트에 써보았다.
예) 단어가 aabcad인 경우: arr1[] 모두 0으로 초기화, i=0일 때 arr2[0]=charAt(0), j=0일 때 if(charAt(0)=a) arr1[0]=1 ...

 

그렇게 완성한 알고리즘 :

  • 알파벳이 처음 등장 -> 배열에서 해당 알파벳의 인덱스를 1로 만들기, 다음 문자 계속 검사
  • 알파벳이 중복
    • 이전 문자와 같음 -> 다음 문자 계속 검사
    • 이전 문자와 다름 -> 그룹 단어가 아님

그리고 바로 코드를 짰다.
필요한 변수나 배열은 미리 생각해두었으므로 그때그때 선언해서 썼다.

//굳이 이해하지 않아도 됩니다..

import java.util.*;
import java.io.*;

public class Main {
    public static void main(String[] args)throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        //앞서 나온 글자가 연속하지 않는 것을 발견하면 그룹 단어로 세지 않기

        int count = 0; //그룹 단어 수를 저장할 변수

        int[] arr1 = new int[26]; //알파벳이 등장했는지 확인하기 위한 배열
        for(int i=0; i<arr1.length; i++) { //모두 0으로 초기화
            arr1[i] = 0;
        }

        int N = Integer.parseInt(br.readLine()); //단어의 개수 입력받기

        for(int k=0; k<N; k++) {

            String str = br.readLine();

            //한 문자열마다 arr2 새로 생성 : 이전 문자와의 비교를 위한 공간
            char[] arr2 = new char[str.length()];

            for(int i=0; i<str.length(); i++) { //단어마다 실행
                //현재 문자 저장
                arr2[i] = str.charAt(i);

                //현재 알파벳 자리에 1 저장
                for(int j=0; j<26; j++) { //추출한 문자가 어떤 알파벳에 해당되는지 for문으로 검사

                    if(str.charAt(i) == (char)(j+97)) { 
                        if(arr1[j] == 0) { //처음 등장한 알파벳
                            arr1[j] = 1;
                            break;

                        }else if(arr1[j] != 0) { //이전에 등장한 적이 있는 알파벳

                            if(arr2[i-1] != arr2[i] && i<str.length() && i>0) { //바로 전 문자와 다르다면
                                //해당 문자열은 더 볼 필요 없이, 다음 문자열 검사하면 됨
                                //첫 번째 for문까지 한꺼번에 탈출하기
                                i+=(str.length()-i);
                                break;

                            }else if(arr2[i-1] == arr2[i] && i<str.length() && i>0) { //바로 전 문자와 같다면
                                if(arr2.length == str.length()) //arr2가 모두 채워졌다면 count++
                                    count++;
                                //다음 문자 계속 검사하기
                                break;
                            }

                        }
                    }                
                }

            }
        }
        bw.write(count+" ");
        bw.close();
    }
}

IDE에서 예제를 입력해보니 실행 결과는 전혀 맞지 않고, 어떤 단어에서는 접근 불가능한 인덱스 에러가 발생하기도 했다.


그래서 정확한 원인 파악도 안된 채 조건문에 and연산자로 여러 조건을 붙여보기도 하고, 처음 등장한 알파벳이라면 for문을 두 번이나 탈출해야해서 두 번째 for문에 break문을 썼다 지웠다 반복.. 이상하게 디버깅을 했다.

  • 이 코드의 문제점 + 개선 방안
    • 추출한 문자가 어떤 알파벳인지 굳이 for문을 사용해서 26번 확인하고있다. 이 부분은 일일이 a~z까지 탐색할 필요 없이 추출한 문자의 유니코드로 바로 알 수 있다.
    • 알파벳 체크용 배열을 int로 사용하고있다. int보다 boolean이 효율적이다. boolean을 사용하면 0으로 초기화하는 과정이 필요없어진다.
    • 추출한 문자를 char타입 그대로 사용하고 있다. 추출한 문자를 가지고 어떤 작업을 하는지 생각해보면, 아스키코드(int형)로 받는 게 더 효율적일 것이다. 어차피 이전 문자와 같은지만 체크하면 되고, 배열의 인덱스로도 쓰여야한다.
    • 단어가 그룹 단어인지 체크하는 코드를 별도의 함수로 만드는 게 낫다. for문이 세 번이나 중첩되어있을 정도로 복잡한데도 이미 짠 코드를 수정하면 못 알아보게될까봐 별도의 함수로 빼지 않았다. 별도의 함수로 빼서 boolean형으로 선언하면 return false를 이용해 그룹 단어가 아님을 표현할 수 있다.

정답 코드 1

위의 개선방안을 모두 수용해서 수정한 코드이다.

checker함수 설명 :

  • 알파벳이 처음 등장 -> checkAlpha배열에서 해당 알파벳의 인덱스를 true로 만들기, pre를 now로 변경, 다음 문자 계속 검사
  • 알파벳이 이전에 나온 적 있음
    • 이전 문자와 다름 -> 그룹 단어가 아님 (return false)
    • 이전 문자와 같음 -> 다음 문자 계속 검사
import java.util.*;
import java.io.*;

public class Main {    
    public static void main(String[] args)throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        int count = 0;
        int N = Integer.parseInt(br.readLine());
        for(int i=0; i<N; i++) {
            if(checker(br.readLine())) {
                count++;
            }
        }
        bw.write(count+" ");
        bw.close();

    }
    static boolean checker(String str) {
        int pre = -1; //이전 문자를 저장할 pre변수 선언, -1로 초기화
        boolean[] checkAlpha = new boolean[26]; //알파벳이 이전에 등장했는지 확인할 boolean타입의 checkAlpha배열 선언. 초기화하지 않아도 초기값 모두 false

        for(int i=0; i<str.length(); i++) {
            int now = str.charAt(i);

            if(checkAlpha[now-97]==false) { //알파벳이 처음 등장
                checkAlpha[now-97] = true; //해당 알파벳의 인덱스를 true로 만들기
                pre = now;
                continue;
            }else { //알파벳이 이전에 나왔음
                if(pre != now) { //이전 문자와 다름 -> 그룹 단어 아님
                    return false;
                }else { //이전 문자와 같음 -> 다음 문자 계속 검사
                    continue;
                }
            }
        }
        return true;
    }
}

 

정답코드 2

정답코드 1과 검사 순서가 반대지만 결론적으로 동작 방식은 같다.

 

checker함수 설명 :

  • 이전 문자와 다름 
    • 알파벳이 처음 등장 -> checkAlpha배열에서 해당 알파벳의 인덱스를 true로 만들기, prev을 now로 변경, 다음 문자 계속 검사
    • 알파벳이 이전에 나온 적 있음 -> 그룹 단어가 아님 (return false)

이전 문자와 같음 -> 다음 문자 계속 검사

import java.util.*;
import java.io.*;

//aabca

public class Main {	
	public static void main(String[] args)throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
		
		int count = 0;
		int N = Integer.parseInt(br.readLine());
		
		for(int i=0; i<N; i++) {
			if(checker(br.readLine())) {
				count++;
			}
		}
		bw.write(count+" ");
		bw.close();
	}
	
	static boolean checker(String str) {
		
		int prev = -1; //이전 문자 저장할 변수
		boolean[] checkAlpha = new boolean[26]; //이전에 나온 알파벳인지 확인할 배열
		
		for(int i=0; i<str.length(); i++) {
			int now = str.charAt(i); //현재 문자 저장할 변수
			
			if(prev != now) { //바로 전 문자와 같지 않다면 이전에 나왔던 알파벳인지 검사해야 함
				if(checkAlpha[now-97] == false) { //처음 등장하는 알파벳이면
					checkAlpha[now-97] = true; //true로 변경
					prev = now;
				}else { //이전에 등장했던 알파벳이면
					return false; //그룹 단어가 아니므로 false 리턴
				}
			}else { //바로 전 문자와 같다면
				continue; //계속 다음 문자 검색하면 됨
			}
		}
		return true;
	}
}

 

Note

  • 아직 어떻게 하면 모든 경우의 수를 안 빼먹고 설계할 수 있는지는 모르겠다. 그래서 그냥 그 부분은 신경쓰지 않고 짜보는 걸로. 웬만한 알고리즘을 다 습득하고 나면 자연히 알게되지 않을까?
  • [Cannot make a static reference to the non-static method checker(String) from the type Main] 에 : static으로 선언된 메서드에서는 static으로 선언된 메서드만 불러올 수 있다. static, non-static 메서드는 다른 메모리 영역에서 생성되어 동작하기 때문이다. 
  • checkAlpha 배열을 int타입으로 선언해서 모두 0으로 초기화한 후 사용한 경우 vs boolean타입으로 선언한 경우 성능 차이. (boolean이 위)