[알고리즘] KMP & Rabin Krap (문자열 매칭)

Coastby·2023년 3월 28일

String matching

백준 9253번에서 문제의 조건은 동일하다. 편의상 사용자가 출력한 문자열의 길이가 문제의 답과 동일하고, 답은 0보다 크다고 가정한다.이라는 조건이 있다. 따라서 세번째로 오는 문자열이 위의 두 문자열에 포함되는지 여부만 확인하면 된다.

백준 9253문제를 풀면서 contains()를 사용해서 통과를 하기는 했다. 그러나 더 빠른 방법을 찾다가 문자열에서 포함여부를 확인하는 알고리즘으로 KMP 알고리즘을 알게되었다. 그래서 contains()는 어떻게 구현되어 있는지 궁금해서 찾아보았다. (잘 짜여져있을 거라 기대했다..)

String 클래스의 contains()는 indexOf()를 이용한 것이었다.

public boolean contains(CharSequence s) {
    return indexOf(s.toString()) >= 0;
}

그리고 indexOf()의 메서드를 따라가면 StringLatin1 클래스의 indexOf() 메서드를 만난다.

@IntrinsicCandidate
public static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) {
    byte first = str[0];
    int max = (valueCount - strCount);
    for (int i = fromIndex; i <= max; i++) {
        // Look for first character.
        if (value[i] != first) {
            while (++i <= max && value[i] != first);
        }
        // Found first character, now look at the rest of value
        if (i <= max) {
            int j = i + 1;
            int end = j + strCount - 1;
            for (int k = 1; j < end && value[j] == str[k]; j++, k++);
            if (j == end) {
                // Found whole string.
                return i;
            }
        }
    }
    return -1;
}

별거없네..

KMP 알고리즘

  • Knuth-Morris-Pratt 알고리즘
  • 문자열 매칭 알고리즘
  • 접두사와 접미사를 이용하여 매칭이 실패했을 때 일치했던 부분까지 되돌아가서 다시 검사하는 방식
  • 일치하지 않는 문자가 나왔을 때 아래와 같이 점프할 수 있다.

table 만들기

문자열을 하나씩 매칭하다가 다른 경우, 찾으려는 문자열에서 어느 인덱스로 돌아갈지 저장하는 table이다. KMP에서는 이를 참고해서 매칭을 더 빨리 할 수 있다.

static int[] makeTable(String str) {
	int[] table = new int[str.length()];
	int idx = 0;
	for (**int i = 1**; i < table.length; i++) {
		while (idx > 0 && str.charAt(i) != str.charAt(idx)) {
			idx = table[idx - 1];
		}

		if (str.charAt(idx) == str.charAt(i)) {
			table[i] = ++idx;
		}
	}
	return table;
}

최종 답안

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

public class Main {

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		String A = br.readLine();
		String B = br.readLine();
		String answer = br.readLine();
		int[] table = makeTable(answer);

		String result = "NO";
		if (KMP(A, answer, table) && KMP(B, answer, table)) {
			result = "YES";
		}
		System.out.println(result);
	}

	static int[] makeTable(String str) {
		int[] table = new int[str.length()];
		int idx = 0;
		for (int i = 1; i < table.length; i++) {
			while (idx > 0 && str.charAt(i) != str.charAt(idx)) {
				idx = table[idx - 1];
			}

			if (str.charAt(idx) == str.charAt(i)) {
				table[i] = ++idx;
			}
		}
		return table;
	}

	static boolean KMP (String parent, String target, int[] table) {
		int idx = 0;
		for (int i = 0; i < parent.length(); i++) {
			while (idx > 0 && parent.charAt(i) != target.charAt(idx)){
				idx = table[idx - 1];
			}
			if (parent.charAt(i) == target.charAt(idx)) {
				if (idx == target.length()-1) {
					return true;
				} else {
					idx++;
				}
			}
		}
		return false;
	}
}

Rabin-karp (Rolling-hash)

  • 해싱을 이용해서 찾으려는 문자열과 sliding index로 어느 한 부분을 비교한다. 같은 문자열이라면 같은 해싱값을 가질 것이다. (하지만 혹시 모르니 한 번 더 체크를 하긴한다.)

Rabin fingerprint

아래와 같은 해싱함수를 이용한다. 왜냐하면 옆으로 옮겼을 때 모든 값을 다시 계산하지 않고 양 사이드 값만 빼고, 더해주면 되기 때문이다.
하지만 찾으려는 문자열의 크기가 크면 숫자가 너무 커져 모듈러를 적용해보려고 했는데 복잡해져서 보류하였다.

Untitled

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		String A = br.readLine();
		String B = br.readLine();
		String answer = br.readLine();

		String result = "NO";
		if (rabin(A, answer) && rabin(B, answer)) {
			result = "YES";
		}
		System.out.println(result);
	}

	static boolean rabin (String parent, String target) {
		boolean result;
		int tLen = target.length();
		int power = 1;
		int tHash = 0;
		int pHash = 0;

		for (int i = 1; i < tLen; i++) {
			tHash += power * target.charAt(tLen - i);
			pHash += power * parent.charAt(tLen - i);
			power *= 2;
		}

		tHash += power * target.charAt(0);
		pHash += power * parent.charAt(0);

		for (int i = 0; i <= parent.length() - tLen; i++) {
			if (i > 0) {
				pHash -= power * parent.charAt(i-1);
				pHash = pHash * 2 + parent.charAt(i + tLen - 1);
			}
			if (tHash == pHash) {
				result = true;
				for (int j = 0; j < tLen; j++) {
					if (parent.charAt(i + j) != target.charAt(j)) {
						result = false;
						break;
					}
				}
				if (result) return true;
			}
		}
		return false;
	}
}

비교

Rabin이 조금 더 빠르다.

Rabin

Untitled

KMP

profile
훈이야 화이팅

0개의 댓글