รับรายการแบบสุ่มถ่วงน้ำหนัก


51

ยกตัวอย่างเช่นฉันมีตารางนี้

+ ----------------- +
| ผลไม้ | น้ำหนัก
+ ----------------- +
| แอปเปิ้ล 4 |
| ส้ม 2 |
| มะนาว 1 |
+ ----------------- +

ฉันต้องการคืนผลไม้แบบสุ่ม แต่แอปเปิ้ลควรจะเลือก 4 ครั้งบ่อยเท่ามะนาวและ 2 ครั้งบ่อยเท่าส้ม

ในกรณีทั่วไปมากขึ้นควรเป็นf(weight)บ่อยครั้ง

อัลกอริทึมทั่วไปที่ดีในการใช้พฤติกรรมนี้คืออะไร

หรืออาจจะมีอัญมณีบางอย่างที่พร้อมใน Ruby? :)

PS
ฉันใช้อัลกอริทึมปัจจุบันใน Ruby https://github.com/fl00r/pickup


11
ที่ควรจะเป็นสูตรเดียวกันสำหรับการได้รับของขวัญแบบสุ่มใน Diablo :-)
Jalayn

1
@ Jalayn: ที่จริงแล้วความคิดสำหรับการแก้ปัญหาช่วงเวลาในคำตอบของฉันด้านล่างมาจากสิ่งที่ฉันจำเกี่ยวกับตารางการต่อสู้ใน World of Warcraft :-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
วิธีนามแฝงเป็นวิธีที่ defacto การจัดการนี้ฉันสุจริตประหลาดใจที่จำนวนของโพสต์ที่ทำซ้ำรหัสเดียวกันซ้ำแล้วซ้ำอีกทั้งหมดในขณะที่ไม่สนใจวิธีการนามแฝง เพื่อประโยชน์ของพระเจ้าคุณจะได้รับการแสดงเวลาคงที่!
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
ไม่เป็นไร

ควรเป็นคำตอบที่ได้รับการยอมรับ ฉันชอบสิ่งนี้ดีกว่าวิธี "ช่วงเวลา" และ "รายการซ้ำ"
Vivin Paliath

2
ฉันแค่อยากจะบอกว่าฉันกลับมาที่หัวข้อนี้ 3 หรือ 4 ครั้งในสองสามปีที่ผ่านมาเพื่อใช้วิธีนี้ วิธีนี้ประสบความสำเร็จในการให้คำตอบที่ฉันต้องการอย่างรวดเร็วเพียงพอสำหรับวัตถุประสงค์ของฉัน ฉันหวังว่าฉันจะสามารถเอาชนะคำตอบนี้ทุกครั้งที่ฉันกลับมาใช้มัน
Jim Yarbro

1
วิธีแก้ปัญหาที่ดีถ้าคุณต้องเลือกเพียงครั้งเดียว มิฉะนั้นการทำงานเตรียมความพร้อมสำหรับการแก้ปัญหาในคำตอบแรกเมื่อมีประสิทธิภาพมากขึ้น
Deduplicator

22

ปัจจุบันคำตอบนั้นดีและฉันจะขยายออกไปเล็กน้อย

ดังที่เบนจามินแนะนำจำนวนเงินสะสมที่ใช้โดยทั่วไปในปัญหาเช่นนี้:

+------------------------+
| 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 (log n)ในความเป็นจริง เคล็ดลับคือการใช้การค้นหาแบบไบนารีแทนการวนซ้ำแบบปกติ

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
สิ่งที่คุณอ้างว่าเป็น "รุ่นที่อ่านได้มากกว่า" ของรหัสของฉันนั้นไม่ใช่ รหัสของคุณจำเป็นต้องทราบน้ำหนักรวมและผลรวมสะสมล่วงหน้า ของฉันไม่ได้
ไม่เป็นไร

@Benjamin Kloster รหัสของฉันเรียกฟังก์ชันน้ำหนักเพียงครั้งเดียวต่อองค์ประกอบ - คุณไม่สามารถทำได้ดีกว่านี้ แม้ว่าคุณจะถูกเกี่ยวกับตัวเลขสุ่ม
ไม่เป็นไร

@Nevermind: คุณเรียกเพียงครั้งเดียวต่อการเรียกไปยังฟังก์ชั่นเลือกดังนั้นหากผู้ใช้เรียกมันสองครั้งฟังก์ชั่นน้ำหนักจะถูกเรียกอีกครั้งสำหรับแต่ละองค์ประกอบ แน่นอนว่าคุณสามารถแคชได้ แต่คุณไม่ใช่ O (1) สำหรับความซับซ้อนของพื้นที่อีกต่อไป
Benjamin Kloster

8

นี่เป็นการใช้งาน Python อย่างง่าย:

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) ส่งคืน 0 ซึ่งเป็นดัชนีสำหรับ 'apple' สำหรับอาร์เรย์ที่กำหนด

นอกจากนี้โค้ดนี้ทดสอบวิธีการที่ให้ไว้ข้างต้น:

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.