용묻이
용스용스
용묻이
전체 방문자
오늘
어제
  • 분류 전체보기 (40)
    • 일상 (7)
      • 개발라이프 (1)
      • 대학라이프 (0)
      • 회사라이프 (1)
      • 군대라이프 (4)
    • 개발 (24)
      • 파이썬 (6)
      • 데이터 과학 (2)
      • 데이터 엔지니어링 (1)
      • 데이터 분석 (2)
      • 기타 (2)
      • BOJ (1)
      • Articles 번역 (1)
      • 논문 정리 (9)
    • 리뷰 (9)
      • IT 서적 (1)
      • 책 (4)
      • 영화 (1)
      • 드라마 (0)
      • 웹소설 (1)
      • 웹툰 (0)
      • 애니메이션 (2)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 신영복
  • 사이버펑크
  • 아우슈비츠
  • 파이썬
  • Method Resolution Order
  • 빅터 프랭클
  • MRO
  • 빅토르 프랑클
  • 동양사상
  • 객체지향
  • 로고테라피
  • 설득의 심리학
  • 홀로코스트
  • 먹먹함
  • 로버트 치알디니
  • 동양고전
  • 죽음의 수용소에서
  • OOP
  • 프로토콜
  • 엣지러너

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
용묻이

용스용스

[백준] 15829 Hashing
개발/BOJ

[백준] 15829 Hashing

2022. 8. 9. 19:07

해싱을 적용하는 문제로, 솔직히 쉬운 축에 속하는 문제이다. 이 문제 자체에서만큼은 자료형만 무작정 키워도 100점을 맞을 수 있고, 인터넷에 찾아보면 모듈러 연산의 성질로부터 정말 쉽게 푸는 방법들이 많다.

 

나도 처음엔 50점만 맞아서 뭐지 하고 온갖 방법으로 시도했었고, 그중에서 인터넷에 찾아봐도 딱히 나오지 않은 방법이 하나 있어서 포스팅해보려고 한다. 이 방법은 솔직히 말하면 쓸데없이 복잡하니 심심한 사람이 아니라면 다른 블로그에서 풀이법을 찾아보길 바란다.

 


hash값이 $(\sum_0^{L-1}{a_ir^i})\,\bmod\,M$인데, 문제에서 주어진 $L,\,r$값으로 무작정 계산하면 일반적인 int 자료형의 크기를 넘어선다. 더 큰 자료형을 써도 되겠지만, 만약 $L$이 매우 커진다면 제대로 작동하지 않을 것이다. 그렇기에 어떤 $L$값이 입력되어도 작동할 수 있는 알고리즘을 고안해야 한다.

 

먼저, small size에서만 작동하는 간단한 알고리즘을 생각해보자. $\{a_i\}$이 이미 주어져 있을 때, 아래와 같은 알고리즘이 가장 먼저 떠오를 것이다.

hashVal = 0
for i in 0...(L - 1)
	hashVal = hashVal + a[i] * r^i
    
hashVal = hashVal % M

이 알고리즘의 가장 큰 문제점은 3번째 줄에 있다. $L$값이 증가하여 어느 순간부터 overflow가 일어나게 된다. $r^i$값을 계산하는 시점에서 overflow가 발생하는 것이다. 그러니 우리는 $r^i$값을 계산하지 않고도 $(\sum{a_ir^i})\,\bmod\,M$값을 계산해야 한다.

 

쉽게 생각해볼 수 잇는 접근 중 하나는, 모든 loop를 지나며 합한 값에 모듈러 연산을 적용하는 기존의 방식이 아니라, 매 loop마다 적당히 작은 단위의 값이 계산되어 해당 값들을 더하면 최종 해시값이 도출되는 알고리즘이다. 특히 $r^i\,\bmod\,M$의 값으로 $r^{i+1}\,\bmod\,M$의 값을 계산할 수 있는 알고리즘이 반가운 상황이다.

 

다음의 수식을 생각해보자.

$$ a_ir^i=P_iM+(a_ir^i\,\bmod\,M) $$

 

$P_i$는 $a_ir^i$를 $M$으로 나눈 몫이 된다. 그 다음 항은 자연스럽게 나머지가 되고, 이를 $H_i=a_ir^i\,\bmod\,M$이라 하자.

 

여기서 다음의 수식을 증명해야 한다.

$$ (\sum(a_ir^i)\,\bmod\,M=(\sum{H_i})\,\bmod\,M $$

 

이는 다음과 같이 보일 수 있다.

$\sum{a_ir^i}=\sum(P_iM+H_i)=M\sum{P_i}+\sum{H_i}$인데, $(N\pm kM)\,\bmod\,M=N\,\bmod\,M$이므로(직관적이므로 증명하지 않는다), $(\sum{a_ir^i})\,\bmod\,M=(\sum{H_i})\,\bmod\,M$이다.

 

 

따라서 우리는 매 loop마다 $H_i$를 계산하여 적당히 작은 값들의 합으로 hash를 구할 수 있게 된다. $H_i$는 어떻게 구할 수 있을까? $H_i=a_ir^i-P_iM=a_ir^i-floor(a_ir^i/M) \times M$의 수식대로 계산하면 되는 걸까?

 

이미 앞서서 $r^i$을 계산하면 안됨을 얘기했다. 우리는 작은 단위의 계산에서 $H_i$값을 구해야 한다. 이를 위해 몇 가지 새로운 도구를 도입해야 한다. 첫째로 $x$의 소수부를 구하는 함수 $f(x)$이다.

 

$$ f(x) := x -floor(x) $$

 

이 $f(x)$의 성질 중 $a\,\bmod\,c=f(a/c)\times c$ 라는 것이 중요하다. 둘 째로, $r^i$의 $M$에 대한 나머지 $h_i$이다.

$$ h_i = r^i\,\bmod\,M=f(r^i/M)\times M $$

$H_i$랑 다름에 유의하자. $H_i$는 $a_ir^i\,\bmod\,M$이고, $h_i$는 $r^i\,\bmod\,M$이다.

 

이 두 가지 도구를 활용하면 $H_i$를 다르게 표현할 수 있다.

 

$$ H_{i+1} = a_{i+1}r^{i+1}\,\bmod\,M=f(a_{i+1}r^{i+1}/M)\times M = f(a_{i+1}r\cdot{r^i\over M})\times M $$

 

여기서 $f(x)$의 또 다른 성질을 이용한다.

 

$ x = \text{floor}(x) + f(x),\,y = \text{floor}(y) + f(y) $ 에서

$ xy =$ $\text{floor}(x)\cdot\text{floor}(y)$ $+$ $\text{floor}(x)\cdot f(y)$ $+$ $\text{floor}(y)\cdot f(x)$ $+$ $f(x)\cdot f(y) $ 이고

$ f(xy) = f(\text{floor}(x)f(y)+\text{floor}(y)f(x) + f(x)f(y)) $ 이다.

 

$x=a_{i+1}r,\,y=r^i/M$으로 두면, $f(a_{i+1}r\cdot{r^/M})=f(\text{floor}(a_{i+1}r)f(r^i/M)+f(a_{i+1}r)\text{floor}(r^i/M)+f(a_{i+1}r)f(r^i/M))$이고, $a_{i+1}r$이 정수이므로 $f(a_{i+1}r)=0,\,\text{floor}(a_{i+1}r)=a_{i+1}r$이다. 따라서

 

$$ f(a_{i+1}r\cdot{r^i\over M})=f(a_{i+1}r\cdot f({r^i\over M})) $$

 

위에서 확인했듯, $h_i=f(r^i/M)\times M\Leftrightarrow f(r^i/M)=h_i/M$이다.

 

최종적으로 $H_{i+1}=f(a_{i+1}r\cdot h_i/M)\times M$이다. 여기서도 아직 해결되지 않은 문제가 있는데, $h_i$를 계산해야한다는 것이다. $f(r^i/M)$으로 계산하기에는 $r^i$을 구해야 하므로 불가하다. $h_i$는 점화식의 형태로 풀어내야 한다.

 

$h_i=r^i\,\bmod\,M$이므로, $r^i=Q_iM+r^i\,\bmod\,M=Q_iM+h_i$로 표현할 수 있다. 여기에 $r$을 곱하면 아래와 같다.

 

$$ r\cdot r^i=r^{i+1}=rQ_iM+rh_i=Q_{i+1}M+rh_{i+1} $$

 

$rh_i>M$일 수 있으므로 $rh_i=h_{i+1}$이라 하기 힘들다. 다만 $h_{i+1}=r^{i+1}\,\bmod\,M$이므로

$$ r^{i+1}\,\bmod\,M=(rQ_iM+rh_i)\,\bmod\,M=rh_i\,\bmod\,M=h_{i+1} $$

이 된다.

 

위 식으로부터 우리는 $r^i$과 같은 큰 수를 다루지 않고서도 $h_i$를 계산할 수 있고, 이로부터

$ H_i=f(a_ir\cdot{h_{i-1}\over M})\times M $

$ \,\,=f(a_ir{rh_{i-1}\,\bmod\,M\over M})\times M $

$ \,\,=[a_ir(rh_{i-1}\,\bmod\,M)]\,\bmod\,M $

임을 알 수 있다.

 

이 식은 $a_i,\,r,\,h_i$ 등 작은 단위의 수로만 이루어진 계산이다.

이를 알고리즘으로 작성하면 아래와 같다.

 

h[0] = 1
hashVal = 0

for i in 1...(L-1)
	h[i] = (r * h[i - 1]) % M
    hashVal = hashVal + (a[i] * r * h[i - 1]) % M
    
hashVal = hashVal % M

 

위와 같은 로직으로 답을 구할 수 있다. $a_i$의 최댓값이 26이고, $h_i$의 최댓값이 $M-1$이다. 즉, $a_irh_{i-1}\le26\times31\times1234567890$인데, 이는 int의 범위를 벗어난다. 따라서 변수들을 long int 정도로만 설정해주면 된다.

 

또, 위 알고리즘에서 마지막 라인의 모듈러 계산 부분을 loop 안으로 넣어주면 hashVal도 overflow할 가능성을 없애므로 아무리 큰 $L$값이 입력되어도 해시값을 계산할 수 있다.


코드 결과물에 비해 설명이 장황했다. 서두에 설명했지만 이것보다 더 좋은 방법이 많다. 다만 이렇게 쓸데없이 수식을 떡칠하면서 풀어낼 수도 있다고 글을 써보고 싶었다..ㅋㅋ;

 

C++ 답안 코드는 아래와 같다.

#include <iostream>

int main() {
    long int r = 31;
    long int M = 1234567891;
    long int L;
    std::cin >> L;

    char *S = new char[L];
    int a;
    long int *h = new long int[L]; // r^i 에 대한 나머지
    long int *H = new long int[L]; // a_i*r^i에 대한 나머지

    std::cin >> S;

    a = static_cast<long int>(S[0]) - 96;
    h[0] = 1;
    H[0] = a;

    long int hashVal = a % M;
    long int pow = 1;
    for(int i = 1; i < L; i++) {
        pow *= 31;
        a = static_cast<int>(S[i]) - 96;
        h[i] = (r * h[i - 1]) % M;
        H[i] = (a * r * h[i - 1]) % M;
        hashVal += H[i];
    }
    hashVal %= M;
    std::cout << hashVal << std::endl;
}
    용묻이
    용묻이

    티스토리툴바