2

假设我在数据库中存储了以下信息:

User         Points
 A            2000
 B            1000

我想根据点数随机选择一个获胜者。在这种情况下,由于总共有 3000 分,“A”有 67% 的机会被选中,“B”有 33% 的机会。

使用 PHP 选择获胜者的最有效方法是什么(从计算概率到选择获胜者)?请注意,玩游戏的用户数量不是固定的,范围可能很大(因此它应该计算“每个用户”而不是固定在 A 和 B)。

我一直在尝试潜在的解决方案,但还没有弄清楚。我很想听听你的解决方案!

4

3 回答 3

6

基于处理用户和点的整个地图可能有许多类似的方法。这很简单,如果用户不多,也可以正常工作。但是当用户数量增加时,内存甚至 CPU 使用率可能会出现很大的性能问题。所以我考虑了一个可能的解决方案,考虑到性能。

这里描述了我将遵循的根据概率绘制用户的方法:

+------+--------+
| User | Points |     Bar graph:
+------+--------+
|  A   |     20 |     |~~~~~~~~~~~~~~~~~~~~|
+------+--------+
|  B   |     10 |     |~~~~~~~~~~|
+------+--------+
|  C   |      1 |     |~|
+------+--------+
|  D   |      5 |     |~~~~~|
+------+--------+
|  E   |     12 |     |~~~~~~~~~~~~|
+------+--------+
|  F   |      8 |     |~~~~~~~~|
+------+--------+
 TOTAL |     56 |     Random number: 33
       +--------+

If we take all bars and put them heel and toe we get something like this:
          A               B      C   D        E          F
|~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~|~|~~~~~|~~~~~~~~~~~~|~~~~~~~~|
             Position 33 is up here ▲ so we got a winner: user D

这是一个简单的概念,但它可能会破坏性能,具体取决于算法实现(例如,如果我们尝试按顺序执行)。

我要做的是将用户组一分为二,并将第一部分的点总和与随机数进行比较。如果数量小于第一部分积分(累计)总和,则该数字必须对应该部分内的用户,否则该数字对应第二部分。这种方法必须递归地应用于每个选择的部分,直到我们得到一个用户(获胜者)。这样,我们在第一次迭代中丢弃了 1/2 的用户总数,在第二次迭代中丢弃了 1/4,依此类推。这意味着,如果我们有 100 万用户,那么在第二次迭代中,我们将摆脱 75 万用户。不错的国际海事组织。

这是基于 PHP 和 MySQL 的解决方案...

用户表:

CREATE TABLE `User` (
  `id` int(15) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `points` int(15) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1;

数据库交互类:

class UserDb
{
    private $pdo;
    private $sumIntervalStmt;

    public function __construct(\PDO $pdo)
    {
        $this->pdo             = $pdo;
        $this->sumIntervalStmt = null;
    }

    public function getTotal()
    {
        $query  = 'SELECT COUNT(`id`) FROM `User`;';
        $result = $this->pdo->query($query);
        return (int)($result->fetchColumn());
    }

    public function getTotalPoints()
    {
        $query  = 'SELECT SUM(`points`) FROM `User`;';
        $result = $this->pdo->query($query);
        return (int)($result->fetchColumn());
    }

    public function sumPointsInterval($offset, $length)
    {
        if ($this->sumIntervalStmt === null) {
            $this->sumIntervalStmt = $this->pdo->prepare(
                'SELECT SUM(points) FROM (' .
                    'SELECT points FROM `User` LIMIT ?, ?' .
                ') AS Subgroup;'
            );
        }
        $this->sumIntervalStmt->bindValue(1, (int)$offset, \PDO::PARAM_INT);
        $this->sumIntervalStmt->bindValue(2, (int)$length, \PDO::PARAM_INT);
        $this->sumIntervalStmt->execute();
        return (int)($this->sumIntervalStmt->fetchColumn());
    }

    public function getUserByOffset($offset)
    {
        $query = 'SELECT * FROM `User` LIMIT ?, 1;';
        $stmt  = $this->pdo->prepare($query);
        $stmt->bindValue(1, (int)$offset, \PDO::PARAM_INT);
        $stmt->execute();
        return $stmt->fetchObject();
    }
}

用户抽奖等级:

class UserRaffle
{
    private $users;

    public function __construct(UserDb $users)
    {
        $this->users = $users;
    }

    public function drawUser()
    {
        $total  = $this->users->getTotal();
        $number = rand(1, $this->users->getTotalPoints());
        $offset = 0;
        $length = ceil($total / 2);
        $count  = $total;
        $sum    = $this->users->sumPointsInterval($offset, $length);
        $accum  = 0;
        while ($count > 1) {
            if ($number <= $sum) {
                $count   -= $count - $length;
                $length   = ceil($length / 2);
                $interval = $this->users->sumPointsInterval($offset, $length);
                $sum      = $accum + $interval;
            } else {
                $accum   += $sum;
                $offset  += $length;
                $count   -= $length;
                $length   = ceil($count / 2);
                $interval = $this->users->sumPointsInterval($offset, $length);
                $sum     += $interval;
            }
        }
        return $this->users->getUserByOffset($offset);
    }
}

和执行:

$pdo    = new \PDO('mysql:dbname=test;host=localhost', 'username', '********');
$users  = new UserDb($pdo);
$raffle = new UserRaffle($users);

$winner = $raffle->drawUser();
于 2012-11-20T12:43:01.520 回答
2

只是一个想法(参考我关于机会的评论):总结所有积分并为玩家排序。然后在 1 和 之间选择一个随机数$sum。现在你可以从你的随机数中减去玩家的分数,直到你达到 0。

$players = array(
        "A" => 2000,
        "B" => 1000
);
$sum = array_sum($players);
echo $random = rand(1, $sum)."\n";
foreach($players as $player => $points) {
        $winner = $player;
        $random -= $points;
        if($random <= 0)
                break;
}
echo $winner."\n";

您可以使用概率$points/$sum和 0.0 到 1.0 之间的随机数类似地执行此操作。

于 2012-11-19T14:25:38.053 回答
0

您可以对所有玩家使用公式 1/p。我认为标准化它是个好主意,以便所有概率的总和为 1。然后您可以使用 [0...1] 中的随机生成器并遍历所有玩家。当随机生成器的数字小于当前玩家概率时,选择该玩家,否则从随机数中减去当前概率并移动到下一个玩家:

       x = random([0.0, 1.0])
       for i in 0..n
           if x < probabilities[i]
               choose(i)
              break
           else
              x -= probabilities[i]
          end
        end

当您需要标准化时,您必须将每 1/px 乘以所有 1/px 乘数之和的 reziproken。例如:您有一个具有两条边 p1=30 和 p2=15 的顶点。1/(1/30 + 1/15) = 10,P1 = 10 * 1/30 = 1/3 和 P2 = 10 * 1/15 = 2/3。

于 2012-11-19T14:30:42.753 回答