小样本大概率事件的正确处理方式 - 1. 概率的含义和误差产生的原因
2017-05-16 12:57
274 查看
问题描述
在很多时候——特别是在游戏中——我们经常需要对某一事件进行随机触发处理,例如:攻击有几率触发暴击,怪物有几率掉出装备,武器有几率强化失败。通常我们的做法就是,从0~1中取随机数,然后判断是否大于给出的几率(实际上大多数时候为了计算效率会用0~100的随机整数)。当我们从整个游戏世界上去统计这个随机事件时,无论这个概率是小概率还是大概率,得到的结果与我们的期望都不会有很大的差别。但是从单一玩家的角度,或者是某一短时间段的角度去统计这个事件时,对于结果的直观感受会有很大的波动性——换句话说,得到的实际概率往往并不是我们所想要的概率。
实际上从概率论的角度能给出非常合理的解释,期望方差标准差啥的一套公式概念拿出来,似乎就“原来如此”了。但是我们要的是直观的解释和合理的算法,而不是一堆晦涩难懂的公式概念。
问题分析
举个栗子:玩家在攻击时有30%的几率触发暴击。从游戏世界中统计一个大样本,得到的触发概率基本不会与预期有很大的差别。但是对于一个玩家十次攻击的结果来说呢?
从上图模拟能很明显看到,对于一个小样本模拟,其概率与我们的期望相差甚远。你完全能够脑补出玩家在心中反复念叨的“我擦,这人品”。事实上,0/10或者10/10这样的样本,在独立随机事件中是完全合乎逻辑的。初中课本上就出现过的概念:对于独立随机事件,任何一次事件都不会影响到另一次事件的发生。
煎蛋一菊花的解释就是:概率值的本质上是对一个样本中某个特定事件的统计计算结果,而并非对某一事件是否发生的约束条件。样本和事件是前提,概率才是结果,反过来从某一统计概率上去预测样本事件的发生情况,并不能得到我们所想要的结果(所以彩票预测和股票周四会涨啥的完全就是扯淡)。
从心理感官方面,如果是一个概率非常小的事件,我们并不会从小样本上去分析,千分之一的概率在一百次中出现一次以上的概率太小,而在一千次中没有一次出现,或者出现了两次,看起来都并没有什么奇怪。所以我们需要解决的,是“小样本大概率事件”在程序中的逻辑优化。
解决方案
首先,我们有一个随机概率P,要让其在样本数量S中体现出来,我们必须将事件触发的次数限制为P*S。也就是说,我们需要将S次样本大小为1的发生概率为P的独立重复随机事件,变成一次样本大小为S的,触发次数为P*S的事件,从而把该样本的统计概率约束在P。但是,作为一个合理的随机事件,“随机”是必须存在一定概率波动的,只是独立重复随机事件的“随机”并不可控,所以我们无法将波动控制在我们的可接受范围内。而对固定次数的事件做优化,给定一个波动值D,将触发次数变成P*S ±D中的随机数,那么就能够对该事件进行“可控随机化”了。
需求分析
初始化参数样本数量quantity;
期望次数expectation;
波动大小deviation;
公共接口
bool GetNext():下次事件是否触发;
void Reset():重新计算随机事件;
void Reset(int deviation):以给定的波动值重新计算随机事件;
逻辑流程
以expectation和deviation得出一个随机数作为样本中触发事件数量count;
从0到quantity中取count个随机数保存到selection[];
每次用GetNext()取结果时,样本编号加一(如果超过样本数量,则重新计算),判断该样本编号是否存在于selection[]中,存在则返回true;
代码编写(可略过不看)
逻辑优化:对于判断当前样本是否为触发点,即样本编号是否在selection中,可以对selection进行排序后进行判断;/* * Author: 熊哲 * CreateTime: 5/14/2017 2:20:04 PM * Description: * */ using System.Text; using UnityEngine; // 只用了unity引擎的Random,非unity环境改掉就行 public class RandomTrigger { public int quantity { get; private set; } public int expectation { get; private set; } public int deviation { get; private set; } public int sampleIndex { get; private set; } public int triggerIndex { get; private set; } public int[] triggers { get; private set; } public RandomTrigger(int quantity, int expectation, int deviation = 0) { this.quantity = quantity; this.expectation = expectation; this.deviation = deviation; Reset(); } public void Reset() { sampleIndex = 0; triggerIndex = 0; int count = Random.Range(expectation - deviation, expectation + deviation + 1); if (count < quantity) { triggers = RandomSelect(quantity, count); Sort(triggers, 0, triggers.Length); } else // 触发次数大于样本数量,必定每次都会返回true { triggers = new int[quantity]; } } public void Reset(int deviation) { this.deviation = deviation; Reset(); } public bool GetNext() { // 重新取样 if (sampleIndex >= quantity) Reset(); // 触发次数大于样本数或者是触发点则返回true; if (triggers.Length >= quantity || (triggerIndex < triggers.Length && sampleIndex == triggers[triggerIndex])) { sampleIndex++; triggerIndex++; return true; } else { sampleIndex++; return false; } } // 从0~amount中得到count个不重复的随机数 protected int[] RandomSelect(int amount, int count) { int[] array = new int[amount]; int[] result = < c182 span class="hljs-keyword">new int[count]; for (int i = 0; i < count; i++) { int left = amount - i; int random = Random.Range(0, left); if (array[random] > 0) { result[i] = array[random]; } else { result[i] = random; } if (array[left - 1] == 0) { array[random] = left - 1; } else { array[random] = array[left - 1]; } } return result; } // 快速排序算法 protected void Sort(int[] array, int left, int right) { if (left < right) { int low = left; int high = right - 1; int key = array[low]; while (low < high) { while (array[high] > key) high--; Swap(array, low, high); while (array[low] < key) low++; Swap(array, low, high); } Sort(array, left, high - 1); Sort(array, high + 1, right); } } // 数组元素交换 protected void Swap<T>(T[] array, int index1, int index2) { T temp = array[index1]; array[index1] = array[index2]; array[index2] = temp; } public override string ToString() { StringBuilder str = new StringBuilder(); for (int i = 0; i < triggers.Length; i++) { str.Append(triggers[i] + " "); } return str.ToString(); } }
结果验证
对于每次判断某概率是否发生,所得的概率与取样数量的关系曲线会在期望值上波动,并且波动幅度并不受控制,误差也很大。而修正后的算法,波动会有所减小并且可控,如果波动值为0,每当样本数量是某一值的倍数时,概率会修正到期望值(详见小样本大概率事件的正确处理方式 - 2. 结果分析)。
注意事项
任何概率性事件都可以使用该方法进行概率约束,并不仅限于小样本事件。在该约束下3/10与6/20会稍有不同,最明显的区别是取样20,前者连续触发最高为6次,而后者最高为12次(前一次触发集中于最后而后一次触发集中于最前)。
该文章的算法只是为了方便读者理解概念,不应该在实际工作中应用。
小样本大概率事件的正确处理方式 - 2. 结果分析
小样本大概率事件的正确处理方式 - 3. 实际使用
相关文章推荐
- 小样本大概率事件的正确处理方式 - 2. 结果分析
- 小样本大概率事件的正确处理方式 - 3. 实际使用
- 正确处理WPF中Slider值改变事件的方式
- java Collecttion的fail-fast的产生原因和处理方式 以及java中fail-fast 和 fail-safe的区别
- Content is not allowed in prolog异常产生原因及处理方式
- Content is not allowed in prolog异常产生原因及处理方式
- 关于java.lang.NoSuchMethodError的分析,产生的原因及处理方式
- 关闭Socket的正确方式及ECONNRESET,WSAECONNRESET产生的原因
- VBScript事件处理方式
- SQL*Net message from client 事件产生的原因分析
- MS DTC 无法正确处理DC升级/降级事件。MS DTC 将继续运行并使用现有的安全设置。
- Session丢失的原因以及处理方式
- wince下强制使用重载的方式来解决窗口打开、关闭时的事件处理
- MS DTC 无法正确处理 DC 升级/降级事件的解决
- 以编程方式向 ASP.NET 控件添加客户端事件处理程序--TextBox.Attributes.Add()
- 内存碎片产生原因及处理
- Java事件处理机制-事件监听器的四种实现方式
- MS DTC 无法正确处理 DC 升级/降级事件的解决
- MSDTC无法正确处理DC升级/降级事件
- ASP.NET 截取的、在处理请求期间产生的事件