22

我希望能够将“平均”参数添加到Random.Next(Lower, Upper). 此方法将具有min,maxaverage参数。不久前我创建了一个这样的方法进行测试(它使用列表并且很糟糕),所以我想要一些关于如何编写正确实现的想法。

具有此功能的原因是我的游戏中有许多程序/随机事件。假设您希望树在大多数情况下都高 10 个单位,但仍然可以低至 5 或 15 个单位。法线Random.Next(5,15)会全部返回结果,但这种方法的结果会有更多的钟形曲线。这意味着 10 将是最常见的,并且在每个方向上都不太常见。例如,将平均值降低到 7 会生成相对较小的树(或任何正在使用的树),但仍然可以使用较大的树,但并不常见。

以前的方法(伪代码)

  1. Loop from min to max
  2. Closer to average numbers are added to the list more times
  3. A random element is selected from the list,更接近平均值的元素被添加更多,因此它们更有可能被选中。

好吧,这就像把一堆糖果扔进袋子里,然后随机挑选一个。是的,慢。您对改进这一点有什么想法?

插图:(不完全准确,但你看到了这个想法) 在此处输入图像描述

注意:许多人建议使用钟形曲线,但问题是如何能够改变曲线的峰值以在这个意义上偏向一侧。

4

9 回答 9

12

我正在扩展生成 n 个随机数的想法,并取它们的平均值以获得钟形曲线效应。“紧密度”参数控制曲线的陡峭程度。

编辑:Central Limit Theorem支持对一组随机点求和以获得“正态”分布。使用偏置函数来影响特定方向的结果是一种常见的技术,但我不是那里的专家。

为了解决问题末尾的注释,我通过操纵“内部”随机数来扭曲曲线。在此示例中,我将其提高到您提供的指数。由于 Random 返回的值小于 1,因此将其提高到任何幂都不会超过 1。但是平均值偏向零,因为小于一的数字的正方形、立方体等甚至小于基数。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 的不同值进行了试验,生成了 0 到 99 之间的 100,000 个整数。这是分布的结果。

偏斜随机分布

我不确定峰值与 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, peak) 的直方图,图例中显示了各种峰值。我四舍五入得到 0 到 99 之间的整数,将紧密度设置为 5,并尝试了 20 到 80 之间的峰值。(在极端峰值时事情会变得不稳定,所以我把它省略了,并在代码中添加了警告。)你可以在它们应该在的地方看到山峰。

各种峰值,紧密度=5

接下来,我尝试将 Tightness 提高到 10...

在此处输入图像描述

分布更紧密,峰值仍在应有的位置。它也相当快!

于 2014-05-02T20:57:22.170 回答
6

这是实现此目的的简单方法。由于您已经有了详细说明如何生成正态分布的答案,并且有大量资源,我不会重复这一点。相反,我将参考我将调用的方法,该方法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

当然,正态分布不能保证保持在一个标准差内,因此您可能需要做两件事:

  • 添加一个额外的参数来控制分布的紧密程度
  • 如果您想要minmax成为硬限制,则必须对超出这些范围的值添加拒绝。

一个完整的方法,加上这两个附加项(int现在再次保持一切为 s),可能看起来像;

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版本),它不会是最漂亮的发行版,但它应该足以满足您的目的。

编辑

上面我说这个结果不是特别漂亮。为了扩展这一点,最明显的“丑陋”是中心点的不连续性,因为正态分布的峰值高度取决于其标准偏差。因此,您最终得到的发行版将如下所示:

原版

(对于最小 10、最大 100 和中心点 70,使用 3 的“紧密度”)

因此,虽然低于中心的值的概率等于上述概率,但结果将在一侧比另一侧更紧密地“聚集”在平均值周围。如果这对你来说太难看,或者你认为通过这样的分布生成特征的结果看起来太不自然,我们可以添加一个额外的修改,权衡中心的哪一侧被左侧范围的比例选择或中心右侧。将其添加到代码中(假设您可以访问Random我刚刚调用的 a 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;
}

作为比较,这将产生具有相同最小值、最大值、中心和紧密度的分布是:

更流畅的版本

正如您所看到的,这现在是频率连续的,以及一阶导数(给出平滑的峰值)。此版本相对于另一个版本的缺点是现在您更有可能在中心的一侧获得结果而不是另一侧。中心现在是模态平均值,而不是平均值。因此,您是喜欢更平滑的分布还是让中心成为分布的真实均值取决于您。

于 2014-05-02T03:09:01.327 回答
5

既然您正在寻找一个值在一个点周围的正态分布,在界限内,为什么不使用 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;
}
于 2013-09-15T00:56:20.687 回答
3

您在这里有两个选择:

  1. 总结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;
    
  2. 使用具有均值和标准差的实际正态分布。但是,这不能保证最小值或最大值。

    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;
    }
    
于 2013-10-30T20:19:40.060 回答
3

这是我的解决方案。MyRandom 类具有与 Next() 等效的函数,带有 3 个附加参数。centerspan表示期望的范围,retry是重试次数,每重试一次,在期望范围内生成数字的概率理论上应该增加 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;
    }
}
于 2014-04-28T09:10:28.983 回答
3

我会做这样的事情:

  1. 计算均匀分布的双精度
  2. 使用它,使用正态分布公式(如果我没记错的话,你称之为“反密度函数”?嗯,将[0,1]“返回”到累积概率的那个)或类似的计算期望值 - 例如您可以稍微调整正态分布,不仅取平均值和标准差/方差,而且取平均值和两个这样的值来处理最小值/最大值
  3. 四舍五入到整数,确保最小值、最大值等
于 2013-09-15T00:50:30.177 回答
2

有什么理由说分布实际上必须是钟形曲线?例如,使用:

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

于 2014-05-02T21:00:37.963 回答
1

您可以使用 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();
    }
}
于 2014-05-02T01:47:34.517 回答
0

不确定这是您想要的,但这是一种绘制随机数的方法,该随机数的分布从 to 到 to 均匀,min同时确保均值等于。avgavgmaxavg

假设来自 [min avg] 的平局概率 p 和来自 [avg max] 的概率 1-p。预期值为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]

概率图是分段常数,pminavg1-pavgmax。极端值不会受到惩罚。

于 2014-05-02T22:39:22.247 回答