আমি কীভাবে একটি ভারী সংগ্রহ তৈরি করব এবং এর থেকে এলোমেলো উপাদান বেছে নেব?


34

আমার একটি লুট বাক্স রয়েছে যা আমি একটি এলোমেলো আইটেমটি পূরণ করতে চাই। তবে আমি চাই প্রতিটি আইটেমের বাছাইয়ের আলাদা সুযোগ থাকুক। উদাহরণ স্বরূপ:

  • 10 সোনার 5% সম্ভাবনা
  • তরোয়াল 20% সুযোগ
  • 45% ঝাল সম্ভাবনা
  • 20% বর্ম সম্ভাবনা
  • দাহ করার 10% সুযোগ

আমি কীভাবে এটি তৈরি করতে পারি যাতে আমি ঠিক উপরের আইটেমগুলির মধ্যে একটি নির্বাচন করতে পারি, যেখানে সেই শতাংশগুলি লুটপাটের সম্ভাবনা রয়েছে?


1
এফওয়াইআই, তত্ত্ব অনুসারে, কোনও সীমাবদ্ধ বিতরণের জন্য নমুনা অনুসারে ও (1) সময় সম্ভব, এমনকি এমন একটি বিতরণ যা এর এন্ট্রিগুলি পরিবর্তনশীলভাবে পরিবর্তন করে। উদাহরণস্বরূপ দেখুন cstheory.stackexchange.com/questions/37648/…
নিল ইয়ং

উত্তর:


37

নরম কোডেড সম্ভাব্যতা সমাধান

হার্ডকোডযুক্ত সম্ভাবনার সমাধানটির অসুবিধা রয়েছে যা আপনাকে আপনার কোডে সম্ভাব্যতা সেট করতে হবে। আপনি রানটাইমে এগুলি নির্ধারণ করতে পারবেন না। এটি বজায় রাখাও শক্ত।

এখানে একই অ্যালগরিদমের গতিশীল সংস্করণ।

  1. প্রতিটি আইটেমের জোড় প্রকৃত আইটেম এবং ওজনের একটি অ্যারে তৈরি করুন
  2. আপনি একটি আইটেম যোগ করেন, তখন আইটেমটি চাহিদার ওজন নিজস্ব ওজন হতে প্লাস ইতিমধ্যে অ্যারের মধ্যে এই সমস্ত আইটেমের ওজন এর সমষ্টি। সুতরাং আপনার আলাদা আলাদা করে যোগফলটি ট্র্যাক করা উচিত। বিশেষত কারণ পরবর্তী পদক্ষেপের জন্য আপনার এটির প্রয়োজন হবে।
  3. কোনও বস্তু পুনরুদ্ধার করতে, 0 এবং সমস্ত আইটেমের ওজনের যোগফলের মধ্যে একটি এলোমেলো সংখ্যা তৈরি করুন
  4. যতক্ষণ না আপনি এলোমেলো সংখ্যার চেয়ে বড় বা সমান ওজনের একটি এন্ট্রি খুঁজে পান না হওয়া অবধি শেষ থেকে অ্যারেটি পুনরাবৃত্তি করুন

এখানে জাভাতে একটি টেম্পলেট শ্রেণির আকারে একটি নমুনা বাস্তবায়ন যা আপনি আপনার গেমটি যে কোনও অবজেক্টের জন্য ব্যবহার করতে পারেন তা ইনস্ট্যান্ট করতে পারেন। এরপরে আপনি পদ্ধতিটির সাহায্যে অবজেক্ট যুক্ত করতে .addEntry(object, relativeWeight)এবং আপনার পূর্বে যুক্ত করা একটি এন্ট্রি চয়ন করতে পারেন.get()

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class WeightedRandomBag<T extends Object> {

    private class Entry {
        double accumulatedWeight;
        T object;
    }

    private List<Entry> entries = new ArrayList<>();
    private double accumulatedWeight;
    private Random rand = new Random();

    public void addEntry(T object, double weight) {
        accumulatedWeight += weight;
        Entry e = new Entry();
        e.object = object;
        e.accumulatedWeight = accumulatedWeight;
        entries.add(e);
    }

    public T getRandom() {
        double r = rand.nextDouble() * accumulatedWeight;

        for (Entry entry: entries) {
            if (entry.accumulatedWeight >= r) {
                return entry.object;
            }
        }
        return null; //should only happen when there are no entries
    }
}

ব্যবহার:

WeightedRandomBag<String> itemDrops = new WeightedRandomBag<>();

// Setup - a real game would read this information from a configuration file or database
itemDrops.addEntry("10 Gold",  5.0);
itemDrops.addEntry("Sword",   20.0);
itemDrops.addEntry("Shield",  45.0);
itemDrops.addEntry("Armor",   20.0);
itemDrops.addEntry("Potion",  10.0);

// drawing random entries from it
for (int i = 0; i < 20; i++) {
    System.out.println(itemDrops.getRandom());
}

আপনার ইউনিটি, এক্সএনএ বা মনোগেম প্রকল্পের জন্য এখানে সি # তে একই শ্রেণি প্রয়োগ করা হয়েছে :

using System;
using System.Collections.Generic;

class WeightedRandomBag<T>  {

    private struct Entry {
        public double accumulatedWeight;
        public T item;
    }

    private List<Entry> entries = new List<Entry>();
    private double accumulatedWeight;
    private Random rand = new Random();

    public void AddEntry(T item, double weight) {
        accumulatedWeight += weight;
        entries.Add(new Entry { item = item, accumulatedWeight = accumulatedWeight });
    }

    public T GetRandom() {
        double r = rand.NextDouble() * accumulatedWeight;

        foreach (Entry entry in entries) {
            if (entry.accumulatedWeight >= r) {
                return entry.item;
            }
        }
        return default(T); //should only happen when there are no entries
    }
}

এবং এখানে জাভাস্ক্রিপ্ট একটি :

var WeightedRandomBag = function() {

    var entries = [];
    var accumulatedWeight = 0.0;

    this.addEntry = function(object, weight) {
        accumulatedWeight += weight;
        entries.push( { object: object, accumulatedWeight: accumulatedWeight });
    }

    this.getRandom = function() {
        var r = Math.random() * accumulatedWeight;
        return entries.find(function(entry) {
            return entry.accumulatedWeight >= r;
        }).object;
    }   
}

প্রো:

  • যে কোনও ওজনের অনুপাত পরিচালনা করতে পারে। আপনি চাইলে সেটটিতে জ্যোতির্বিদ্যাগতভাবে ছোট সম্ভাবনার আইটেমগুলি রাখতে পারেন। ওজনগুলিকেও 100 পর্যন্ত যোগ করার দরকার নেই।
  • আপনি রানটাইমগুলিতে আইটেমগুলি এবং ওজনগুলি পড়তে পারেন
  • অ্যারের আইটেমের সংখ্যার সাথে আনুপাতিক মেমরির ব্যবহার

বিরূদ্ধে:

  • ডান পেতে আরও কিছু প্রোগ্রামিং প্রয়োজন
  • সবচেয়ে খারাপ ক্ষেত্রে আপনাকে পুরো অ্যারেটি পুনরায় করতে হবে ( O(n)রানটাইম জটিলতা)। সুতরাং আপনার যখন আইটেমগুলির একটি খুব বড় সেট থাকে এবং খুব ঘন ঘন আঁকেন, এটি ধীর হয়ে যেতে পারে। একটি সাধারণ অপ্টিমাইজেশন হ'ল সবচেয়ে সম্ভাব্য আইটেমগুলি প্রথমে রাখি যাতে বেশিরভাগ ক্ষেত্রে অ্যালগরিদম শীঘ্রই বন্ধ হয়ে যায়। আরও জটিল অপ্টিমাইজেশন যা আপনি করতে পারেন তা হ'ল অ্যারে বাছাই করা হয়েছে এবং দ্বিখণ্ডিত অনুসন্ধান করা। এটি কেবল O(log n)সময় নেয় ।
  • এটি ব্যবহারের আগে আপনাকে মেমরির তালিকা তৈরি করতে হবে (যদিও রানটাইমের সময় আপনি সহজেই আইটেম যুক্ত করতে পারেন items আবার O(n)খারাপ সময় রানটাইম আছে)

2
সি # কোড লিনকিউ: রিটার্ন এন্ট্রিগুলি ব্যবহার করে লেখা যেতে পারে irst ফার্স্টঅর্ডার ডিফল্ট (e => e.accumulatedWeight> = r)। আরও গুরুত্বপূর্ণ, একটি সামান্য সম্ভাবনা রয়েছে যে ভাসমান পয়েন্ট যথার্থতা হ্রাসের কারণে এই অ্যালগরিদমটি বাতিল হয়ে যাবে যদি এলোমেলো মান জমে থাকা মানের চেয়ে সামান্য কিছুটা বেশি হয়ে যায়। সতর্কতা হিসাবে, আপনি শেষ উপাদানটিতে একটি সামান্য মান (বলুন, 1.0) যুক্ত করতে পারেন, তবে তারপরে আপনাকে আপনার কোডে স্পষ্টভাবে বলতে হবে যে তালিকাটি চূড়ান্ত is
আইভিল

1
এর জন্য একটি ছোট বৈকল্পিক আমি ব্যক্তিগতভাবে ব্যবহার করেছি, যদি আপনি চান রানটাইমের সময় ওজনের মানগুলি ওজন-প্লাস-সমস্ত-পূর্ববর্তী মানে পরিবর্তন না হয় তবে আপনি প্রতিটি এন্ড্রিজের ওজনকে আপনার এলোমেলো মান থেকে বিয়োগ করতে পারবেন, যখন থামবে এলোমেলো মান বর্তমান আইটেমের ওজনের চেয়ে কম (বা ওজন বিয়োগ করার সময় মানটি <0) করে
লুনিন

2
@ ব্লুরাজা-ড্যানিপ্লুঘুফুট অকাল অপটিমাইজেশন ... প্রশ্নটি একটি খোলা লুট বাক্স থেকে কোনও বিষয় নির্বাচন করার বিষয়ে ছিল। কে প্রতি সেকেন্ডে 1000 টি বাক্স খুলতে যাচ্ছে?
21:58 এএমআইল

4
@IMil: No, the question is a general catch-all for selecting random weighted items. For lootboxes specifically, this answer is probably fine because there are a small number of items and the probabilities don't change (though, since those are usually done on a server, 1000/sec is not unrealistic for a popular game).
BlueRaja - Danny Pflughoeft

4
@opa then flag to close as a dupe. Is it really wrong to upvote a good answer just because the question has been asked before?
Baldrickk

27

Note: I created a C# library for this exact problem

অন্যান্য সমাধানগুলি ঠিক আছে যদি আপনার কাছে কেবলমাত্র অল্প সংখ্যক আইটেম থাকে এবং আপনার সম্ভাবনাগুলি কখনই পরিবর্তিত হয় না। তবে প্রচুর আইটেম বা সম্ভাব্যতা পরিবর্তন করার সাথে (উদাঃ আইটেমগুলি নির্বাচনের পরে অপসারণ) , আপনি আরও শক্তিশালী কিছু চাইবেন।

এখানে দুটি সাধারণ সমাধান রয়েছে (উভয়ই উপরের লাইব্রেরিতে অন্তর্ভুক্ত রয়েছে)

ওয়াকারের এলিয়াস পদ্ধতি

আপনার সম্ভাবনাগুলি যদি অবিচ্ছিন্ন থাকে তবে একটি চতুর সমাধান যা অত্যন্ত দ্রুত ( O(1)!) । সংক্ষেপে, অ্যালগরিদম আপনার সম্ভাব্যতার বাইরে একটি 2 ডি ডার্টবোর্ড ("ওরফে টেবিল") তৈরি করে এবং এতে একটি ডার্ট নিক্ষেপ করে।

Dartboard

আছে প্রবন্ধ প্রচুর অনলাইন কিভাবে এটি কাজ করে যদি আপনি আরো জানতে চাই সম্পর্কে।

The only issue is that if your probabilities change, you need to regenerate the alias table, which is slow. Thus, if you need to remove items after they're picked, this is not the solution for you.

Tree-based solution

The other common solution is to make an array where each item stores the sum of its probability and all the items before it. Then just generate a random number from [0,1) and do a binary search for where that number lands in the list.

এই সমাধানটি কোডিং / বোঝার পক্ষে খুব সহজ, তবে একটি নির্বাচন করা ওয়াকারের আলিয়াস পদ্ধতির চেয়ে ধীর এবং সম্ভবত সম্ভাবনাগুলি পরিবর্তন করা এখনও O(n)। অ্যারেটিকে বাইনারি-অনুসন্ধান ট্রি হিসাবে রূপান্তর করে আমরা এটিকে উন্নত করতে পারি, যেখানে প্রতিটি নোড তার সাবট্রির সমস্ত আইটেমের সম্ভাবনার যোগফলের উপর নজর রাখে। তারপরে যখন আমরা [0,1) থেকে সংখ্যাটি উত্পন্ন করি, তখন আমরা কেবল গাছটিকে নীচে রেখে আইটেমটি উপস্থাপন করতে পারি।

এই আমাদের দেয় O(log n)একটি আইটেম বাছাই এবং সম্ভাব্যতা পরিবর্তন করুন! এটি NextWithRemoval()অত্যন্ত দ্রুত করে তোলে !

ফলাফলগুলো

এই দুটি পদ্ধতির তুলনা করে উপরের লাইব্রেরি থেকে এখানে কিছু দ্রুত মানদণ্ড দেওয়া হয়েছে

         ওয়েটআরেন্ডোমাইজার বেঞ্চমার্ক | গাছ | টেবিল
-------------------------------------------------- ---------------------------------
Add()x10000 + NextWithReplacement()x10:                 |    4 ms    |      2 ms
Add()x10000 + NextWithReplacement()x10000:              |    7 ms    |      4 ms
Add()x10000 + NextWithReplacement()x100000:             |   35 ms    |     28 ms
( Add() + NextWithReplacement() )x10000 (interleaved)   |    8 ms    |   5403 ms
Add()x10000 + NextWithRemoval()x10000:                  |   10 ms    |   5948 ms

So as you can see, for the special case of static (non-changing) probabilities, Walker's Alias method is about 50-100% faster. But in the more dynamic cases, the tree is several orders of magnitude faster!


The tree-based solution also gives us a decent run-time (nlog(n)) when sorting items by weight.
Nathan Merrill

2
I'm skeptical of your results, but this is the correct answer. Not sure why this isn't the top answer, considering this is actually the canonical way to handle this problem.
whn

Which file contains the tree based solution? Second, your benchmark table: is Walker's Alias the "table" column?
Yakk

1
@Yakk: The code for the tree-based solution is here. It's built upon an open-source implementation of an AA-tree. And 'yes' to your second question.
BlueRaja - Danny Pflughoeft

1
The Walker part is pretty just link-only.
Acccumulation

17

The Wheel of Fortune solution

You can use this method when the probabilities in your item pool have a rather large common denominator and you need to draw from it very often.

Create an array of options. But put each element into it multiple times, with the number of duplicates of each element proportional to its chance of appearing. For the example above, all elements have probabilities which are multipliers of 5%, so you can create an array of 20 elements like this:

10 gold
sword
sword
sword
sword
shield
shield
shield
shield
shield
shield
shield
armor
armor
armor
armor
potion
potion

Then simply pick a random element of that list by generating one random integer between 0 and the length of the array - 1.

Disadvantages:

  • You need to build the array the first time you want to generate an item.
  • When one of your elements is supposed to have a very low probability, you end up with a really large array, which can require a lot of memory.

Advantages:

  • When you already have the array and want to draw from it multiple times, then it is very fast. Just one random integer and one array access.

3
As a hybrid solution to avoid the second disadvantage, you can designate the last slot as "other," and handle it via other means, such as Philipp's array approach. Thus you might fill that last slot with an array offering a 99.9% chance of a potion, and just a 0.1% chance of an Epic Scepter of the Apocalypse. Such a two tiered approach leverages the advantages of both approaches.
Cort Ammon - Reinstate Monica

1
I use somewhat a variation of this in my own project. What I do is calculate each item & weight, and store those in an array, [('gold', 1),('sword',4),...], sum up all of the weights, and then roll a random number from 0 to the sum, then iterate the array and calculate where the random number lands (ie a reduce). Works fine for arrays that are updated often, and no major memory hog.

1
@Thebluefish That solution is described in my other answer "The Soft-coded Probabilities Solution"
Philipp

7

The Hard-coded Probabilities Solution

The most simple way find a random item from a weighted collection is to traverse down a chain of if-else statements, where each if-else increases in probably, as the previous one does not hit.

int rand = random(100); //Random number between 1 and 100 (inclusive)
if(rand <= 5) //5% chance
{
    print("You found 10 gold!");
}
else if(rand <= 25) //20% chance
{
    print("You found a sword!");
}
else if(rand <= 70) //45% chance
{
    print("You found a shield!");
}
else if(rand <= 90) //20% chance
{
    print("You found armor!");
}
else //10% chance
{
    print("You found a potion!");
}

The reason the conditionals are equal to its chance plus all of the previous conditionals chances is because the previous conditionals have already eliminated the possibility of it being those items. So for the shield's conditional else if(rand <= 70), 70 is equal to the 45% chance of the shield, plus the 5% chance of the gold and 20% chance of the sword.

Advantages:

  • Easy to program, because it requires no data structures.

Disadvantages:

  • Hard to maintain, because you need to maintain your drop-rates in your code. You can't determine them at runtime. So if you want something more future proof, you should check the other answers.

3
This would be really annoying to maintain. E.g. if you wish to remove gold, and make potion takes its spot, you need to adjust the probabilities of all items between them.
Alexander - Reinstate Monica

1
To avoid the issue that @Alexander mentions, you can instead subtract the current rate at each step, instead of adding it to each condition.
AlexanderJ93

2

In C# you could use a Linq scan to run your accumulator to check against a random number in the range 0 to 100.0f and .First() to get. So like one line of code.

So something like:

var item = a.Select(x =>
{
    sum += x.prob;
    if (rand < sum)
        return x.item;
    else
        return null;
 }).FirstOrDefault());

sum is a zero initialized integer and a is a list of prob/item structs/tuples/instances. rand is a previously generated random number in the range.

This simply accumulates the sum over the list of ranges until it exceeds the previously selected random number, and returns either the item or null, where null would be returned if the random number range (e.g. 100) is less than the total weighting range by mistake, and the random number selected is outside the total weighting range.

However, you will notice that weights in OP closely match a normal distribution (Bell Curve). I think in general you will not want specific ranges, you will tend to want a distribution that tapers off either around a bell curve or just on a decreasing exponential curve (for example). In this case you could just use a mathematical formula to generate an index into an array of items, sorted in order of preferred probability. A good example is CDF in normal distribution

Also an example here.

Another example is that you could take a random value from 90 degrees to 180 degrees to get the lower right quadrant of a circle, take the x component using cos(r) and use that to index into a prioritized list.

With different formulae you could have a general approach where you just input a prioritized list of any length (e.g. N) and map the outcome of the formula (e.g.: cos(x) is 0 to 1) by multiplication (e.g.: Ncos(x) = 0 to N) to get the index.


3
Could you give us this line of code if it's just one line? I'm not as familiar with C# so I don't know what you mean.
HEGX64

@HEGX64 added but using mobile and editor not working. Can you edit?
Sentinel

4
Can you change this answer to explain the concept behind it, rather than a specific imlementation in a specific language?
Raimund Krämer

@RaimundKrämer Erm, done?
Sentinel

Downvote without explanation = useless and antisocial.
WGroleau

1

Probabilities don’t need to be hard-coded. The items and the thresholds can be together in an array.

for X in itemsrange loop
  If items (X).threshold < random() then
     Announce (items(X).name)
     Exit loop
  End if
End loop

You do have to accumulate the thresholds still, but you can do it when creating a parameter file instead of coding it.


3
Could you elaborate on how to calculate the correct threshold? For example, if you have three items with 33% chance each, how would you build this table? Since a new random() is generated each time, the first would need 0.3333, the second would need 0.5 and the last would need 1.0. Or did I read the algorithm wrong?
pipe

You compute it the way others did in their answers. For equal probabilities of X items, the first threshold is 1/X, the second, 2/X, etc.
WGroleau

Doing that for 3 items in this algorithm would make the thresholds 1/3, 2/3 and 3/3 but the outcome probabilities 1/3, 4/9 and 2/9 for the first, second and third item. Do you really mean to have the call to random() in the loop?
pipe

No, that's definitely a bug. Each check needs the same random number.
WGroleau

0

I done this function: https://github.com/thewheelmaker/GDscript_Weighted_Random Now! in your case you can use it like this:

on_normal_case([5,20,45,20,10],0)

It gives just a number between 0 to 4 but you can put it in array where you got the items.

item_array[on_normal_case([5,20,45,20,10],0)]

Or in function:

item_function(on_normal_case([5,20,45,20,10],0))

Here is the code. I made it on GDscript, you can, but it can alter other language, also check for logic errors:

func on_normal_case(arrayy,transformm):
    var random_num=0
    var sum=0
    var summatut=0
    #func sumarrays_inarray(array):
    for i in range(arrayy.size()):
        sum=sum+arrayy[i]
#func no_fixu_random_num(here_range,start_from):
    random_num=randi()%sum+1
#Randomies be pressed down
#first start from zero
    if 0<=random_num and random_num<=arrayy[0]:
        #print(random_num)
        #print(array[0])
        return 0+ transformm
    summatut=summatut+arrayy[0]
    for i in range(arrayy.size()-1):
        #they must pluss together
        #if array[i]<=random_num and random_num<array[i+1]:
        if summatut<random_num and random_num<=summatut+arrayy[i+1]:
            #return i+1+transform
            #print(random_num)
            #print(summatut)
            return i+1+ transformm

        summatut=summatut+arrayy[i+1]
    pass

It works like this: on_normal_case([50,50],0) This gives 0 or 1, it has same probability both.

on_normal_case([50,50],1) This gives 1 or 2, it has same probability both.

on_normal_case([20,80],1) This gives 1 or 2, it has bigger change to get two.

on_normal_case([20,80,20,20,30],1) This give random numbers range 1-5 and bigger numbers are more likely than smaller numbers.

on_normal_case([20,80,0,0,20,20,30,0,0,0,0,33],45) This throw dices between numbers 45,46,49,50,51,56 you see when there is zero it never occure.

So it function returns just one random number that depends lenght of that arrayy array and transformm number, and ints in the array are probability weights that a number might occure, where that number is location on the array, pluss transformm number.

আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.