<?php
/**
* 与えられたアイテムの累積重みを計算します。
*
* @param array<int|float> $items アイテムとその重みを格納した配列。キーにアイテム、値に重み
* @return array{0: array<int|float>, 1: int|float} 累積重みの配列と総重み。累積重みの配列はキーにアイテム、値に累積重み
*/
function prepare_cumulative_weights(array $items): array
{
// 累積重みの配列を初期化
$cumulative_weights = [];
// 重みの総和を初期化
$total = 0.0;
// 各アイテムに対して累積重みを計算
foreach ($items as $item => $weight) {
$total += $weight;
$cumulative_weights[$item] = $total;
}
// 累積重みの配列と総重みを返す
return [$cumulative_weights, $total];
}
/**
* 累積重みを基にランダムにアイテムを選択します。
*
* @param array<int|float> $cumulative_weights 累積重みの配列
* @param float|int $total 総重み
* @param string $algo アルゴリズム名。binary_search, linear_search
* @return float|int|string 選択されたアイテムのキー
*/
function weighted_random_pick(array $cumulative_weights, float|int $total, string $algo = 'binary_search'): float|int|string
{
// ランダムな数を生成
$num = mt_rand() / mt_getrandmax() * $total;
// 生成された数に基づいてアイテムを探索して選択
return $algo($cumulative_weights, $num);
}
// 何度も参照する値のキャッシュ置き場
class WeightRandomCacheStorage {
public static array|null $arrayValues;
public static array|null $arrayKeys;
public static function clear(): void
{
self::$arrayValues = null;
self::$arrayKeys = null;
}
}
/**
* 二分探索を行い、指定した値以上の最初の要素を見つけます。
*
* @param array<float|int> $array 検索対象の配列。昇順で累積重みが値に入っている値
* @param float|int|string $value 検索する値
* @return float|int|string 指定した値以上の最初の要素
*/
function binary_search(array $array, float|int|string $value): float|int|string
{
WeightRandomCacheStorage::$arrayValues ??= array_values($array);
WeightRandomCacheStorage::$arrayKeys ??= array_keys($array);
$left = 0;
$right = count(WeightRandomCacheStorage::$arrayValues) - 1;
// 二分探索
while ($left <= $right) {
$middle = floor(($left + $right) / 2);
if (WeightRandomCacheStorage::$arrayValues[$middle] == $value) {
return $middle;
}
if (WeightRandomCacheStorage::$arrayValues[$middle] < $value) {
$left = $middle + 1;
} else {
$right = $middle - 1;
}
}
// $valueを超える最初の要素の値を返す
return WeightRandomCacheStorage::$arrayKeys[$left];
}
/**
* 線形探索を行い、指定した値以上の最初の要素を見つけます。
*
* @param array<float|int> $array 検索対象の配列。昇順で累積重みが値に入っている値
* @param float|int|string $value 検索する値
* @return float|int|string 指定した値以上の最初の要素
*/
function linear_search(array $array, float|int|string $value): float|int|string
{
global $arrayV, $arrayK;
$arrayV ??= array_values($array);
$arrayK ??= array_keys($array);
// 線形探索
for ($i = 0; $i < count($arrayV); ++$i) {
if ($arrayV[$i] >= $value) {
return $arrayK[$i]; // $value以上の最初の要素のキーを返す
}
}
return $arrayK[$i]; // 指定された値以上の要素が見つからない場合
}
/**
* 与えられた回数だけアイテムをランダムに選択します。
*
* @param array<int|float> $items アイテムとその重みを格納した配列。キーにアイテム、値に重み
* @param int $count ランダムに選択する回数
* @param string|null $algo アルゴリズム名。binary_search, linear_search
* @return array<int> 選択された各アイテムの数を格納した配列。キーにアイテム、値に選択された回数
*/
function some_weighted_random_pick(array $items, int $count, string|null $algo = null): array
{
if(!isset($algo)){
// 要素が少ないなら線形探索、多いなら二分探索
$algo = count($items) < 50 ? 'linear_search' : 'binary_search';
}
// 累積重みと総重みを計算
list($cumulative_weights, $total) = prepare_cumulative_weights($items);
// 結果を格納する配列を初期化
$results = array_combine(array_keys($items), array_fill(0, count($items), 0));
// 指定された回数だけランダムにアイテムを選択
for ($i = 0; $i < $count; ++$i) {
$result = weighted_random_pick($cumulative_weights, $total, $algo);
++$results[$result];
}
WeightRandomCacheStorage::clear();
// 選択結果を返す
return $results;
}
/**
* 一度だけアイテムをランダムに選択します。
*
* @param array<int|float> $items アイテムとその重みを格納した配列。キーにアイテム、値に重み
* @param string $algo アルゴリズム名。binary_search, linear_search
* @return float|int|string 選択されたアイテムのキー
*/
function single_weighted_random_pick(array $items, string $algo = 'binary_search'): float|int|string
{
// 累積重みと総重みを計算
list($cumulative_weights, $total) = prepare_cumulative_weights($items);
// ランダムにアイテムを選択
$ret = weighted_random_pick($cumulative_weights, $total, $algo);
WeightRandomCacheStorage::clear();
return $ret;
}
/** 使用例 **/
// ランダム対象
$items = [];
// どれが選ばれたのか集計する配列
$results = [];
for ($i = 0; $i < 10; ++$i) {
$items["item$i"] = [
'name' => "item$i",
'weight' => random_int(1, 1e5),
];
$results[$i] = 0;
}
// アイテムをシャッフル
shuffle($items);
// 実行するランダム選択回数
$num_trials = 1e5;
foreach([null, 'linear_search', 'binary_search',] as $algo) {
// 処理開始時間を記録 & ランダムなアイテムの選択を繰り返す
$start = microtime(true);
$results = any_item_some_weighted_random_pick($items, $num_trials, static fn($item)=>$item['weight'], $algo);
$end = microtime(true);
// 処理時間を表示
$algo ??= 'auto_algo_select';
echo $algo . ' Time: ' . ($end - $start) . "\n";
// 選択結果を表示
foreach($results as $item => $count) {
echo "key:\t$item\tweight:\t{$items[$item]['weight']}\trate:\t" . ($count / $num_trials) . "\n";
}
}
function any_item_some_weighted_random_pick(array $items, int $count, callable|null $weightGetter = null, string|null $algo = null)
{
$_items = [];
if(isset($weightGetter)) {
foreach($items as $key => $item) {
$_items[$key] = $weightGetter($item);
}
} else {
$_items = $items;
}
return some_weighted_random_pick($_items, $count, $algo);
}
- Output for git.master_jit
- auto_algo_select Time: 0.026458024978638
key: 0 weight: 3791 rate: 0.0082
key: 1 weight: 26242 rate: 0.05639
key: 2 weight: 65438 rate: 0.14224
key: 3 weight: 54542 rate: 0.118
key: 4 weight: 39605 rate: 0.08528
key: 5 weight: 41077 rate: 0.08934
key: 6 weight: 92286 rate: 0.20006
key: 7 weight: 66524 rate: 0.14484
key: 8 weight: 62223 rate: 0.13314
key: 9 weight: 10640 rate: 0.02251
linear_search Time: 0.026773929595947
key: 0 weight: 3791 rate: 0.0086
key: 1 weight: 26242 rate: 0.05619
key: 2 weight: 65438 rate: 0.14195
key: 3 weight: 54542 rate: 0.118
key: 4 weight: 39605 rate: 0.08541
key: 5 weight: 41077 rate: 0.08885
key: 6 weight: 92286 rate: 0.19977
key: 7 weight: 66524 rate: 0.14457
key: 8 weight: 62223 rate: 0.13273
key: 9 weight: 10640 rate: 0.02393
binary_search Time: 0.057395935058594
key: 0 weight: 3791 rate: 0.00802
key: 1 weight: 26242 rate: 0.05754
key: 2 weight: 65438 rate: 0.14067
key: 3 weight: 54542 rate: 0.11724
key: 4 weight: 39605 rate: 0.08655
key: 5 weight: 41077 rate: 0.08873
key: 6 weight: 92286 rate: 0.20009
key: 7 weight: 66524 rate: 0.1431
key: 8 weight: 62223 rate: 0.13481
key: 9 weight: 10640 rate: 0.02325
- Output for git.master
- auto_algo_select Time: 0.025741100311279
key: 0 weight: 35686 rate: 0.06331
key: 1 weight: 85284 rate: 0.15156
key: 2 weight: 48677 rate: 0.08635
key: 3 weight: 81046 rate: 0.14529
key: 4 weight: 19310 rate: 0.03412
key: 5 weight: 92576 rate: 0.16596
key: 6 weight: 25724 rate: 0.04543
key: 7 weight: 65398 rate: 0.11666
key: 8 weight: 69220 rate: 0.12367
key: 9 weight: 38243 rate: 0.06765
linear_search Time: 0.027000904083252
key: 0 weight: 35686 rate: 0.06417
key: 1 weight: 85284 rate: 0.15272
key: 2 weight: 48677 rate: 0.0872
key: 3 weight: 81046 rate: 0.14338
key: 4 weight: 19310 rate: 0.03355
key: 5 weight: 92576 rate: 0.16465
key: 6 weight: 25724 rate: 0.04589
key: 7 weight: 65398 rate: 0.11559
key: 8 weight: 69220 rate: 0.12381
key: 9 weight: 38243 rate: 0.06904
binary_search Time: 0.055678844451904
key: 0 weight: 35686 rate: 0.06393
key: 1 weight: 85284 rate: 0.15006
key: 2 weight: 48677 rate: 0.08726
key: 3 weight: 81046 rate: 0.1446
key: 4 weight: 19310 rate: 0.03418
key: 5 weight: 92576 rate: 0.16623
key: 6 weight: 25724 rate: 0.04677
key: 7 weight: 65398 rate: 0.1166
key: 8 weight: 69220 rate: 0.12245
key: 9 weight: 38243 rate: 0.06792
This tab shows result from various feature-branches currently under review by the php developers. Contact me to have additional branches featured.
Active branches
Archived branches
Once feature-branches are merged or declined, they are no longer available. Their functionality (when merged) can be viewed from the main output page
preferences:
27.05 ms | 410 KiB | 5 Q