抽奖算法-指定概率的随机

 

抽奖模型

普通概率模型

普通概率模型是最常用的一种模型,但是在游戏运营过程中的确发现很多小白玩家不能正确理解——他们认为中奖率 10% 的设定等同于抽 10 次肯定会中一次。这显然是错误的,普通概率模型的中奖抽奖次数是基于正态分布的,而且每次抽奖的事件是独立的,并不会因为你前面抽了 9 次没中奖,第十次就一定能中奖。

虽然在大量的统计中,两次中奖的平均间隔是 10 次,但是还有一个有趣的数据是连续 10 次都没中奖的概率约为 (1-10%)^10 ~= 34.8% 可不小呢。

此外「标准差」是一个很有意思的数据,经过模拟统计,10% 中奖率得到的标准差为 9.62 ——也就是说绝大分部人经过 10 ± 9.62 次抽奖即能中奖,运气再背抽 20 次也差不多能得到奖励了。

这种概率模型能非常准确地实现策划的需求,但是会惹来一些小白玩家的差评——为什么你说中奖率是 10% 但是我抽了 20 次还没有中奖!然后给你打个一星。所以很多游戏运营商为了顾及玩家的体验,会对普通概率模型进行修订,增设一些保底抽奖次数,例如每第 10 次固定中奖(10,20,30...)

对于这种做法,我暂不于评价。但是让我们看看如果硬生生地加入固定中奖的设定,会给数值带来什么变化吧。

固定中奖模型

每次抽奖中奖率依旧为 10% ,但每第十次抽奖必中。

这时候玩家得到的抽奖体验是:10 次抽奖肯定能中奖,而且不止中一次,爽暴了是不是。实际期望高达 19% 这远远超出策划 10% 的预期。所以策划琢磨着不能便宜了玩家,只能把中奖率调低。但是这会导致中奖集中在每 10 次附近,抽奖的乐趣几近丧失。

这样看来,固定中奖模型是否真的无药可救?其实还是有可以优化的地方。

计数器模型

每次抽奖中奖率依旧为 10% ,若连续 9 次未中奖,下一次抽奖必中奖。

这个需求看起来和上面好像没什么不同,但是保底的条件不再是每第 10 次,而是发生在每连续 9 次未中奖后。也就是说计数器会在每次中奖后清 0 重计。

 

随机步长累加模型

也是一种保底中奖模型,只不过去掉了独立随机事件,并把计数增长改为随机量,最终在累计超过阈值时得奖。这种模型如果有个较大的阈值和较小的步长下限,还可以起到让玩家在头几次抽奖必然不中(大)奖的效果。另外在这种模型下,计数器甚至可以对玩家可见,让看玩家看到进度和目标,感受到奖励是可达的、近在眼前的。

 

 

 

o9vrr

 

抽奖算法

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/// <summary>
/// 抽奖
/// </summary>
public class Prize
{
    /// <summary>
    /// 奖品关键字
    /// </summary>
    public string Key { get; set; }
 
    /// <summary>
    /// 权重/数量
    /// </summary>
    public int Poll { get; set; }
 
 
    /// <summary>
    /// 中奖区间
    /// </summary>
    class Area
    {
        /// <summary>
        /// 奖品关键字
        /// </summary>
        public string Key { get; set; }
 
        /// <summary>
        /// 开始索引位置
        /// </summary>
        public int Start { get; set; }
 
        /// <summary>
        /// 截止索引位置
        /// </summary>
        public int Over { get; set; }
    }
 
    /// <summary>
    /// 随机种子
    /// </summary>
    static Random Rand = new Random((int)DateTime.Now.Ticks);
 
 
    /// <summary>
    /// 轮盘抽奖,权重值(在轮盘中占的面积大小)为中奖几率
    /// </summary>
    /// <param name="prizeList">礼品列表(如果不是百分百中奖则轮空需要加入到列表里面)</param>
    /// <returns></returns>
    public static string Roulette(List<Prize> prizeList)
    {
        if (prizeList == null || prizeList.Count == 0) return string.Empty;
        if (prizeList.Any(x => x.Poll < 1)) throw new ArgumentOutOfRangeException("poll权重值不能小于1");
        if (prizeList.Count == 1) return prizeList[0].Key; //只有一种礼品
 
        Int32 total = prizeList.Sum(x => x.Poll); //权重和              
        if (total > 1000) throw new ArgumentOutOfRangeException("poll权重和不能大于1000"); //数组存储空间的限制。最多一千种奖品(及每种奖品的权重值都是1)
 
        List<int> speed = new List<int>(); //随机种子
        for (int i = 0; i < total; i++) speed.Add(i);
 
        int pos = 0;
        Dictionary<int, string> box = new Dictionary<int, string>();
        foreach (Prize p in prizeList)
        {
            for (int c = 0; c < p.Poll; c++) //权重越大所占的面积份数就越多
            {
                pos = Prize.Rand.Next(speed.Count); //取随机种子坐标
                box[speed[pos]] = p.Key; //乱序 礼品放入索引是speed[pos]的箱子里面
                speed.RemoveAt(pos); //移除已抽取的箱子索引号
            }
        }
        return box[Prize.Rand.Next(total)];
    }
 
    /// <summary>
    /// 奖盒抽奖,每个参与者对应一个奖盒,多少人参与就有多少奖盒
    /// </summary>
    /// <param name="prizeList">礼品列表</param>
    /// <param name="peopleCount">参与人数</param>
    /// <returns></returns>
    public static string LunkyBox(List<Prize> prizeList, int peopleCount)
    {
        if (prizeList == null || prizeList.Count == 0) return string.Empty;
        if (prizeList.Any(x => x.Poll < 1)) throw new ArgumentOutOfRangeException("poll礼品数量不能小于1个");
        if (peopleCount < 1) throw new ArgumentOutOfRangeException("参数人数不能小于1人");
        if (prizeList.Count == 1 && peopleCount <= prizeList[0].Poll) return prizeList[0].Key; //只有一种礼品且礼品数量大于等于参与人数
 
        int pos = 0;
        List<Area> box = new List<Area>();
        foreach (Prize p in prizeList)
        {
            box.Add(new Area() { Key = p.Key, Start = pos, Over = pos + p.Poll }); //把礼品放入奖盒区间
            pos = pos + p.Poll;
        }
 
        int total = prizeList.Sum(x => x.Poll); //礼品总数
        int speed = Math.Max(total, peopleCount); //取礼品总数和参数总人数中的最大值
 
        pos = Prize.Rand.Next(speed);
        Area a = box.FirstOrDefault(x => pos >= x.Start && pos < x.Over); //查找索引在奖盒中对应礼品的位置
 
        return a == null ? string.Empty : a.Key;
    }
 
}
 
 
/*
List<Prize> prizes = new List<Prize>();
prizes.Add(new Prize() { Key = "电脑", Poll = 1 });
prizes.Add(new Prize() { Key = "机柜", Poll = 2 });
prizes.Add(new Prize() { Key = "鼠标", Poll = 3 });
                         
 
string lp1 = Prize.LunkyBox(prizes, 6);
Console.WriteLine(lp1);
 
 
prizes.Add(new Prize() { Key = "谢谢惠顾", Poll = 5 });
string lp2 = Prize.Roulette(prizes);     
Console.WriteLine(lp2);
*/

 

 

参考

Return random `list` item by its `weight`

不同概率模型的抽奖体验

抽奖算法

一个简单抽奖算法的实现以及如何预防超中

posted @   霍旭东  阅读(24754)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
点击右上角即可分享
微信分享提示