có được mục ngẫu nhiên có trọng số


51

Tôi có, ví dụ, bảng này

+ ----------------- +
| trái cây | trọng lượng |
+ ----------------- +
| táo | 4 |
| cam | 2 |
| chanh | 1 |
+ ----------------- +

Tôi cần phải trả lại một quả ngẫu nhiên. Nhưng táo nên được hái thường xuyên gấp 4 lần chanh và 2 lần thường xuyên như cam .

Trong trường hợp tổng quát hơn, nó nên được f(weight)thường xuyên.

Một thuật toán chung tốt để thực hiện hành vi này là gì?

Hoặc có thể có một số đá quý đã sẵn sàng trên Ruby? :)

PS
Tôi đã triển khai thuật toán hiện tại trong Ruby https://github.com/fl00r/pickup


11
đó phải là cùng một công thức để có được các chiến lợi phẩm ngẫu nhiên trong Diablo :-)
Jalayn

1
@Jalayn: Thật ra, ý tưởng cho giải pháp khoảng trong câu trả lời của tôi dưới đây xuất phát từ những gì tôi nhớ về các bảng chiến đấu trong World of Warcraft. :-D
Benjamin Kloster



Tôi đã thực hiện một số thuật toán ngẫu nhiên có trọng số đơn giản . Hãy cho tôi biết nếu bạn có thắc mắc.
Leonid Ganeline

Câu trả lời:


50

Giải pháp đơn giản nhất về mặt khái niệm sẽ là tạo một danh sách trong đó mỗi phần tử xảy ra nhiều lần so với trọng lượng của nó, vì vậy

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

Sau đó, sử dụng bất kỳ chức năng nào bạn có theo ý của bạn để chọn một yếu tố ngẫu nhiên từ danh sách đó (ví dụ: tạo một chỉ mục ngẫu nhiên trong phạm vi phù hợp). Điều này tất nhiên là không hiệu quả bộ nhớ và yêu cầu trọng số nguyên.


Một cách tiếp cận khác, phức tạp hơn một chút sẽ như thế này:

  1. Tính tổng các trọng số tích lũy:

    intervals = [4, 6, 7]

    Trong đó chỉ số dưới 4 đại diện cho một quả táo , 4 đến dưới 6 một quả cam và 6 đến dưới 7 quả chanh .

  2. Tạo một số ngẫu nhiên ntrong phạm vi 0đến sum(weights).

  3. Tìm mục cuối cùng có tổng tích lũy ở trên n. Quả tương ứng là kết quả của bạn.

Cách tiếp cận này đòi hỏi mã phức tạp hơn so với lần đầu tiên, nhưng ít bộ nhớ và tính toán hơn và hỗ trợ các trọng số dấu phẩy động.

Đối với một trong hai thuật toán, bước thiết lập có thể được thực hiện một lần cho một số lượng lựa chọn ngẫu nhiên tùy ý.


2
giải pháp khoảng có vẻ tốt
Jalayn

1
Đây là suy nghĩ đầu tiên của tôi :). Nhưng nếu tôi có bàn với 100 quả và cân nặng có thể vào khoảng 10k thì sao? Nó sẽ là mảng rất lớn và điều này sẽ không hiệu quả như tôi muốn. Đây là về giải pháp đầu tiên. Giải pháp thứ hai có vẻ tốt
fl00r

1
Tôi đã triển khai thuật toán này trong Ruby github.com/fl00r/pickup
fl00r

1
Phương pháp bí danh là cách không chính xác để xử lý điều này Tôi thực sự ngạc nhiên về số lượng bài viết lặp lại cùng một mã nhiều lần, tất cả trong khi bỏ qua phương thức bí danh . vì chúa vì bạn có được hiệu suất thời gian liên tục!
opa

30

Đây là một thuật toán (trong C #) có thể chọn phần tử có trọng số ngẫu nhiên từ bất kỳ chuỗi nào, chỉ lặp lại qua nó một lần:

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. 
}

Điều này dựa trên lý do sau: chúng ta hãy chọn phần tử đầu tiên của chuỗi là "kết quả hiện tại"; sau đó, trên mỗi lần lặp, hãy giữ nó hoặc loại bỏ và chọn phần tử mới làm hiện tại. Chúng ta có thể tính xác suất của bất kỳ phần tử đã cho nào được chọn cuối cùng là một sản phẩm của tất cả các xác suất mà nó sẽ không bị loại bỏ trong các bước tiếp theo, lần xác suất mà nó sẽ được chọn ở vị trí đầu tiên. Nếu bạn làm toán, bạn sẽ thấy rằng sản phẩm này đơn giản hóa (trọng số của phần tử) / (tổng của tất cả các trọng số), đó chính xác là những gì chúng ta cần!

Vì phương thức này chỉ lặp lại qua chuỗi đầu vào một lần, nên nó hoạt động ngay cả với các chuỗi lớn khó hiểu, với điều kiện là tổng trọng số phù hợp với một int(hoặc bạn có thể chọn một số loại lớn hơn cho bộ đếm này)


2
Tôi sẽ điểm chuẩn này trước khi cho rằng nó tốt hơn chỉ vì nó lặp lại một lần. Tạo nhiều giá trị ngẫu nhiên cũng không chính xác nhanh.
Jean-Bernard Pellerin

1
@ Jean-Bernard Pellerin Tôi đã làm, và nó thực sự nhanh hơn trong danh sách lớn. Trừ khi bạn sử dụng trình tạo ngẫu nhiên mạnh về mật mã (-8
Nevermind

Nên là câu trả lời imo. Tôi thích điều này hơn phương pháp "khoảng" và "lặp lại".
Vivin Paliath

2
Tôi chỉ muốn nói rằng tôi đã quay lại chủ đề này 3 hoặc 4 lần trong vài năm qua để sử dụng phương pháp này. Phương pháp này đã nhiều lần thành công trong việc cung cấp các câu trả lời tôi cần đủ nhanh cho mục đích của tôi. Tôi ước tôi có thể nâng cao câu trả lời này mỗi khi tôi quay lại sử dụng nó.
Jim Yarbro

1
Giải pháp tốt đẹp nếu bạn thực sự chỉ phải chọn một lần. Mặt khác, thực hiện công việc chuẩn bị cho giải pháp trong câu trả lời đầu tiên một lần sẽ hiệu quả hơn nhiều.
Ded repeatator

22

Đã có câu trả lời là tốt và tôi sẽ mở rộng ra một chút.

Như Benjamin đề nghị các khoản tiền tích lũy thường được sử dụng trong loại vấn đề này:

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

Để tìm một mục trong cấu trúc này, bạn có thể sử dụng một cái gì đó giống như đoạn mã của Nevermind. Đoạn mã C # này mà tôi thường sử dụng:

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

Bây giờ đến phần thú vị. Cách tiếp cận này hiệu quả như thế nào và giải pháp hiệu quả nhất là gì? Đoạn mã của tôi yêu cầu bộ nhớ O (n) và chạy trong thời gian O (n) . Tôi không nghĩ rằng nó có thể được thực hiện với ít hơn không gian O (n) nhưng độ phức tạp thời gian có thể thấp hơn nhiều, thực tế là O (log n) . Bí quyết là sử dụng tìm kiếm nhị phân thay vì vòng lặp thông thường.

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];
}

Ngoài ra còn có một câu chuyện về việc cập nhật trọng lượng. Trong trường hợp xấu nhất, việc cập nhật trọng số cho một yếu tố gây ra việc cập nhật các khoản tiền tích lũy cho tất cả các yếu tố làm tăng độ phức tạp của cập nhật lên O (n) . Điều đó cũng có thể được cắt xuống O (log n) bằng cách sử dụng cây được lập chỉ mục nhị phân .


Điểm hay về tìm kiếm nhị phân
fl00r

Câu trả lời của Nevermind không cần thêm dung lượng, vì vậy, đó là O (1), nhưng thêm độ phức tạp thời gian chạy bằng cách liên tục tạo số ngẫu nhiên và đánh giá hàm trọng số (tùy thuộc vào vấn đề tiềm ẩn, có thể tốn kém).
Benjamin Kloster

1
Những gì bạn tuyên bố là "phiên bản dễ đọc hơn" của mã của tôi thực sự là không. Mã của bạn cần biết tổng số trọng số và tổng tiền tích lũy trước; của tôi không.
Không bao giờ

@Benjamin Kloster Mã của tôi chỉ gọi hàm trọng số một lần cho mỗi phần tử - bạn không thể làm gì tốt hơn thế. Bạn đúng về số ngẫu nhiên, mặc dù.
Không bao giờ

@Nevermind: Bạn chỉ gọi nó một lần cho mỗi cuộc gọi đến chức năng chọn, vì vậy nếu người dùng gọi nó hai lần, hàm trọng số sẽ được gọi lại cho mỗi phần tử. Tất nhiên bạn có thể lưu trữ nó, nhưng sau đó bạn không còn là O (1) vì sự phức tạp của không gian nữa.
Benjamin Kloster

8

Đây là một triển khai Python đơn giản:

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)

Trong thuật toán di truyền, quy trình chọn này được gọi là lựa chọn tỷ lệ thể hình hoặc Lựa chọn bánh xe Roulette kể từ:

  • một tỷ lệ của bánh xe được gán cho mỗi lựa chọn có thể dựa trên giá trị trọng lượng của chúng. Điều này có thể đạt được bằng cách chia trọng số của một lựa chọn cho tổng trọng số của tất cả các lựa chọn, từ đó bình thường hóa chúng thành 1.
  • sau đó một lựa chọn ngẫu nhiên được thực hiện tương tự như cách bánh xe roulette được xoay.

Lựa chọn bánh xe Roulette

Các thuật toán điển hình có độ phức tạp O (N) hoặc O (log N) nhưng bạn cũng có thể thực hiện O (1) (ví dụ: lựa chọn bánh xe Roulette thông qua chấp nhận ngẫu nhiên ).


Bạn có biết nguồn gốc của hình ảnh này là gì không? Tôi muốn sử dụng nó cho một bài báo nhưng cần phải chắc chắn về sự quy kết.
Malcolm MacLeod

@MalcolmMacLeod Xin lỗi, nó được sử dụng trong rất nhiều bài viết / trang web GA nhưng tôi không biết ai là tác giả.
manlio

0

Ý chính này đang làm chính xác những gì bạn đang yêu cầu.

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;
    }

bạn có thể sử dụng nó như thế:

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

Đoạn mã trên rất có thể sẽ (% 98) trả về 0, là chỉ mục cho 'apple' cho mảng đã cho.

Ngoài ra, mã này kiểm tra phương thức được cung cấp ở trên:

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);

Nó cung cấp một đầu ra giống như thế:

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

2
Lập trình viên là về câu hỏi khái niệm và câu trả lời dự kiến ​​sẽ giải thích mọi thứ. Ném các đoạn mã thay vì giải thích giống như sao chép mã từ IDE sang bảng trắng: nó có thể trông quen thuộc và thậm chí đôi khi có thể hiểu được, nhưng nó cảm thấy kỳ lạ ... chỉ là lạ. Bảng trắng không có trình biên dịch
gnat

Bạn nói đúng, tôi đã tập trung vào mã nên tôi quên nói nó hoạt động như thế nào. Tôi sẽ thêm một lời giải thích về cách nó hoạt động.
Ramazan Polat
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.