PHP 项目中常用算法

蓄水池算法

代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$k = 3;
$n = 10;
$pool = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
$result = [];
for ($i = 0; $i < $k; $i++) {
// 前 k 个元素直接放入数组中
$result[$i] = $pool[$i];
}

for ($i = $k; $i < $n; $i++) {
$r = mt_rand(0, $i);
// 随机整数落在[0, k - 1]范围内,则替换蓄水池中的元素
if ($r < $k) {
$result[$r] = $pool[$i];
}
}
应用场景 - 等概率抽奖
  1. 参与抽奖时进行中奖序号计算

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <?php
    // 已参与人数
    $participantCount = 2;
    // 第N人参与时
    $participantCount += 1;
    // 计算排名
    $rank = mt_rand(1, $participantCount);
    if ($rank !== $participantCount) {
    // 与指定用户的排名互换,若更新失败,则不互换
    $result = 'UPDATE test_table SET rank = $participantCount WHERE rank = $rank;';
    if (!$result) {
    $rank = $participantCount;
    }
    }

    // 保存参与记录
    $result = 'INSERT INTO test_table(rank) VALUES($rank)';
  2. 防并发处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    class RedisService 
    {
    /**
    * @var Redis
    */
    private $redis;

    /**
    * Bootstrap.
    *
    * @throws RedisException|Exception
    */
    public function __construct()
    {
    parent::__construct(config('redis.'));
    $this->redis = new Redis();
    $this->redis->connect($this->config->get('host'), $this->config->get('port'));

    if (!$this->redis->auth($this->config->get('auth'))) {
    throw new RedisException("redis password verification failed, auth={$this->config->get('auth')}");
    }

    if ($this->redis->ping() !== '+PONG') {
    throw new RedisException("redis connection is not available, ping={$this->redis->ping()}");
    }
    }

    /**
    * Incr Atomic.
    *
    * @param string $key
    * @param int $defaultValue
    * @param int $value
    *
    * @return int
    */
    public function incrAtomic(string $key, int $defaultValue = 1, int $value = 1)
    {
    // 使用 redis 的 eval (lua 语法), 把对比和自增变成原子操作
    $script = "if redis.call('get', KEYS[1]) == false then return redis.call('incrBy', KEYS[1], ARGV[1]) else return redis.call('incrBy', KEYS[1], ARGV[2]) end";
    return $this->redis->eval($script, [$key, $defaultValue, $value], 1);
    }
    }

    $lottery = [
    'lotteryId' => 1,
    'participantCount' => 10,
    ];
    $redisService = new RedisService();
    // 获取当前活动参与人数
    $participantCount = $redisService->incrAtomic("lottery:incr_{$lottery['lotteryId']}", $lottery['participantCount'] + 1);
    // 计算中奖序号
    $winningNumber = mt_rand(1, $participantCount);
    // 开启事务
    $participationRecordModel->startTrans();
    // 与已有的中奖序号互换
    if ($winningNumber !== $participantCount) {
    $updateResult = $participationRecordModel::where([
    'lottery_id' => $lottery['lotteryId'],
    'winning_number' => $winningNumber,
    ])->update([
    'winning_number' => $participantCount
    ]);
    if (!$updateResult) {
    $winningNumber = $participantCount;
    }
    }

    // 保存参与记录
    $participationRecordModel->insertGetId([
    'lottery_id' => $lottery['lotteryId'],
    'user_id' => $user->userId,
    'winning_number' => $winningNumber,
    'generate_time' => date('Y-m-d H:i:s'),
    ]);
    // 提交事务
    $participationRecordModel->commit();

转盘抽奖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<?php
$lotteryPrize = [];
$prizes = [
[
'prizeId' => 1,
'prizeLevel' => '一等奖',
'prizeName' => 'iphone',
'prizeCount' => 1,
'prizeProbability' => '0.001'
],
[
'prizeId' => 2,
'prizeLevel' => '二等奖',
'prizeName' => 'ipad',
'prizeCount' => 3,
'prizeProbability' => '0.005'
],
[
'prizeId' => 3,
'prizeLevel' => '三等奖',
'prizeName' => '500购物券',
'prizeCount' => 0,
'prizeProbability' => '10.000'
],
[
'prizeId' => 4,
'prizeLevel' => '四等奖',
'prizeName' => '200元话费',
'prizeCount' => 10,
'prizeProbability' => '30.000'
],
[
'prizeId' => 5,
'prizeLevel' => '五等奖',
'prizeName' => '100元话费',
'prizeCount' => 30,
'prizeProbability' => '50.000'
],
[
'prizeId' => 6,
'prizeLevel' => '六等奖',
'prizeName' => '50元话费',
'prizeCount' => 50,
'prizeProbability' => '50.000'
],
[
'prizeId' => 7,
'prizeLevel' => '七等奖',
'prizeName' => '30元话费',
'prizeCount' => 100,
'prizeProbability' => '90.000'
]
];
// 按概率从低到高排序
$probabilityArray = array_column($prizes, 'prizeProbability');
array_multisort($probabilityArray, SORT_ASC, $prizes);
// 奖品数组
$prizesArray = [];
// 总概率
$probabilitySum = 0;
$rate = 1000;

foreach ($prizes as $prize) {
// 奖品有库存
if ($prize['prizeCount'] > 0) {
$prize['prizeProbability'] *= $rate;
$prizesArray[$prize['prizeId']] = $prize;
$probabilitySum += $prize['prizeProbability'];
}
}

// 中奖的奖品ID
$winningPrizeId = 0;
// 原始概率
$originalProbability = 100 * $rate;
foreach ($prizesArray as $key => $value) {
// 总概率不足$originalProbability时
$max = $probabilitySum < $originalProbability ? $originalProbability : $probabilitySum;
$rand = mt_rand(1, $max);
if ($rand <= $value['prizeProbability']) {
$winningPrizeId = $key;
break;
}

$probabilitySum -= $value['prizeProbability'];
}

isset($prizesArray[$winningPrizeId]) && ($lotteryPrize = $prizesArray[$winningPrizeId]);

echo '中奖奖品:';
print_r($lotteryPrize);

砍价

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
/**
* 计算用户砍价金额
*
* @param float $remainingAmount
* @param int $remainingCount
* @param int $bargainCount
*
* @return string
*/
$calculateBargainAmountFunc = function (float $remainingAmount, int $remainingCount, int $bargainCount) {
// 剩余一次
if ($remainingCount == 1) {
return $remainingAmount;
}

$minMoney = 0.01;
// 剩余金额 = 剩余金额 - (剩余刀数 * 最低金额)
$remainingAmount = bcsub($remainingAmount, $remainingCount * $minMoney);
// 平均金额
$avgAmount = bcdiv($remainingAmount, $remainingCount);
// 剩余刀数占比
$remainingProgress = bcdiv($remainingCount, $bargainCount);

// 第一刀 = (总刀数的15% ~ 30%) * 平均金额
if (bccomp($remainingProgress, 1) === 0) {
$minScaleFactor = $remainingCount * 0.15;
$maxScaleFactor = $remainingCount * 0.35;
} // 前30%刀 = 平均金额 * (2 ~ 4)
elseif (bccomp($remainingProgress, 0.7) !== -1) {
$minScaleFactor = 2;
$maxScaleFactor = 4;
} // 后70%刀 = 平均金额 * (0.1 ~ 2)
else {
$minScaleFactor = 0.1;
$maxScaleFactor = 3;
}

$minAmount = bcmul($avgAmount, $minScaleFactor);
$maxAmount = bcmul($avgAmount, $maxScaleFactor);
// 最大金额超出剩余金额
$maxAmount > $remainingAmount && ($maxAmount = $remainingAmount);
// 最低金额大于最大金额
$minAmount > $maxAmount && ($minAmount = $maxAmount);

// 砍价金额
return bcadd(bcdiv(mt_rand($minAmount * 100, $maxAmount * 100), 100), $minMoney);
};

$totalAmount = 100;
$estimateBargainCount = 20;
$bargainedCount = 0;
$t = 0;
bcscale(2);
while ($bargainedCount < $estimateBargainCount) {
$remainingCount = $estimateBargainCount - $bargainedCount;
$bargainAmount = $calculateBargainAmountFunc($totalAmount, $remainingCount, $estimateBargainCount);
$totalAmount = bcsub($totalAmount, $bargainAmount);
$bargainedCount++;
echo "第{$bargainedCount}次,砍价金额:{$bargainAmount}", '<br />';
$t = bcadd($t, $bargainAmount);
}
0%