如何让稀有物品掉落率低、普通物品掉落率高?加权随机算法提供了完美的概率控制方案。
问题背景
在很多应用场景中,我们需要按照预设的概率分布来进行随机选择,而不是简单的均匀随机:
- 游戏开发:稀有装备1%概率,普通装备50%概率
- 推荐系统:热门商品推荐概率高,冷门商品概率低
- 负载均衡:高性能服务器分配更多请求
- 广告投放:高价值广告展示频率更高
- 抽奖活动:一等奖概率0.1%,参与奖概率90%
简单随机选择无法满足这些需求,而加权随机(Weighted Random) 算法提供了完美的解决方案。
算法原理
加权随机核心思想是:根据权重构建概率区间,随机数落在哪个区间就选择对应的元素。
基本概念
假设我们有三个物品和对应的权重:
物品A: 权重 5 物品B: 权重 3 物品C: 权重 2
|
总权重 = 5 + 3 + 2 = 10
每个物品的概率分布:
物品A: 5/10 = 50 物品B: 3/10 = 30 物品C: 2/10 = 20
|
实现方法
方法一:线性扫描(简单直观)
<?php
class WeightedRandom {
public static function linearSelect($items) { $totalWeight = array_sum($items); $random = mt_rand(1, $totalWeight); $current = 0; foreach ($items as $item => $weight) { $current += $weight; if ($random <= $current) { return $item; } } return null; } }
$lootTable = [ '普通药水' => 50, '稀有装备' => 30, '传奇武器' => 15, '神器' => 5 ];
echo "=== 游戏物品掉落模拟 ===\n"; for ($i = 0; $i < 10; $i++) { $loot = WeightedRandom::linearSelect($lootTable); echo "第 " . ($i + 1) . " 次掉落: " . $loot . "\n"; }
?>
|
方法二:别名方法(高效算法)
对于需要频繁抽样的大规模场景,可以使用更高效的别名方法:
<?php
class AliasMethod { private $prob; private $alias; private $n;
public function __construct($weights) { $this->n = count($weights); $this->prob = array_fill(0, $this->n, 0); $this->alias = array_fill(0, $this->n, 0); $this->buildAliasTable($weights); }
private function buildAliasTable($weights) { $total = array_sum($weights); $prob = array_map(function($w) use ($total) { return $w * $this->n / $total; }, $weights); $small = []; $large = []; for ($i = 0; $i < $this->n; $i++) { if ($prob[$i] < 1) { $small[] = $i; } else { $large[] = $i; } } while (!empty($small) && !empty($large)) { $s = array_pop($small); $l = array_pop($large); $this->prob[$s] = $prob[$s]; $this->alias[$s] = $l; $prob[$l] = ($prob[$l] + $prob[$s]) - 1; if ($prob[$l] < 1) { $small[] = $l; } else { $large[] = $l; } } while (!empty($large)) { $l = array_pop($large); $this->prob[$l] = 1; } while (!empty($small)) { $s = array_pop($small); $this->prob[$s] = 1; } }
public function next() { $i = mt_rand(0, $this->n - 1); if (mt_rand() / mt_getrandmax() < $this->prob[$i]) { return $i; } else { return $this->alias[$i]; } } }
class EfficientWeightedRandom {
public static function aliasSelect($items) { static $aliasMethods = []; $key = md5(serialize($items)); if (!isset($aliasMethods[$key])) { $aliasMethods[$key] = new AliasMethod(array_values($items)); } $index = $aliasMethods[$key]->next(); $itemNames = array_keys($items); return $itemNames[$index]; } }
?>
|
方法三:二分查找(性能均衡)
<?php
class BinarySearchWeightedRandom { private $prefixSums; private $items; private $totalWeight;
public function __construct($items) { $this->items = array_keys($items); $this->totalWeight = array_sum($items); $this->prefixSums = []; $sum = 0; foreach ($items as $weight) { $sum += $weight; $this->prefixSums[] = $sum; } }
public function select() { $target = mt_rand(1, $this->totalWeight); $left = 0; $right = count($this->prefixSums) - 1; while ($left < $right) { $mid = $left + intval(($right - $left) / 2); if ($this->prefixSums[$mid] < $target) { $left = $mid + 1; } else { $right = $mid; } } return $this->items[$left]; } }
?>
|
完整应用示例
<?php
class WeightedRandomApplication {
public static function gameLootSystem() { echo "=== 游戏掉落系统 ===\n"; $monsterLootTable = [ '金币' => 400, '普通装备' => 300, '稀有装备' => 200, '史诗装备' => 80, '传奇装备' => 18, '神器' => 2 ]; $drops = []; for ($i = 0; $i < 1000; $i++) { $loot = WeightedRandom::linearSelect($monsterLootTable); if (!isset($drops[$loot])) { $drops[$loot] = 0; } $drops[$loot]++; } echo "1000次击杀掉落统计:\n"; foreach ($drops as $item => $count) { $rate = ($count / 1000) * 100; $expected = ($monsterLootTable[$item] / array_sum($monsterLootTable)) * 100; echo sprintf("%-10s: 实际%.2f%% (预期%.2f%%)\n", $item, $rate, $expected); } }
public static function recommendationSystem() { echo "\n=== 商品推荐系统 ===\n"; $products = [ '热门商品A' => 100, '热门商品B' => 80, '普通商品C' => 30, '普通商品D' => 25, '冷门商品E' => 10, '新品F' => 5 ]; echo "今日推荐商品:\n"; for ($i = 0; $i < 5; $i++) { $recommended = WeightedRandom::linearSelect($products); echo "推荐位 " . ($i + 1) . ": " . $recommended . "\n"; } }
public static function lotterySystem() { echo "\n=== 抽奖活动系统 ===\n"; $prizes = [ '一等奖:iPhone' => 1, '二等奖:AirPods' => 5, '三等奖:优惠券' => 50, '参与奖:谢谢参与' => 944 ]; echo "抽奖结果:\n"; $results = []; for ($i = 0; $i < 100; $i++) { $prize = WeightedRandom::linearSelect($prizes); if (!isset($results[$prize])) { $results[$prize] = 0; } $results[$prize]++; } foreach ($results as $prize => $count) { echo $prize . ": " . $count . "次\n"; } }
public static function performanceTest() { echo "\n=== 性能测试 ===\n"; $largeWeightTable = []; for ($i = 0; $i < 1000; $i++) { $largeWeightTable["item_$i"] = mt_rand(1, 100); } $start = microtime(true); for ($i = 0; $i < 10000; $i++) { WeightedRandom::linearSelect($largeWeightTable); } $linearTime = microtime(true) - $start; $bsRandom = new BinarySearchWeightedRandom($largeWeightTable); $start = microtime(true); for ($i = 0; $i < 10000; $i++) { $bsRandom->select(); } $binaryTime = microtime(true) - $start; echo "线性扫描: " . number_format($linearTime, 4) . " 秒\n"; echo "二分查找: " . number_format($binaryTime, 4) . " 秒\n"; echo "性能提升: " . number_format(($linearTime - $binaryTime) / $linearTime * 100, 2) . "%\n"; } }
WeightedRandomApplication::gameLootSystem(); WeightedRandomApplication::recommendationSystem(); WeightedRandomApplication::lotterySystem(); WeightedRandomApplication::performanceTest();
?>
|
算法对比
| 方法 |
时间复杂度 |
空间复杂度 |
适用场景 |
| 线性扫描 |
O(n) |
O(1) |
权重项少,简单应用 |
| 二分查找 |
O(log n) |
O(n) |
权重项多,性能要求高 |
| 别名方法 |
O(1) |
O(n) |
超大规模,频繁抽样 |
最佳实践建议
1. 权重设计原则
$goodWeights = [ '常见物品' => 70, '稀有物品' => 25, '史诗物品' => 4, '传说物品' => 1 ];
$badWeights = [ '物品A' => 0.0001, '物品B' => 999999, ];
|
2. 动态权重调整
class DynamicWeightedRandom { private $baseWeights; private $dynamicFactors; public function __construct($baseWeights) { $this->baseWeights = $baseWeights; $this->dynamicFactors = array_fill_keys(array_keys($baseWeights), 1); }
public function adjustWeight($item, $factor) { if (isset($this->dynamicFactors[$item])) { $this->dynamicFactors[$item] *= $factor; } }
public function getCurrentWeights() { $current = []; foreach ($this->baseWeights as $item => $weight) { $current[$item] = $weight * $this->dynamicFactors[$item]; } return $current; }
public function select() { $currentWeights = $this->getCurrentWeights(); return WeightedRandom::linearSelect($currentWeights); } }
|
总结
加权随机算法是概率控制的重要工具,具有以下特点:
核心优势
- 精确控制概率:严格按照权重分配选择概率
- 灵活配置:支持动态调整权重
- 性能可控:多种实现满足不同性能需求
- 应用广泛:适用于游戏、推荐、抽奖等场景
选择建议
- 小规模数据:使用线性扫描,简单直观
- 大规模数据:使用二分查找,性能均衡
- 超频繁抽样:使用别名方法,极致性能
加权随机算法让概率控制变得简单而精确,是每个开发者都应该掌握的重要算法工具。