어떤 array에서 random한 i번째 element를 뽑고 싶다고 하자. 일반적으로 전체 배열의 size를 알아내고 rand () % size를 한 후 해당 index를 가져오는 방식을 택할 것이다. 그러나 모종의 이유로 전체 배열 size를 알 수 없는 상황에서도 무작위 추출을 할 수 있다. 예를 들어 array가 아니라 singly linked list여서 끝을 모르고, 단 1번 순회해야 한다는 조건이라면? 1번의 순회로 size를 알아내도 index에 접근하는 데 O(n)이 걸리기 때문에 해결할 수 없다. 이런 상황에서 Reservoir sampling을 사용한다.
Reservoir Sampling
정의
Reservoir sampling은 어떤 배열을 1번에 1개의 element만 보며 순회할 때 무작위 추출할 수 있는 알고리즘이다.
오직 1개의 값만 저장할 수 있는 공간을 만든다. 이 공간에 i번째 element는 1i1i의 확률로 가져오고, i−1i의 확률로 이전 element를 유지한다. 이 간단한 방법으로 단 1번 순회하면서 무작위 추출을 할 수 있다.
예를 들어...
1번째 순회에는 1번째 element를 1의 확률로 가져온다. 1번째 element를 가져오지 않을 확률은 없다.
2번째 순회에는 2번째 element를 1/2의 확률로 가져오고, 1/2의 확률로 원래 있던 element를 유지한다.(1번째 element)
3번째 순회에는 3번째 element를 1/3의 확률로 가져오고, 2/3의 확률로 원래 있던 element를 유지한다.(2번째 순회에서 살아남은 element)
...
n번째 순회에는 n번째 element를 1/n의 확률로 가져오고, n−1n의 확률로 원래 있던 element를 유지한다.(n-1번째 순회에서 살아남은 element)
pseudo code : 1번의 순회, O(n)
pseudo code는 아래와 같다. singly linked list를 탐색한다는 조건이다.
// input : head vertex of singly linked list // node has value element and next element // node->value : value of each node // node->next : next pointer of each node procedure Reservoir(head) count = 1 curNode = head // 순회에 사용할 vertex curValue // 리턴값 while curNode != NULL do if rand() % count == 0 then // k번째 vertex는 1/k의 확률로 남김 curValue = curNode->value end if curNode = curNode->next count++ end while return curValue
처음부터 끝까지 단 1번 순회하기 때문에 time complexity는 O(n)이며, 별다른 추가 공간을 사용하지 않기 때문에 space complex는 O(1)이다.
증명
n번째 element가 선택될 확률은 n번째 순회에서 n번째 element가 선택될 확률과 같으므로 1/n이다.
n-1번째 element가 선택될 확률은
- n-1번째 순회에서 n-1번째 element가 선택될 확률 1n−1
- n번째 순회에서 n번째 element가 선택되지 않을 확률 n−1n
- 이 둘의 곱이므로 1n이다.
n-2번째가 선택될 확률은
- n-2번째 순회에서 n-2번째 element가 선택될 확률 1n−2
- n-1번째 순회에서 n-1번째 element가 선택되지 않을 확률 n−2n−1
- n번째 순회에서 n번째 element가 선택되지 않을 확률 n−1n
- 이 셋의 곱이므로 1n이다.
...
1번째가 선택될 확률은
- 1번째 순회에서 1번째 element가 선택될 확률 1
- 2번째 순회에서 2번째 element가 선택되지 않을 확률 1/2
- 3번째 순회에서 3번째 element가 선택되지 않을 확률 2/3
- ...
- n-1번째 순회에서 n-1번째 element가 선택될 확률 n−2n−1
- n번째 순회에서 n번째 element가 선택되지 않을 확률 n−1n
- 이들의 곱이므로 1n이다.
수식으로 증명한다면
어떤 수열 A=A1,A2,...,An이 있다고 하자. i번째 sampling 결과를 Si라고 하자.
objective : Reservoir sampling을 따랐을 때 A1,A2,...,An이 뽑힐 확률이 모두 1n이다.
Reservoir sampling을 따르면 i번째 sampling 결과는
Si={Ai with p = 1/i Si−1 otherwise
이다. 이 때 1<=j<=n인 어떤 j에 대해 Aj=Sn일 확률을 구하면 된다.
P[Aj=Sn]=P[Sn=Sn−1=Sn−2=...=Sj+1=Sj=Aj]=P[(Sn=Sn−1) ∩ (Sn−1=Sn−2) ∩ ... ∩ (Sj+1=Sj) ∩ (Sj=Aj)]=P[(Sn=Sn−1)] × P[(Sn−1=Sn−2)] × ... × P[(Sj+1=Sj)] × P[(Sj=Aj)]=n−1n × n−2n × ... × jj+1 × 1j=1n
1->2 : Aj가 Sn이 Sn=Sn−1이어야 하고, Sn−1=Sn−2, ... , Sj+1=Sj여야 하며 Sj=Aj여야 한다.
2->3 : 각 확률의 intersect로 표현되기 때문에 바뀐다.
3->4 : 각 확률을 나타낸다.
정리하면 1n이 된다.
즉, 1<=j<=n인 어떤 j에 대해 Aj=Sn가 성립한다. 따라서 array의 모든 index가 뽑힐 확률이 1n이다.
Q.E.D.
'PS > Algorithm' 카테고리의 다른 글
Two Pointer, Sliding Window, Meet in the Middle ***TODO (0) | 2022.08.22 |
---|---|
누적 합 Prefix Sum (0) | 2022.07.10 |
동적 계획법 Dynamic Programming (+ LIS) (0) | 2022.07.09 |
좌표 압축 (0) | 2022.06.28 |
정렬 ***TODO (0) | 2022.06.27 |