ABOUT ME

Today
Yesterday
Total
  • [Python 3] 백준 1786-찾기
    BOJ/KMP 2020. 2. 17. 19:34

    https://www.acmicpc.net/problem/1786

     

    1786번: 찾기

    첫째 줄에, T 중간에 P가 몇 번 나타나는지를 나타내는 음이 아닌 정수를 출력한다. 둘째 줄에는 P가 나타나는 위치를 차례대로 출력한다. 예컨대, T의 i~i+m-1번 문자와 P의 1~m번 문자가 차례로 일치한다면, i를 출력하는 식이다.

    www.acmicpc.net

    KMP를 그대로 이용하는 문제.

    KMP 알고리즘

    Knuth-Morris-Pratt 알고리즘의 줄임말이다. 입력으로 전체 문자열과 찾고 싶은 문자열을 받는다.

    뇌를 비우고 문자열을 확인한다면, 첫 번째 문자부터 끝까지 다 뒤져 보면서 맞는 것을 찾을 수 있겠지만, 전체 문자열의 길이=N, 찾고 싶은 문자열의 길이=M이라 하면 O(NM)이다. 못 써먹는다.

    다만, 여기서 기존의 결과를 이용하여 처음부터 가망이 없는 결과를 다 스킵한다면 시간복잡도를 줄일 수 있다.

    예를 들어서, 문자열 'ABC ABCDAB ABCDABCDABDE'에서 'ABCDABD'를 찾는다고 한다면...

    • 최초에는, 첫 세 글자 ABC만 일치한다. 
    • 그 이후 2번째부터~4번째부터 시작할 때는 첫판부터 OUT.
    • 5번째부터 7번째까지는 ABC가 전부 일치한다. 즉 정석적으로 보면 1번째 글자 체크 이후 5번째로 바로 스킵해야 함.

    이를 빠르게 찾기 위해서는, 첫 문자열의 끝 부분과 이후에 확인해야만 하는 문자열이 중첩되므로, Prefix(접두사)와 Suffix(접미사)가 같은 경우를 찾아야 한다. 예를 들어서 abcdabc 같은 경우 앞의 세 글자인 abc와 뒤의 세 글자인 abc가 접두사=접미사 관계를 보여준다. 이러한 접두사-접미사 관계는 첫 글자부터 특정 글자까지 전부 찾아내야 한다.

    ABCDABD에 대해 다 찾아보면...

    i 부분문자열 접두사/접미사 pi[i]
    0 A A 1
    1 AB - 0
    2 ABC - 0
    3 ABCD - 0
    4 ABCDA A 1
    5 ABCDAB AB 2
    6 ABCDABD - 0

    pi는 인덱스 i인 문자열의 최대 접두사/접미사 길이이다.

    이 경우 pi는 [1, 0, 0, 0, 1, 2, 0]이다. 

    문제의 경우, 최초 3글자만 일치하므로 pi[2]를 체크.

    0 1 2 3 4 5 6 7 8 9
    A B C   A B C D A B
    A B C D            
    0 1 2 3 4 5 6 7 8 9
    A B C   A B C D A B
          A            
    4 5 6 7 8 9 10 11 12 13
    A B C D A B   A B C
    A B C D A B D      

    P[5]=2. 즉 8번째부터 시작한다.

    8 9 10 11 12 13 14 15 16 17
    A B   A B C D A B C
    A B C              
    8 9 10 11 12 13 14 15 16 17
    A B   A B C D A B C
      A B              
    8 9 10 11 12 13 14 15 16 17
    A B   A B C D A B C
        A              
    11 12 13 14 15 16 17 18 19 20
    A B C D A B C D A B
    A B C D A B D      
    15 16 17 18 19 20 21 - - -
    A B C D A B D - - -
    A B C D A B D - - -

    i=15, 즉 앞에서부터 16번째 글자부터 찾고 싶은 문자열이 나온다. 끝!

    알고리즘의 구현

    탐색을 시작하는 위치를 begin, 현재 일치하는 글자 수를 matched라 하자.

    그러면 begin이 글자 총 개수를 넘지 않는 동안만, 이하의 행동을 반복한다.

    • matched<m이고 begin+matched 위치에 있는 글자가 찾고자 하는 문자열의 matched번째 글자와 동일하면 matched를 1 증가시키고 matched가 m에 도달하면 찾는 데 성공했으므로 결과값을 추가한다.
    • 그 외의 경우, 첫판부터 맞지 않는 경우(matched=0) begin을 1칸 뒤로 민다. 그렇지 않다면, begin을 matched-pi[matched-1]로 바꾸고 matched를 pi[matched-1]로 바꾼다. 즉, 접미사 쪽부터 조사를 시작한다.

    pi 배열 찾기

    pi 배열을 빠르게 구하는 방법 역시 KMP 알고리즘과 동일하다. 

    단, 이 경우에는 begin<=n-m이 아닌, begin+matched가 m보다 작을 경우만 가능. 이 때는 접두사-접미사 관계를 만족시킬 때에만 matched를 증가시킨다.

    def getpartialmatch(N):
      m=len(N)
      pi=[0 for i in range(m)]
      begin=1
      matched=0
      while begin+matched<m:
        if N[begin+matched]==N[matched]:
          matched+=1
          pi[begin+matched-1]=matched
        else:
          if matched==0:
            begin+=1
          else:
            begin+=matched-pi[matched-1]
            matched=pi[matched-1]
      return pi
    
    def KMP(H, N):
      n=len(H)
      m=len(N)
      ret=[]
      pi=getpartialmatch(N)
      begin=0
      matched=0
      while begin<=n-m:
        if matched<m and H[begin+matched]==N[matched]:
          matched+=1
          if matched==m:
            ret.append(begin)
        else:
          if matched==0:
            begin+=1
          else:
            begin+=matched-pi[matched-1]
            matched=pi[matched-1]
      return ret
    
    T=str(input())
    P=str(input())
    x=KMP(T, P)
    print(len(x))
    for i in x:
      print(i+1, end=" ")
    

     

     

    댓글

Designed by Tistory.