가중 임의 항목 가져 오기


51

예를 들어이 테이블이 있습니다

+ ----------------- +
| 과일 | 무게 |
+ ----------------- +
| 사과 | 4 |
| 오렌지 | 2 |
| 레몬 | 1 |
+ ----------------- +

나는 임의의 과일을 반환해야합니다. 그러나 사과 는 레몬 보다 4 배 , 오렌지 보다 2 배 자주 골라야 합니다.

보다 일반적인 경우에는 f(weight)시간이 자주 있어야합니다 .

이 동작을 구현하는 데 유용한 일반적인 알고리즘은 무엇입니까?

아니면 루비에 준비된 보석이 있습니까? :)

추신 :
나는 루비에서 현재 알고리즘을 구현했습니다 https://github.com/fl00r/pickup


11
그것은 디아블로에서 무작위 전리품을 얻는 것과 같은 공식이어야합니다 :-)
— Jalayn

1
@Jalayn : 사실, 아래 답변에서 인터벌 솔루션에 대한 아이디어는 월드 오브 워크래프트의 전투 테이블에 대한 기억에서 비롯됩니다. :-D
— Benjamin Kloster



몇 가지 간단한 가중 랜덤 알고리즘을 구현 했습니다 . 궁금한 점이 있으면 알려주세요.
— Leonid Ganeline

답변:


50

개념적으로 가장 간단한 해결책은 각 요소가 가중치의 몇 배나 발생하는지 목록을 작성하는 것입니다.

fruits = [apple, apple, apple, apple, orange, orange, lemon]

그런 다음 원하는 기능을 사용하여 해당 목록에서 임의의 요소를 선택하십시오 (예 : 적절한 범위 내에서 임의의 인덱스 생성). 이것은 물론 메모리 효율성이 높지 않고 정수 가중치가 필요합니다.


약간 더 복잡한 또 다른 접근법은 다음과 같습니다.

  1. 누적 가중치 합계를 계산하십시오.

    intervals = [4, 6, 7]

    4 이하의 지수는 사과를 나타내며 , 4 ~ 6은 오렌지 , 6 ~ 7은 레몬을 나타 냅니다.

  2. ~ n범위의 난수를 생성하십시오 .0sum(weights)

  3. 누적 합계가 위에있는 마지막 항목을 찾으십시오 n. 해당 과일이 결과입니다.

이 방법은 첫 번째보다 더 복잡한 코드가 필요하지만 메모리와 계산이 적고 부동 소수점 가중치를 지원합니다.

어느 알고리즘이든, 임의의 수의 무작위 선택을 위해 설정 단계를 한 번 수행 할 수 있습니다.


2
간격 솔루션은 좋은 것 같습니다
— Jalayn

1
이것은 나의 첫번째 생각이었다 :). 그러나 100 과일과 무게를 가진 테이블을 가지고 있다면 무게는 약 10k가 될 수 있습니까? 그것은 매우 큰 배열 일 것이고 이것은 내가 원하는만큼 효율적이지 않을 것입니다. 이것은 첫 번째 해결책에 관한 것입니다. 두번째 해결책은 좋아 보인다
— fl00r

1
이 알고리즘을 Ruby에서 구현했습니다. github.com/fl00r/pickup
— fl00r

1
alias 메소드는 이것을 처리하는 사실상의 방법 입니다. alias 메소드 를 무시 하면서 동일한 코드를 반복해서 반복하는 게시물 수에 정직하게 놀랐습니다 . 신을 위해 당신은 일정한 시간 성능을 얻을!
— opa

30

다음은 시퀀스에서 임의의 가중치 요소를 선택하고 한 번만 반복하는 알고리즘 (C #)입니다.

public static T Random<T>(this IEnumerable<T> enumerable, Func<T, int> weightFunc)
{
    int totalWeight = 0; // this stores sum of weights of all elements before current
    T selected = default(T); // currently selected element
    foreach (var data in enumerable)
    {
        int weight = weightFunc(data); // weight of current element
        int r = Random.Next(totalWeight + weight); // random value
        if (r >= totalWeight) // probability of this is weight/(totalWeight+weight)
            selected = data; // it is the probability of discarding last selected element and selecting current one instead
        totalWeight += weight; // increase weight sum
    }

    return selected; // when iterations end, selected is some element of sequence. 
}

이것은 다음과 같은 추론에 근거한다 : 시퀀스의 첫 번째 요소를 "현재 결과"로 선택하자; 그런 다음 각 반복에서 유지하거나 삭제하고 새 요소를 현재로 선택하십시오. 우리는 주어진 요소가 마지막 단계에서 버려 지지 않을 모든 확률의 곱으로 처음에 선택 될 확률의 곱으로 계산할 수 있습니다. 수학을 수행하면이 제품이 (요소의 무게) / (모든 무게의 합계)로 단순화되는 것을 알 수 있습니다.

이 방법은 입력 시퀀스를 한 번만 반복하므로 가중치의 합이 int(또는이 카운터에 대해 더 큰 유형을 선택할 수 있는 경우) 외설적으로 큰 시퀀스에서도 작동 합니다.


2
한 번 반복하기 때문에 더 좋다고 가정하기 전에 이것을 벤치 마크 할 것입니다. 많은 임의의 값을 생성하는 것도 빠르지는 않습니다.
— Jean-Bernard Pellerin

1
@ Jean-Bernard Pellerin 내가 했었고 실제로 큰 목록에서 더 빠릅니다. 암호로 강력한 랜덤 생성기를 사용하지 않는 한 (-8
— Nevermind

허용되는 답변 imo 여야합니다. 나는 이것을 "간격"과 "반복 된 엔트리"접근 방식보다 더 좋아한다.
— Vivin Paliath

2
이 방법을 사용하기 위해 지난 몇 년 동안이 스레드로 3 ~ 4 번 돌아 왔다고 말하고 싶었습니다. 이 방법은 반복적으로 목적에 필요한 답변을 충분히 제공하는 데 성공했습니다. 다시 사용할 때마다이 답변을 공표 할 수 있기를 바랍니다.
— Jim Yarbro 8:15에

1
정말로 한 번만 선택하면 좋은 솔루션입니다. 그렇지 않으면 첫 번째 답변에서 솔루션 준비 작업을 한 번 수행하는 것이 훨씬 더 효율적입니다.
— 중복 제거기

22

이미 제시된 답변이 좋으며 조금 더 확대하겠습니다.

Benjamin이 제안한 것처럼 누적 합계는 일반적으로 이러한 종류의 문제에 사용됩니다.

+------------------------+
| fruit  | weight | csum |
+------------------------+
| apple  |   4    |   4  |
| orange |   2    |   6  |
| lemon  |   1    |   7  |
+------------------------+

이 구조에서 항목을 찾으려면 Nevermind의 코드 조각과 같은 것을 사용할 수 있습니다. 내가 일반적으로 사용하는이 C # 코드는 다음과 같습니다.

double r = Random.Next() * totalSum;
for(int i = 0; i < fruit.Count; i++)
{
    if (csum[i] > r)
        return fruit[i];
}

이제 흥미로운 부분입니다. 이 방법은 얼마나 효율적이며 가장 효율적인 솔루션은 무엇입니까? 내 코드는 O (n) 메모리 가 필요 하고 O (n) 시간에 실행됩니다 . 나는보다 함께 할 수 있다고 생각하지 않습니다 O (n)이 훨씬 낮을 수 공간이 있지만, 시간 복잡도 O는 (로그 n) 사실. 트릭은 일반 for 루프 대신 이진 검색을 사용하는 것입니다.

double r = Random.Next() * totalSum;
int lowGuess = 0;
int highGuess = fruit.Count - 1;

while (highGuess >= lowGuess)
{
    int guess = (lowGuess + highGuess) / 2;
    if ( csum[guess] < r)
        lowGuess = guess + 1;
    else if ( csum[guess] - weight[guess] > r)
        highGuess = guess - 1;
    else
        return fruit[guess];
}

가중치 업데이트에 대한 이야기도 있습니다. 최악의 경우 한 요소에 대한 가중치를 업데이트하면 모든 요소에 대한 누적 합계가 업데이트되어 업데이트 복잡성이 O (n)으로 증가합니다 . 이진 인덱스 트리를 사용하여 O (log n) 으로 줄일 수도 있습니다 .


이진 검색에 대한 좋은 지적
— fl00r

Nevermind의 대답에는 추가 공간이 필요하지 않으므로 O (1)이지만 난수를 반복적으로 생성하고 가중치 함수 (기본 문제에 따라 비용이 많이들 수 있음)를 평가하여 런타임 복잡성을 추가합니다.
— Benjamin Kloster

1
내 코드의 "더 읽기 쉬운 버전"이라고 주장하는 것은 실제로 아닙니다. 코드는 사전에 총 가중치와 누적 합계를 알아야합니다. 내하지 않습니다.
— Nevermind

@ Benjamin Kloster 내 코드는 요소 당 한 번만 weight 함수를 호출합니다. 그보다 더 좋은 것은 없습니다. 그래도 난수에 대해서는 맞습니다.
— Nevermind

@Nevermind : pick 함수에 대한 호출마다 한 번만 호출하므로 사용자가 두 번 호출하면 각 요소에 대해 weight 함수가 다시 호출됩니다. 물론 캐시 할 수는 있지만 더 이상 공간 복잡성을 위해 O (1)이 아닙니다.
— Benjamin Kloster

8

이것은 간단한 파이썬 구현입니다.

from random import random

def select(container, weights):
    total_weight = float(sum(weights))
    rel_weight = [w / total_weight for w in weights]

    # Probability for each element
    probs = [sum(rel_weight[:i + 1]) for i in range(len(rel_weight))]

    slot = random()
    for (i, element) in enumerate(container):
        if slot <= probs[i]:
            break

    return element

과

population = ['apple','orange','lemon']
weights = [4, 2, 1]

print select(population, weights)

유전자 알고리즘에서이 선택 절차는 다음 과 같은 이유로 피트니스 비례 선택 또는 룰렛 휠 선택 이라고합니다 .

  • 휠의 비율은 무게 값에 따라 가능한 각 선택에 할당됩니다. 이것은 선택의 가중치를 모든 선택의 전체 가중치로 나누어서 1로 정규화함으로써 달성 될 수 있습니다.
  • 그런 다음 룰렛 휠이 회전하는 방식과 유사하게 무작위로 선택됩니다.

룰렛 휠 선택

일반적인 알고리즘에는 O (N) 또는 O (log N) 복잡성이 있지만 O (1)도 수행 할 수 있습니다 (예 : 확률 적 수락을 통한 룰렛 휠 선택 ).


이 이미지의 원본이 무엇인지 알고 있습니까? 종이로 사용하고 싶지만 속성을 확인해야합니다.
— Malcolm MacLeod

@MalcolmMacLeod 죄송합니다. 많은 GA 논문 / 사이트에서 사용되고 있지만 누가 저자인지 모르겠습니다.
— manlio

0

이 요지 는 정확히 당신이 요구하는 것을하고 있습니다.

public static Random random = new Random(DateTime.Now.Millisecond);
public int chooseWithChance(params int[] args)
    {
        /*
         * This method takes number of chances and randomly chooses
         * one of them considering their chance to be choosen.    
         * e.g. 
         *   chooseWithChance(0,99) will most probably (%99) return 1
         *   chooseWithChance(99,1) will most probably (%99) return 0
         *   chooseWithChance(0,100) will always return 1.
         *   chooseWithChance(100,0) will always return 0.
         *   chooseWithChance(67,0) will always return 0.
         */
        int argCount = args.Length;
        int sumOfChances = 0;

        for (int i = 0; i < argCount; i++) {
            sumOfChances += args[i];
        }

        double randomDouble = random.NextDouble() * sumOfChances;

        while (sumOfChances > randomDouble)
        {
            sumOfChances -= args[argCount -1];
            argCount--;
        }

        return argCount-1;
    }

당신은 그것을 다음과 같이 사용할 수 있습니다 :

string[] fruits = new string[] { "apple", "orange", "lemon" };
int choosenOne = chooseWithChance(98,1,1);
Console.WriteLine(fruits[choosenOne]);

위의 코드는 아마도 (% 98) 주어진 배열의 'apple'에 대한 인덱스 인 0을 반환합니다.

또한이 코드는 위에 제공된 방법을 테스트합니다.

Console.WriteLine("Start...");
int flipCount = 100;
int headCount = 0;
int tailsCount = 0;

for (int i=0; i< flipCount; i++) {
    if (chooseWithChance(50,50) == 0)
        headCount++;
    else
        tailsCount++;
}

Console.WriteLine("Head count:"+ headCount);
Console.WriteLine("Tails count:"+ tailsCount);

다음과 같은 출력을 제공합니다.

Start...
Head count:52
Tails count:48

2
프로그래머는 대한 개념 질문과 답변이 일을 설명 할 것으로 예상된다. 설명 대신 코드 덤프를 던지는 것은 IDE에서 화이트 보드로 코드를 복사하는 것과 같습니다. 친숙해 보이고 때로는 이해할 수 있지만 이상하게 느껴집니다. 화이트 보드에는 컴파일러가 없습니다
— gnat

당신 말이 맞아요, 나는 코드에 집중했기 때문에 그것이 어떻게 작동하는지 말하는 것을 잊었습니다. 작동 방식에 대한 설명을 추가하겠습니다.
— Ramazan Polat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.