問題描述
在很多時候——特別是在遊戲中——我們經常需要對某一事件進行隨機觸發處理,例如:攻擊有機率觸發暴擊,怪物有機率掉出裝備,武器有機率強化失敗。通常我們的做法就是,從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 = 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次(前一次觸發集中於最後而後一次觸發集中於最前)。
- 該文章的算法只是爲了方便讀者理解概念,不應該在實際工作中應用。