首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 實現加權隨機抽獎算法:從原理到生產級代碼

     編輯:PHP 綜合知識

加權隨機抽獎是電商促銷、遊戲掉寶、廣告投放的核心邏輯。本文從數學原理推導到面向物件的生產級 PHP 實作,涵蓋邊界條件、擴展性與效能優化。

一、核心算法原理

權重轉區間法

獎品權重:[A:5, B:6, C:7, D:2],總權重 N = 20

  1. 計算累積權重區間:
    • A: 1-5
    • B: 6-11
    • C: 12-18
    • D: 19-20
  2. 生成隨機數 M ∈ [1, N]
  3. 落在哪個區間,中哪個獎品

時間複雜度:建立區間 O(n)、查找 O(n) 或 O(log n) (二分查找)

二、面向物件實作 (生產級)

核心類別設計

id = $id;
        $this->weight = max(0, $weight);
        $this->name = $name ?: "Prize #$id";
    }

    public function setRange(int $start, int $end): void {
        $this->rangeStart = $start;
        $this->rangeEnd = $end;
    }

    public function inRange(int $num): bool {
        return $num >= $this->rangeStart && $num <= $this->rangeEnd;
    }

    public function getId(): int { return $this->id; }
    public function getWeight(): int { return $this->weight; }
    public function getName(): string { return $this->name; }
}

/**
 * 獎品池:管理獎品集合、支援動態增刪改
 */
class PrizePool implements IteratorAggregate, Countable {
    private array $items = [];

    public function add(Prize $prize): self {
        $this->items[$prize->getId()] = $prize;
        return $this;
    }

    public function remove(int $id): self {
        unset($this->items[$id]);
        return $this;
    }

    public function update(Prize $prize): self {
        if (isset($this->items[$prize->getId()])) {
            $this->items[$prize->getId()] = $prize;
        }
        return $this;
    }

    public function getVisible(): array {
        return array_filter($this->items, fn($p) => $p->getWeight() > 0);
    }

    public function count(): int { return count($this->items); }
    public function getIterator(): Traversable { return new ArrayIterator($this->items); }
}

/**
 * 抽獎引擎:核心算法封裝
 */
class LotteryEngine {
    private PrizePool $pool;

    public function __construct(PrizePool $pool) {
        $this->pool = $pool;
    }

    public function draw(): ?Prize {
        $items = $this->pool->getVisible();
        if (empty($items)) throw new RuntimeException('No visible prizes');

        // 建立累積區間
        $sum = 0;
        foreach ($items as $prize) {
            $start = $sum + 1;
            $sum += $prize->getWeight();
            $prize->setRange($start, $sum);
        }

        $rand = $this->randomInt(1, $sum);

        // 線性查找 (獎品數少時足夠)
        foreach ($items as $prize) {
            if ($prize->inRange($rand)) return $prize;
        }
        return null; // 理論不會到達
    }

    // 可替換為 random_int (PHP 7+) 或 openssl_random_pseudo_bytes
    protected function randomInt(int $min, int $max): int {
        return random_int($min, $max);
    }
}

使用範例

try {
    $pool = new PrizePool();
    $pool->add(new Prize(1, 5, '一等獎'))
         ->add(new Prize(2, 10, '二等獎'))
         ->add(new Prize(3, 20, '三等獎'))
         ->add(new Prize(4, 65, '參與獎')); // 總權重 100

    $engine = new LotteryEngine($pool);
    
    // 模擬 10000 次驗證機率
    $stats = [];
    for ($i = 0; $i < 10000; $i++) {
        $prize = $engine->draw();
        $stats[$prize->getId()] = ($stats[$prize->getId()] ?? 0) + 1;
    }
    print_r($stats);
    // 輸出接近: [1=>500, 2=>1000, 3=>2000, 4=>6500]
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
}

三、進階優化:二分查找 O(log n)

獎品數超過 100 時,線性查找成瓶頸。改用累積權重陣列 + 二分查找:

class OptimizedLotteryEngine extends LotteryEngine {
    public function draw(): ?Prize {
        $items = $this->pool->getVisible();
        if (empty($items)) return null;

        $cumulative = [];
        $sum = 0;
        foreach ($items as $prize) {
            $sum += $prize->getWeight();
            $cumulative[] = [$sum, $prize]; // [累積權重, 獎品物件]
        }

        $rand = $this->randomInt(1, $sum);

        // 二分查找第一個累積權重 >= rand 的索引
        $low = 0;
        $high = count($cumulative) - 1;
        while ($low < $high) {
            $mid = (($low + $high) >> 1);
            if ($cumulative[$mid][0] >= $rand) {
                $high = $mid;
            } else {
                $low = $mid + 1;
            }
        }
        return $cumulative[$low][1];
    }
}

四、業務場景擴展

1. 保底機制 (Pity System)

class PityLotteryEngine extends OptimizedLotteryEngine {
    private array $pityCounters = []; // user_id => [prize_id => count]
    private array $pityRules = [];    // prize_id => max_attempts

    public function setPityRule(int $prizeId, int $maxAttempts): void {
        $this->pityRules[$prizeId] = $maxAttempts;
    }

    public function draw(int $userId): Prize {
        // 檢查保底
        foreach ($this->pityRules as $pid => $max) {
            $count = $this->pityCounters[$userId][$pid] ?? 0;
            if ($count >= $max - 1) {
                // 必中
                $this->pityCounters[$userId][$pid] = 0;
                return $this->pool->getVisible()[$pid] ?? $this->draw($userId);
            }
        }
        $prize = parent::draw();
        // 更新計數器
        if ($prize) {
            $this->pityCounters[$userId][$prize->getId()] = 
                ($this->pityCounters[$userId][$prize->getId()] ?? 0) + 1;
        }
        return $prize;
    }
}

2. 每日/每用戶限制

// Redis 實現分散式限流
$redis->eval("
    local key = KEYS[1]
    local limit = tonumber(ARGV[1])
    local current = redis.call('INCR', key)
    if current == 1 then redis.call('EXPIRE', key, 86400) end
    return current
", 1, "lottery:user:$userId:daily", 10); // 每日 10 次

3. 防刷/風控整合

  • IP 頻率限制
  • 裝置指紋識別
  • 行為分析 (點擊間隔、軌跡)
  • 中獎結果異步寫入、對帳

五、測試與驗證

// 統計檢定:卡方檢驗驗證機率分布
function chiSquareTest(array $observed, array $expected): float {
    $chi2 = 0;
    foreach ($observed as $k => $o) {
        $e = $expected[$k];
        $chi2 += (($o - $e) ** 2) / $e;
    }
    return $chi2;
}

// 100萬次模擬,p-value > 0.05 表示分布符合預期
$engine = new OptimizedLotteryEngine($pool);
$results = [];
for ($i = 0; $i < 1000000; $i++) {
    $results[$engine->draw()->getId()]++;
}
// 驗證...

六、部署檢查清單

項目 標準
隨機數來源random_int() (PHP 7+) / openssl_random_pseudo_bytes()
權重總和溢位使用 bcmath 或 gmp 處理大數
併發安全Redis Lua 腳本 / 資料庫樂觀鎖
審計日誌每次抽獎記錄 user_id, prize_id, rand, timestamp
可驗證公平性種子公開、鏈上驗證 (區塊鏈場景)

從簡單的區間法到企業級抽獎引擎,核心算法不變,關鍵在於邊界處理、擴展性設計、可審計性。掌握這套模式,應對各類隨機分發場景遊刃有餘。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved