Create a gacha system by binary search

6 minute read

Overview

Given the following probability distribution, we would like to randomly determine one element according to this.

確率分布.png

The source code of the implementation is given at the end of the article. Since Unity etc. came up first as an operation destination, I decided to implement it in C #.
I don’t know if this feature has a name, but it’s called a gacha system because it resembles the gacha that has become popular in social games in recent years.

Way of thinking

As input, we give a sequence of probability distributions $ A_1, A_2, ⋯, A_N $ consisting of $ N $ elements.

double[] odds = { 0.5, 0.3, 0.15, 0.05 };

確率分布 – 1.png

First, determine one random real number $ r $ in the range of $ [0, \ sum_ {i = 1} ^ {N} A_i] $.

Random rnd = new Random();
double r = rnd.NextDouble();

確率分布 – 2.png

Next, find the integer $ k $ such that $ \ sum_ {i = 1} ^ {k} A_i \ leq r <\ sum_ {i = 1} ^ {k + 1} A_i $.
A simple method to come up with is a linear search, but this time I will try to speed it up and implement it with a binary search. * The explanation of binary search is omitted.
Since the partial sum of the probability distribution must be calculated, the implementation as a whole is $ O (N) $. Not $ O (\ log N) $.

For the time being, define the signature of the function.
As arguments, take the probability distribution $ A_1, A_2, ⋯, A_N $, and the randomly determined value $ r \; (0 \ leq r \ leq \ sum_ {i = 1} ^ {N} A_i) $. ..
The return value should be the index $ k $ corresponding to $ r $.

public int BinarySearchInOdds(double[] odds, double r) {
    //Returns the index corresponding to the value of r from the odds probability distribution
}

The implementation of this part is not much different from the general binary search, so the explanation is omitted.
The idea is as above.

Implementation

The BinarySearchInOdds () method implements the “function that returns the index corresponding to the value of r from the probability distribution” that was omitted earlier.
You can easily draw a gacha by passing the probability distribution to the Draw () method.
I wrote Main () so that you can understand the specifications, so please take a look at the execution results as well.

using System;
using System.Collections.Generic;
using System.Linq;

public class Gacha {

    //Returns the index of the element corresponding to r in the probability distribution
    public static int BinarySearchInOdds(double[] odds, double r) {
        if (r < 0) return -1;
        if (odds.Length == 0) return -1;

        //Obtain the partial sum of the probability distribution in advance
        double[] sum_from_left = new double[odds.Length];
        double[] sum_from_right = new double[odds.Length];
        double odd_total = odds.Sum();
        sum_from_left[0] = 0;
        sum_from_right[0] = odd_total;
        for (int i = 1; i < odds.Length; i++) {
            sum_from_left[i] = sum_from_left[i-1] + odds[i-1];
            sum_from_right[i] = sum_from_right[i-1] - odds[i-1];
        }

        int left = 0;
        int right = odds.Length - 1;
        int mid = left + (right - left) / 2;
        double L_mid = sum_from_left[mid];

        while (right >= left) {
            double R_mid = L_mid + odds[mid];
            if ((L_mid <= r && r < R_mid) || (r == R_mid && mid == odds.Length - 1)) {
                //If the key is within the mid-th element
                return mid;
            } else if (r < L_mid) {
                //If the key is in a range smaller than the range of the midth element
                right = mid - 1;
                mid = left + (right - left) / 2;
                R_mid = L_mid - (odd_total - sum_from_left[mid + 1] - sum_from_right[right + 1]);
                L_mid = R_mid - odds[mid];
            } else {
                //If the key is in a range larger than the range of the midth element
                left = mid + 1;
                mid = left + (right - left) / 2;
                if (mid >= odds.Length) break;
                L_mid = R_mid + (odd_total - sum_from_left[left] - sum_from_right[mid]);
                R_mid = L_mid + odds[mid];
            }
        }
        return -1;
    }

    //Returns the index of randomly determined elements according to the probability distribution
    public static int Draw(double[] odds) {
        Random rnd = new Random();
        double r = rnd.NextDouble() * odds.Sum();
        return BinarySearchInOdds(odds, r);
    }

    public static void Main() {
        double[] odds1 = { 0.5, 3.0, 0, 2.0, 1.5, 0.1, 0.9 };
        Console.WriteLine("odds1 = [" + string.Join(", ", odds1) + "]");
        Console.WriteLine("r = 0.0  --->  " + BinarySearchInOdds(odds1, 0.0));
        Console.WriteLine("r = 0.1  --->  " + BinarySearchInOdds(odds1, 0.1));
        //When r is just on the border, the larger index is returned
        Console.WriteLine("r = 0.5  --->  " + BinarySearchInOdds(odds1, 0.5));
        Console.WriteLine("r = 2.0  --->  " + BinarySearchInOdds(odds1, 2.0));
        Console.WriteLine("r = 4.0  --->  " + BinarySearchInOdds(odds1, 4.0));
        Console.WriteLine("r = 6.0  --->  " + BinarySearchInOdds(odds1, 6.0));
        Console.WriteLine("r = 7.08  --->  " + BinarySearchInOdds(odds1, 7.08));
        Console.WriteLine("r = 7.7  --->  " + BinarySearchInOdds(odds1, 7.7));
        //If r is equal to the sum of the probability distributions, the index of the last element is returned
        Console.WriteLine("r = 8.0  --->  " + BinarySearchInOdds(odds1, 8.0));
        //If r is out of range-1 is returned
        Console.WriteLine("r = 10.0  --->  " + BinarySearchInOdds(odds1, 10.0));
        //The same applies when r is negative
        Console.WriteLine("r = -1.0  --->  " + BinarySearchInOdds(odds1, -1.0));
        Console.WriteLine("");

        double[] odds2 = { 1.0 };
        Console.WriteLine("odds2 = [" + string.Join(", ", odds2) + "]");
        //There is no problem even if there is only one element of the probability distribution
        Console.WriteLine("r = 0.0  --->  " + BinarySearchInOdds(odds2, 0.0));
        Console.WriteLine("r = 0.5  --->  " + BinarySearchInOdds(odds2, 0.5));
        Console.WriteLine("r = 1.0  --->  " + BinarySearchInOdds(odds2, 1.0));
        Console.WriteLine("");

        double[] odds3 = {};
        //If there is no element of probability distribution-1 is returned
        Console.WriteLine("odds3 = [" + string.Join(", ", odds3) + "]");
        Console.WriteLine("r = 0.0  --->  " + BinarySearchInOdds(odds3, 0.0));
        Console.WriteLine("");

        double[] odds4 = { 0.5, 0.35, 0.15, 0.05 };
        Console.WriteLine("odds4 = [" + string.Join(", ", odds4) + "]");
        //Draw a gacha 1 million times and check the emission probability
        int[] receivedCounts = new int[4];
        for (int i = 0; i < 1000000; i++) {
            int received = Draw(odds4);
            receivedCounts[received]++;
        }
        for (int i = 0; i < 4; i++) {
            Console.WriteLine("element" + i + "  " + receivedCounts[i] + " / 1000000  ( " + receivedCounts[i] / 10000 + " %)");
        }
    }

}

Execution result

odds1 = [0.5, 3, 0, 2, 1.5, 0.1, 0.9]
r = 0.0  --->  0
r = 0.1  --->  0
r = 0.5  --->  1
r = 2.0  --->  1
r = 4.0  --->  3
r = 6.0  --->  4
r = 7.08  --->  5
r = 7.7  --->  6
r = 8.0  --->  6
r = 10.0  --->  -1
r = -1.0  --->  -1

odds2 = [1]
r = 0.0  --->  0
r = 0.5  --->  0
r = 1.0  --->  0

odds3 = []
r = 0.0  --->  -1

odds4 = [0.5, 0.35, 0.15, 0.05]
Element 0 456796/ 1000000  ( 45 %)
Element 1 333225/ 1000000  ( 33 %)
Element 2 144646/ 1000000  ( 14 %)
Element 3 65333/ 1000000  ( 6 %)

The execution speed was also measured.

N = 10^2  ->  2 ms
N = 10^3  ->  2 ms
N = 10^4  ->  2 ms
N = 10^5  ->  4 ms
N = 10^6  ->  18 ms
N = 10^7  ->  173 ms
N = 10^8  ->  1683 ms

It seems that the order of processing time does not change until about $ N = 10 ^ 5 $. The speedup by binary search has come to life a little …

With this, you can always draw gacha.

Tags:

Updated: