알고리즘/Math

random (1)

Algorithmus 2022. 3. 29. 03:34

랜덤(random)한 수를 다루는 문제는 수학적 코딩 문제의 대표적 유형으로서 자주 출제되나 이 문제를 해결하려면 결코 녹록지가 않다. 이산수학, 조합수학, 추상대수 등의 개념을 담고 있기 때문이다. 하지만 그런 어려운 수학을 전부 공부하지 않더라도 직관과 상식으로 이를 풀어낼 수 있고, 대표적 문제들을 풀다보면 변형된 형태에도 대응할 수 있을 것이라 믿는다. 이 번 포스트에서는 몇 가지 연관된 문제를 풀어본다. EPI (Elements of Programming Interviews in Python)과 LeetCode 등을 참고했다.

부분집합 1

문제: 서로 다른 숫자로 된 배열(원소 n개) A에서 정해진 크기(k)만큼의 원소를 갖는 부분집합 1개를 찾기. 각 부분집합이 나타날 확률은 동일해야 한다. 원래 주어진 집합의 앞부터 k개만큼을 답이 될 원소로 채울 것(in-place).

해설: 크기가 k인 부분집합을 모두 뽑아 한 개를 랜덤으로 뽑으면 될까. 우선 이 방법 자체가 보기보다 상당히 어려운 문제이고 시간도 많이 걸린다. 나중에 따로 다루겠다.

그러면 이 문제를 부분으로 쪼개보자. 우선 원소가 k개인 부분집합을 구하려면 (말장난같지만) k-1개인 부분집합을 구한 다음에 1개를 랜덤으로 골라 추가하면 된다. 그렇다면 이 과정은 base case인 1개짜리 부분집합으로부터 목표한 k 만큼의 원소 수가 될 때 까지 작업하면 된다.

구체적으로, k = 1일 때는 RNG (random number generator) 를 불러와서, 필요한 경우 mod n을 하여 원소 n개 짜리 배열(A라고 하자) 중에 한 개 원소를 고른다. 이것을 r 번째였다고 하자 (0 <= r < n). 그러면 우리는 in-place로 돌려줘야 하니까,

swap(A[0], A[r])

을 해야 한다. 그리고 이미 k 개 중에서 0번째는 해결했으니, 다음 자리에 채울 것을 배열의 나머지 원소인 A[1, n-1] 중에 뽑는다. 위에서 이미 swap을 했기 때문에 원래 A[0] 자리에 있던 것은 A[r] 자리에 가 버렸다. 우리가 지금 신경쓸 것은 그저 A의 index 1부터 n-1까지 중에서 하나를 뽑아 A[1] 자리로 보낸다는 것이다 (혹시 헷갈릴 수 있어 언급하면 A[1] 자리에 있던 것이 뽑혀서 그대로 A[1] 자리로 가는 경우도 있다. 실제 스왑은 일어나지 않겠지만 이 경우를 따로 고려할 필요없이 위 로직에 포함된다).

def random_sample(k, A):
    for i in range(k):   # 0:k-1, thus do k times
        # we pick random numbers among 0:n-1, 1:n-1, ... , k-1:n-1, thus i:n-1
        r = random.randint(i, len(A) - 1)   # inclusive
        A[i], A[r] = A[r], A[i]             # swap

random 함수는 알아두면 편하게 쓸 수 있다. 이 외에도 다음 함수를 참고할 만 하다.

import random

random.randrange(15)   # same as range [0, 15)
random.randint(3,15)   # inclusive [3, 15]
random.random()        # don't need to mention argument, as it returns between (0, 1)
random.shuffle(A)      # shuffle list A randomly
random.choice(A)       # pick one of elements in A randomly

순열

문제: 랜덤한 순열을 만들어라.

해설: 결론부터 말하면 위의 함수를 활용할 수 있다. 즉, n개 원소 중에서 n개를 포함한 subset을 만들때, 그 subset의 각 원소들을 매번 랜덤하게 뽑기 때문에 이것은 랜덤한 순열이라고 할 수 있다. 순열이란 정해진 숫자들을 다른 순서로 나열한 것에 다름 아니기 때문이다.

def random_perm(n):
    perm = list(range(n))
    random_sample(n, perm)
    return perm

부분집합 2

문제: 이번에는 부분집합 크기 k와 함께, 패킷을 계속 입력받아 역시 일양 랜덤 부분집합(uniform random subset)을 유지하는 것이 문제이다.

해설: 예를 들어, k = 2 라고 하고, 패킷이 pqrtuv 순서로 읽힌다면, 일단 앞의 두개를 부분집합에 포함한다: {p, q}. 그 다음 패킷을 선택하려면 총 입수한 패킷의 수인 3개 중에 2개를 선택해야 하므로 2/3의 확률로 p, q, r 중에 2개를 선택한다. 만약에 r이 선택되었다면, 이제 총 부분집합 원소 수를 k = 2개로 유지해야 하므로, 기존의 {p, q} 중에 하나를 선택해 바꿔야 하며, 이 확률은 늘 1/2이 된다.

def online_random_sample(it, k):
    sample = list(itertools.islice(it, k))    # read k iterator
    read = k       # how many iterators have I read from stream
    for p in it:   # read one more packet from iterator
    	read += 1  # augment counter of how many I read
        replace_id = random.randrange(read)   # 0:read-1
        if replace_id < k:   # if randomly selected id to be replaced were within previous subset
            sample[replace_id] = p  # replace it with newly read packet
    return sample  # kept doin' that for whole iterator stream, now ready to return.

 

반응형

'알고리즘 > Math' 카테고리의 다른 글

소수 (솟수)  (0) 2023.08.06
n의 약수를 오름차순으로 정렬할 때 k번째 원소를 구하라  (0) 2023.06.30