網站活動常需按機率發放獎品(如 1% 中 iPad、50% 中 Q幣)。本文詳細解析兩種經典 PHP 概率算法,並提供完整的獎品配置與抽獎邏輯實作,含大獎保護機制與前端整合建議。
<?php
/**
* 全機率計算 - 陣列展開法
* @param array $ps 機率陣列 ['獎項A' => 0.5, '獎項B' => 0.3, '獎項C' => 0.2]
* @return string 中獎鍵名
*/
function random($ps) {
static $cache = [];
// 建立快取鍵 (序列化陣列產生 MD5)
$key = md5(serialize($ps));
if (!isset($cache[$key])) {
$max = array_sum($ps); // 總權重
$pool = [];
// 按權重比例填充池子 (放大 10000 倍精度)
foreach ($ps as $k => $v) {
$count = intval($v / $max * 10000);
for ($i = 0; $i < $count; $i++) {
$pool[] = $k;
}
}
$cache[$key] = $pool;
}
// 隨機取一個
return $cache[$key][mt_rand(0, count($cache[$key]) - 1)];
}
優點:取樣極快 O(1)、適合固定機率重複抽樣
缺點:記憶體佔用大 (權重精度高時)、機率變動需重建快取
<?php
/**
* 經典加權隨機選擇 - 累積機率法
* @param array $proArr 權重陣列 ['獎項A' => 100, '獎項B' => 200, '獎項C' => 300]
* @return string 中獎鍵名
*/
function get_rand($proArr) {
$result = '';
$proSum = array_sum($proArr); // 總權重
foreach ($proArr as $key => $proCur) {
$randNum = mt_rand(1, $proSum); // 1 到總權重間隨機
if ($randNum <= $proCur) {
$result = $key;
break; // 命中,結束循環
} else {
$proSum -= $proCur; // 未命中,縮減權重範圍
}
}
unset($proArr);
return $result;
}
原理:將權重視為連續數軸區間,隨機落點落在哪個區間即中該獎項
優點:O(n) 時間、O(1) 空間、支援動態權重、大數據量極高效
缺點:權重項目極多 (萬級) 時循環較慢,可優化為二分搜尋
<?php
/**
* 抽獎核心類
*/
class Lottery {
private $prizes = [];
private $remaining = [];
public function __construct(array $prizes) {
// 獎品格式: ['id'=>1, 'name'=>'iPad', 'weight'=>1, 'total'=>10, 'won'=>0]
$this->prizes = $prizes;
$this->reset();
}
public function reset() {
$this->remaining = [];
foreach ($this->prizes as $p) {
$this->remaining[$p['id']] = [
'name' => $p['name'],
'weight' => $p['weight'],
'left' => $p['total'] - $p['won']
];
}
}
/**
* 執行抽獎
* @return array ['won'=>中獎獎品, 'remaining'=>未中獎列表]
*/
public function draw() {
// 建構當前可用獎品權重陣列
$weights = [];
foreach ($this->remaining as $id => $info) {
if ($info['left'] > 0) {
$weights[$id] = $info['weight'];
}
}
if (empty($weights)) {
return ['won' => '獎品已發完', 'remaining' => []];
}
// 執行機率抽選
$won_id = get_rand($weights);
$won_prize = $this->remaining[$won_id]['name'];
// 更新庫存
$this->remaining[$won_id]['left']--;
// 整理未中獎列表 (打亂順序)
$remaining_names = [];
foreach ($this->remaining as $id => $info) {
if ($id != $won_id && $info['left'] > 0) {
$remaining_names[] = $info['name'];
}
}
shuffle($remaining_names);
return [
'won' => $won_prize,
'remaining' => $remaining_names
];
}
/**
* 獲取當前獎品狀態
*/
public function getStatus() {
return $this->remaining;
}
}
// ===== 使用範例 =====
$prize_config = [
['id' => 1, 'name' => '平板電腦', 'weight' => 1, 'total' => 2, 'won' => 0],
['id' => 2, 'name' => '數碼相機', 'weight' => 5, 'total' => 5, 'won' => 0],
['id' => 3, 'name' => '音箱設備', 'weight' => 10, 'total' => 20, 'won' => 0],
['id' => 4, 'name' => '4G 優盤', 'weight' => 12, 'total' => 50, 'won' => 0],
['id' => 5, 'name' => '10 Q幣', 'weight' => 22, 'total' => 200,'won' => 0],
['id' => 6, 'name' => '下次沒准就能中哦', 'weight' => 50, 'total' => 9999, 'won' => 0],
];
$lottery = new Lottery($prize_config);
// 模擬 1000 次抽獎統計
$stats = [];
for ($i = 0; $i < 1000; $i++) {
$result = $lottery->draw();
$stats[$result['won']] = ($stats[$result['won']] ?? 0) + 1;
}
echo "統計結果:\n";
foreach ($stats as $prize => $count) {
echo "$prize: $count 次 (" . ($count/10) . "%)\n";
}
| 權重總和 | 權重值 1 的機率 | 適用場景 |
|---|---|---|
| 100 | 1% | 百分比直觀設定 |
| 1000 | 0.1% | 千分位精度 |
| 10000 | 0.01% | 萬分位精度 (大型活動) |
| 1000000 | 0.0001% | 極低機率大獎 |
<?php
// 在 draw() 方法前加入庫存檢查
public function drawWithProtection() {
// 1. 檢查大獎是否達上限
foreach ($this->remaining as $id => $info) {
if ($info['left'] <= 0) {
unset($this->remaining[$id]); // 直接移除已發完獎品
}
}
// 2. 如果只剩安慰獎,直接返回
if (count($this->remaining) === 1) {
$only = array_values($this->remaining)[0];
$only['left']--;
return ['won' => $only['name'], 'remaining' => []];
}
return $this->draw();
}
<!-- HTML -->
<script>
function startDraw() {
const btn = document.getElementById('draw-btn');
btn.disabled = true;
btn.textContent = '抽獎中...';
fetch('/api/lottery/draw.php', {method: 'POST'})
.then(res => res.json())
.then(data => {
document.getElementById('result').innerHTML =
`恭喜獲得:${data.won}`;
btn.disabled = false;
btn.textContent = '再試一次';
})
.catch(err => {
alert('網路錯誤');
btn.disabled = false;
btn.textContent = '開始抽獎';
});
}
</script>
若主辦方將大獎權重設為 0,則該獎項 永遠無法中獎。某些活動甚至在前端顯示「大獎就在旁邊」的煙霧彈獎項,實際程式邏輯早已決定結果。理解算法原理,才能看清遊戲規則。
<?php
function get_rand_binary($proArr) {
// 建構累積權重陣列
$prefix = [];
$sum = 0;
foreach ($proArr as $k => $v) {
$sum += $v;
$prefix[$k] = $sum;
}
$rand = mt_rand(1, $sum);
// 二分搜尋
$keys = array_keys($prefix);
$low = 0;
$high = count($keys) - 1;
while ($low <= $high) {
$mid = (($low + $high) >> 1);
$key = $keys[$mid];
if ($prefix[$key] >= $rand) {
$high = $mid - 1;
} else {
$low = $mid + 1;
}
}
return $keys[$low] ?? array_pop($keys);
}