Подтвердить что ты не робот

Добавление "среднего" параметра в .NET Random.Next() к результатам кривой

Я хотел бы добавить "средний" параметр в Random.Next(Lower, Upper). Этот метод имел бы параметры min, max и average. Я создал метод, подобный этому, для тестирования (он использовал списки и был ужасен), поэтому мне хотелось бы, чтобы некоторые идеи о том, как написать правильную реализацию.

Причиной такой функциональности является множество процедурных/случайных событий в моей игре. Предположим, что вы хотите, чтобы деревья составляли 10 единиц в большую часть времени, но все равно могут быть как 5 или 15. Нормальный Random.Next(5,15) будет возвращать результаты повсюду, но этот метод будет иметь больше кривых колокола по отношению к ним. Значение 10 будет наиболее распространенным, и выход в каждом направлении будет менее распространенным. Например, перемещение среднего значения до 7 приведет к относительно небольшим деревьям (или независимо от того, что это используется), но большие по-прежнему возможны, как бы необычны.

Предыдущий метод (псевдокод-иш)

  • Loop from min to max
  • Closer to average numbers are added to the list more times
  • A random element is selected from the list добавляются элементы, более близкие к средним  больше, поэтому они будут, скорее всего, выбраны.

Хорошо, так что, например, бросать кучу конфет в сумку и выбирать случайную. Да, медленно. Что вы думаете об этом?

Иллюстрация: (Не совсем точно, но вы видите идею) введите описание изображения здесь

ПРИМЕЧАНИЕ. Многие люди предложили кривую колокола, но вопрос заключается в том, как изменить пик кривой в пользу одной стороны в этом смысле.

4b9b3361

Ответ 1

Я расширяю идею генерации n случайных чисел и беру их среднее значение, чтобы получить эффект колоколообразной кривой. Параметр "плотность" определяет, насколько крутая кривая.

Изменить:. Суммирование набора случайных точек для получения "нормального" распределения поддерживается Центральная предельная теорема. Использование функции предвзятости для изменения результатов в определенном направлении является общей методикой, но я не эксперт.

Чтобы обратиться к заметке в конце вашего вопроса, я искажаю кривую, манипулируя "внутренним" случайным числом. В этом примере я поднимаю его до показателя, который вы предоставляете. Так как случайный возвращает значения меньше единицы, повышение его до любой мощности по-прежнему никогда не будет более одного. Но среднее отклонение к нулю, поскольку квадраты, кубы и т.д. Чисел меньше единицы, даже меньше, чем базовое число. exp = 1 не имеет перекоса, тогда как exp = 4 имеет довольно существенный перекос.

        private Random r = new Random();        

        public double RandomDist(double min, double max, int tightness, double exp)
        {
            double total = 0.0;
            for (int i = 1; i <= tightness; i++)
            {
                total += Math.Pow(r.NextDouble(), exp);
            }

            return ((total / tightness) * (max - min)) + min;
        }

Я запускал испытания для разных значений для exp, генерируя 100 000 целых чисел от 0 до 99. Вот как получилось распределение.

Skewed Random Distribution

Я не уверен, как пик относится к значению exp, но чем выше значение exp, тем меньше пик появляется в диапазоне.

Вы также можете изменить направление перекоса, изменив линию внутри цикла на:

 total += (1 - Math.Pow(r.NextDouble(), exp));

..., что дало бы смещение на высокой стороне кривой.

Изменить: Итак, как мы узнаем, что сделать "exp", чтобы получить пик, где мы хотим? Это сложная задача, и ее можно было бы аналитически анализировать, но я разработчик, а не математик. Таким образом, применяя мою торговлю, я провел много испытаний, собрал пиковые данные для различных значений exp и запустил данные через калькулятор кубической подстановки Wolfram Alpha получить уравнение для exp как функцию максимума.

Здесь представлен новый набор функций, реализующих эту логику. Функция GetExp (...) реализует уравнение, найденное WolframAlpha.

RandomBiasedPow (...) - представляющая интерес функция. Он возвращает случайное число в указанных диапазонах, но стремится к пику. Сила этой тенденции определяется параметром герметичности.

    private Random r = new Random();

    public double RandomNormal(double min, double max, int tightness)
    {
        double total = 0.0;
        for (int i = 1; i <= tightness; i++)
        {
            total += r.NextDouble();
        }
        return ((total / tightness) * (max - min)) + min;
    }

    public double RandomNormalDist(double min, double max, int tightness, double exp)
    {
        double total = 0.0;
        for (int i = 1; i <= tightness; i++)
        {
            total += Math.Pow(r.NextDouble(), exp);
        }

        return ((total / tightness) * (max - min)) + min;
    }


    public double RandomBiasedPow(double min, double max, int tightness, double peak)
    {
        // Calculate skewed normal distribution, skewed by Math.Pow(...), specifiying where in the range the peak is
        // NOTE: This peak will yield unreliable results in the top 20% and bottom 20% of the range.
        //       To peak at extreme ends of the range, consider using a different bias function

        double total = 0.0;
        double scaledPeak = peak / (max - min) + min;

        if (scaledPeak < 0.2 || scaledPeak > 0.8)
        {
            throw new Exception("Peak cannot be in bottom 20% or top 20% of range.");
        }

        double exp = GetExp(scaledPeak);

        for (int i = 1; i <= tightness; i++)
        {
            // Bias the random number to one side or another, but keep in the range of 0 - 1
            // The exp parameter controls how far to bias the peak from normal distribution
            total += BiasPow(r.NextDouble(), exp);
        }

        return ((total / tightness) * (max - min)) + min;
    }

    public double GetExp(double peak)
    {
        // Get the exponent necessary for BiasPow(...) to result in the desired peak 
        // Based on empirical trials, and curve fit to a cubic equation, using WolframAlpha
        return -12.7588 * Math.Pow(peak, 3) + 27.3205 * Math.Pow(peak, 2) - 21.2365 * peak + 6.31735;
    }

    public double BiasPow(double input, double exp)
    {
        return Math.Pow(input, exp);
    }

Вот гистограмма с использованием RandomBiasedPow (0, 100, 5, пик) с различными значениями пика, показанных в легенде. Я округлился, чтобы получить целые числа от 0 до 99, установить плотность до 5 и попробовать пиковые значения между 20 и 80. (Вещи становятся неустойчивыми при экстремальных пиковых значениях, поэтому я оставил их и поставил предупреждение в код.) могут видеть пики прямо там, где они должны быть.

Various Peak Values, Tightness=5

Затем я попытался повысить плотность до 10...

enter image description here

Распространение более жесткое, и пики все еще там, где они должны быть. Это тоже довольно быстро!

Ответ 2

Вот простой способ добиться этого. Поскольку у вас уже есть ответы, подробно описывающие, как генерировать нормальные дистрибутивы, и на это много ресурсов, я не буду повторять этого. Вместо этого я буду ссылаться на метод, который я назову GetNextNormal(), который должен генерировать значение из нормального распределения со средним 0 и стандартным отклонением 1.

public int Next(int min, int max, int center)
{
    int rand = GetNextNormal();
    if(rand >= 0)
        return center + rand*(max-center);
    return center + rand*(center-min);
}

(Это можно немного упростить, я написал это для ясности)

Для грубого изображения того, что это делает, представьте два нормальных распределения. Они оба сосредоточены вокруг вашего center, но для одного min - одно стандартное отклонение, влево, а для другого, max - одно стандартное отклонение вправо. Теперь представьте, что вы нарезаете их пополам на center. Слева вы сохраните один со стандартным отклонением, соответствующим min, а справа - соответствующий, соответствующий max.

Конечно, нормальные распределения не гарантируются в пределах одного стандартного отклонения, поэтому есть две вещи, которые вы, вероятно, захотите сделать:

  • Добавьте дополнительный параметр, который определяет, насколько жестким является распределение.
  • Если вы хотите, чтобы min и max были жесткими ограничениями, вам нужно будет добавить отклонение для значений вне этих границ.

Полный метод с этими двумя дополнениями (снова сохраняя все как int), может выглядеть так:

public int Next(int min, int max, int center, int tightness)
{
    int rand = GetNextNormal();
    int candidate;

    do
    {
        if(rand >= 0)
            candidate = center + rand*(max-center)/tightness;
        else
            candidate = center + rand*(center-min)/tightness;
    } while(candidate < min || candidate > max);

    return candidate;
}

Если вы нарисуете результаты этого (особенно версии float/double), это будет не самое красивое распространение, но оно должно быть адекватным для ваших целей.

ИЗМЕНИТЬ

Выше я сказал, что результаты этого не особенно красивы. Чтобы расширить это, наиболее вопиющим "уродством" является разрыв в центральной точке из-за высоты пика нормального распределения в зависимости от его стандартного отклонения. Из-за этого распределение, в котором вы закончите, будет выглядеть примерно так:

original version

(для мин 10, максимум 100 и центральной точки 70, используя "плотность" 3)

Итак, хотя вероятность значения ниже центра равна вероятности выше, результаты будут гораздо более "сгруппированы" вокруг среднего с одной стороны, чем другие. Если это слишком уродливо для вас или вы считаете, что результаты генерации функций по подобному распределению будут казаться слишком неестественными, мы можем добавить дополнительную модификацию, взвешивая, какая сторона центра выбирается пропорциями диапазона влево или справа от центра. Добавив это в код (при условии, что у вас есть доступ к Random, который я только что назвал RandomGen), мы получаем:

public int Next(int min, int max, int center, int tightness)
{
    int rand = Math.Abs(GetNextNormal());
    int candidate;

    do
    {
        if(ChooseSide())
            candidate = center + rand*(max-center)/tightness;
        else
            candidate = center - rand*(center-min)/tightness;
    } while(candidate < min || candidate > max);

    return candidate;
}

public bool ChooseSide(int min, int max, int center)
{
    return RandomGen.Next(min, max) >= center;
}

Для сравнения распределение, которое будет производиться с такими же минимальными значениями, макс, центр и плотность:

smoother version

Как вы можете видеть, это теперь непрерывно по частоте, а также первая производная (дающая гладкий пик). Недостатком этой версии над другой является то, что вы, скорее всего, получите результаты на одной стороне центра, чем другие. Центр теперь является модальным средним, а не средним. Таким образом, это зависит от вас, предпочитаете ли вы более плавное распределение или иметь центр, являющийся истинным средством распространения.

Ответ 3

Поскольку вы ищете нормальное распределение с значением вокруг точки, в пределах границ, почему бы не использовать Random вместо этого, чтобы дать вам два значения, которые вы затем используете, чтобы пройти расстояние от середины? Ниже приводится то, что я считаю вам нужным:

// NOTE: scoped outside of the function to be random
Random rnd = new Random();
int GetNormalizedRandomValue(int mid, int maxDistance)
{
    var distance = rnd.Next(0, maxDistance + 1);
    var isPositive = (rnd.Next() % 2) == 0;
    if (!isPositive)
    {
        distance = -distance;
    }

    return mid + distance;
}

Включение http://www.codeproject.com/Articles/25172/Simple-Random-Number-Generation упрощает и правильно нормализует:

int GetNormalizedRandomValue(int mid, int maxDistance)
{
    int distance;
    do
    {
        distance = (int)((SimpleRNG.GetNormal() / 5) * maxDistance);
    } while (distance > maxDistance);
    return mid + distance;
}

Ответ 4

Я бы сделал что-то вроде этого:

  • вычислить единую распределенную двойную
  • используя эту формулу, используйте формулу для нормального распределения (если я правильно помню, вы называете ее "инверсной функцией плотности"? ну, тот, который отображает [0,1] "назад" на накопленные вероятности) или аналогичен вычислению желаемого значение - например вы можете слегка настроить нормальное распределение, чтобы не только принимать средние значения и stddev/variance, но и средние и два такие значения, чтобы позаботиться о min/max
  • round to int, убедитесь, что min, max и т.д.

Ответ 5

Вот мое решение. Класс MyRandom имеет эквивалентную функцию Next() с тремя дополнительными параметрами. Центр и span указывают желаемый диапазон, Повторить - количество повторов, при каждой попытке вероятность генерации числа в желаемом диапазоне должна теоретически возрастает ровно на 50%.

static void Main()
{
    MyRandom myRnd = new MyRandom();
    List<int> results = new List<int>();

    Console.WriteLine("123456789012345\r\n");

    int bnd = 30;

    for (int ctr = 0; ctr < bnd; ctr++)
    {
        int nextAvg = myRnd.NextAvg(5, 16, 10, 2, 2);
        results.Add(nextAvg);

        Console.WriteLine(new string((char)9608, nextAvg));
    }


    Console.WriteLine("\r\n" + String.Format("Out of range: {0}%", results.Where(x => x < 8 || x > 12).Count() * 100 / bnd)); // calculate out-of-range percentage
    Console.ReadLine();
}

class MyRandom : Random
{
    public MyRandom() { }

    public int NextAvg(int min, int max, int center, int span, int retry)
    {
        int left = (center - span);
        int right = (center + span);

        if (left < 0 || right >= max)
        {
            throw new ArgumentException();
        }

        int next = this.Next(min, max);
        int ctr = 0;

        while (++ctr <= retry && (next < left || next > right))
        {
            next = this.Next(min, max);
        }

        return next;
    }
}

Ответ 6

Здесь у вас есть два варианта:

  • Суммируйте N случайные числа из (0,1/N), которые собирают результаты вокруг 0.5 и масштабируют результаты с помощью x_min и x_max. Число N зависит от того, насколько сужены результаты. Чем выше счет, тем более узкие результаты.

    Random rnd = new Random();
    int N=10;
    double r = 0;    
    for(int i=0; i<N; i++) { r+= rnd.NextDouble()/N; }
    double x = x_min+(x_max-x_min)*r;
    
  • Используйте фактическое нормальное распределение со средним и стандартным отклонением. Это не гарантирует минимум или максимум, хотя.

    public double RandomNormal(double mu, double sigma)
    {
        return NormalDistribution(rnd.NextDouble(), mu, sigma);
    }
    public double RandomNormal()
    {
        return RandomNormal(0d, 1d);
    }
    /// <summary>
    /// Normal distribution
    /// </summary>
    /// <arg name="probability">probability value 0..1</arg>
    /// <arg name="mean">mean value</arg>
    /// <arg name="sigma">std. deviation</arg>
    /// <returns>A normal distribution</returns>
    public double NormalDistribution(double probability, double mean, double sigma)
    {
        return mean+sigma*NormalDistribution(probability);
    }
    /// <summary>
    /// Normal distribution
    /// </summary>
    /// <arg name="probability">probability value 0.0 to 1.0</arg>
    /// <see cref="NormalDistribution(double,double,double)"/>
    public double NormalDistribution(double probability)
    {
        return Math.Sqrt(2)*InverseErrorFunction(2*probability-1);
    }
    public double InverseErrorFunction(double P)
    {
        double Y, A, B, X, Z, W, WI, SN, SD, F, Z2, SIGMA;
        const double A1=-.5751703, A2=-1.896513, A3=-.5496261E-1;
        const double B0=-.1137730, B1=-3.293474, B2=-2.374996, B3=-1.187515;
        const double C0=-.1146666, C1=-.1314774, C2=-.2368201, C3=.5073975e-1;
        const double D0=-44.27977, D1=21.98546, D2=-7.586103;
        const double E0=-.5668422E-1, E1=.3937021, E2=-.3166501, E3=.6208963E-1;
        const double F0=-6.266786, F1=4.666263, F2=-2.962883;
        const double G0=.1851159E-3, G1=-.2028152E-2, G2=-.1498384, G3=.1078639E-1;
        const double H0=.9952975E-1, H1=.5211733, H2=-.6888301E-1;
    
        X=P;
        SIGMA=Math.Sign(X);
        if(P<-1d||P>1d)
            throw new System.ArgumentException();
        Z=Math.Abs(X);
        if(Z>.85)
        {
            A=1-Z;
            B=Z;
            W=Math.Sqrt(-Math.Log(A+A*B));
            if(W>=2.5)
            {
                if(W>=4.0)
                {
                    WI=1.0/W;
                    SN=((G3*WI+G2)*WI+G1)*WI;
                    SD=((WI+H2)*WI+H1)*WI+H0;
                    F=W+W*(G0+SN/SD);
                }
                else
                {
                    SN=((E3*W+E2)*W+E1)*W;
                    SD=((W+F2)*W+F1)*W+F0;
                    F=W+W*(E0+SN/SD);
                }
            }
            else
            {
                SN=((C3*W+C2)*W+C1)*W;
                SD=((W+D2)*W+D1)*W+D0;
                F=W+W*(C0+SN/SD);
            }
        }
        else
        {
            Z2=Z*Z;
            F=Z+Z*(B0+A1*Z2/(B1+Z2+A2/(B2+Z2+A3/(B3+Z2))));
        }
        Y=SIGMA*F;
        return Y;
    }
    

Ответ 7

Вы можете использовать Обычный класс распределения из MathNet.Numerics(mathdotnet.com).

Пример использования:

// Distribution with mean = 10, stddev = 1.25 (5 ~ 15 99.993%)
var dist = new MathNet.Numerics.Distributions.Normal(10, 1.25);
var samples = dist.Samples().Take(10000);
Assert.True(samples.Average().AlmostEqualInDecimalPlaces(10, 3));

Вы можете настроить распространение, изменив стандартное отклонение (1,25, которое я использовал). Единственная проблема заключается в том, что она будет иногда давать вам ценности за пределами вашего желаемого диапазона, чтобы у вас было для них. Если вы хотите что-то более искаженное так или иначе, вы можете попробовать другие функции распределения из библиотеки.

Обновление - Класс примера:

public class Random
{
    MathNet.Numerics.Distributions.Normal _dist;
    int _min, _max, _mean;

    public Random(int mean, int min, int max)
    {
        _mean = mean;
        _min = min;
        _max = max;
        var stddev = Math.Min(Math.Abs(mean - min), Math.Abs(max - mean)) / 3.0;
        _dist = new MathNet.Numerics.Distributions.Normal(mean, stddev);
    }

    public int Next()
    {
        int next;
        do
        {
            next = (int)_dist.Sample();
        } while (next < _min || next > _max);

        return next;
    }

    public static int Next(int mean, int min, int max)
    {
        return new Random(mean, min, max).Next();
    }
}

Ответ 8

Есть ли причина, по которой распределение должно быть кривым? Например, используя:

public int RandomDist(int min, int max, int average)
{
  rnd = new Math.Random();
  n = rnd.NextDouble();
  if (n < 0.75)
  {
    return Math.Sqrt(n * 4 / 3) * (average - min) + min;
  } else {
    return Math.Sqrt(n * 4 - 3) * (max - average) + average;
  }
}

даст число между min и max, с режимом average.

Ответ 9

Не уверен, что это то, что вы хотите, но вот способ нарисовать случайное число с распределением, которое равномерно от min до avg и от avg до max, гарантируя при этом, что среднее равно avg.

Предположим вероятность p для ничьей из [min avg] и вероятности 1-p из [avg max]. Ожидаемое значение будет p.(min+avg)/2 + (1-p).(avg+max)/2 = p.min/2 + avg/2 + (1-p).max/2 = avg. Решим для p: p=(max-avg)/(max-min).

Генератор работает следующим образом: нарисуйте случайное число в [0 1]. Если меньше p, нарисуйте случайное число из [min avg]; в противном случае нарисуйте один из [avg max].

График вероятности кусочно-постоянный, p от min до avg и 1-p от avg до max. Экстремальные значения не наказываются.