【问题标题】:PHP Dart game calculation slow performancePHP Dart 游戏计算速度慢
【发布时间】:2014-01-19 19:12:43
【问题描述】:

我创建了一个类来根据分数计算出局数。

例如,如果当前分数为 140,则该类返回一个包含可能抛出的集合的数组:

[10] => Array
    (
        [0] => T18
        [1] => T18
        [2] => D16
    )

[11] => Array
    (
        [0] => T18
        [1] => T16
        [2] => D19
    )

[13] => Array
    (
        [0] => T17
        [1] => T17
        [2] => D19
    )

[14] => Array
    (
        [0] => 50
        [1] => 50
        [2] => D20

但是计算这些东西非常慢。有什么办法可以优化这个类吗?

<?php
/**
 * PHP Dartgame calculating class
 * @author Youri van den Bogert
 */

class Darts {

    /**
     * @var string
     */
    public static $notation_triple = 'T';

    /**
     * @var string
     */
    public static $notation_double = 'D';

    /**
     * @var int
     */
    private static $maxCheckout = 170;

    /**
     * @var string
     */
    private static $doubleBull = 'Bull';

    /**
     * @var string
     */
    private static $singleBull = 'Single';

    /**
     * @var array
     */
    private static $scoreSheet = array('1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '25', '50');

    /**
     * Get a total thrown score
     * @param $score1
     * @param $score2
     * @param $score3
     * @return array
     */
    public static function getTotalScore ($score1, $score2, $score3) {
        return array(
          'dart1' => self::getScoreOfDart($score1),
          'dart2' => self::getScoreOfDart($score2),
          'dart3' => self::getScoreOfDart($score3),
          'total' => self::getScoreOfDart($score1) + self::getScoreOfDart($score2) + self::getScoreOfDart($score3)
        );
    }


    /**
     * Get score of a single dart
     * @param $score
     * @return mixed
     */
    public static function getScoreOfDart ($score) {

        if (is_numeric($score)) {
            return $score;
        }

        if ($score[0] == self::$notation_triple) {
            $multiplier = 3;
        } elseif ($score[0] == self::$notation_double) {
            $multiplier = 2;
        } else {
            $multiplier = 1;
        }

        $correctScore = filter_var($score, FILTER_SANITIZE_NUMBER_INT);

        return ($correctScore * $multiplier);

    }

    public static function getScoreSheet () {

        return self::$scoreSheet;

    }

    public static function calculatePossibleCheckout ($currentScore) {

        // We cant checkout higher then $maxCheckout
        if ($currentScore > self::$maxCheckout || $currentScore == 1) {
            return false;
        }

        // Return bull
        if ($currentScore == 50) {
            return array(
                'dart1' => self::$doubleBull
            );
        }

        if ($currentScore == self::$maxCheckout) {
            return array(
                'dart1' => self::$notation_triple . '20',
                'dart2' => self::$notation_triple . 'T20',
                'dart3' => 'Bull'
            );
        }

        $lastScore = $currentScore;
        $lastPossibleThrow = 0;
        $checkOut = array();

        // Can current score be checked out?
        if (self::canScore($currentScore) == true) {

            return array(
                'dart1' => self::$notation_double . ($currentScore / 2)
            );

        // Current score can't be checked out - calculate what to throw
        } else {

            for ($x=60; $x >= 0; --$x) {

                if ($x <= 20 || $x == 50 || $x == 25 || ($x % 3 == 0) || ($x <= 40 && ($x % 2 == 0))) {

                    for ($xx=60; $xx >= 0; --$xx) {

                        if ($x <= 20 || $x == 50 || $x == 25 || ($x % 3 == 0) || ($x <= 40 && ($x % 2 == 0))) {

                            for ($xxx=50; $xxx > 0; $xxx = $xxx - 2) {

                                if ($xxx == 48) {
                                    $xxx = 40;
                                }

                                if (self::checkIfScoreExists($xxx) == true && self::checkIfScoreExists($xx) == true && self::checkIfScoreExists($x) == true && ($xxx + $xx + $x) == $currentScore) {

                                    $score_1 = self::getCorrectDartName($xxx);
                                    $score_2 = self::getCorrectDartName($xx);
                                    $score_3 = self::getCorrectDartName($x, true);

                                    if ($score_1[0] == 'D' || $score_2[0] == 'D' || $score_3[0] == 'D') {
                                        $nextKey = (count($checkOut)+1);
                                        if ($xxx != 0) $checkOut[$nextKey][] = $score_1;
                                        if ($xx != 0) $checkOut[$nextKey][] = $score_2;
                                        if ($x != 0) $checkOut[$nextKey][] = $score_3;

                                        usort($checkOut[$nextKey], function($a, $b) {
                                            if (is_int($a) || is_float($a)) {
                                                if (is_int($b) || is_float($b)) {
                                                    return $a - $b;
                                                }
                                                else
                                                    return -1;
                                            }
                                            elseif (is_int($b) || is_float($b)) {
                                                return 1;
                                            }
                                            else {
                                                return strcmp($b, $a);
                                            }
                                        });
                                    }

                                }
                            }
                        }
                    }
                }
            }

        }

        return array_unique($checkOut, SORT_REGULAR);

    }

    public static function getCorrectDartName ($total, $isLast = false) {

        if ($total == 25 || $total == 50) {
            return $total;
        }

        if ($total < 20 && $isLast == false) {
            return $total;
        }

        if ($total %3 == 0) {
            return self::$notation_triple . ($total/3);
        } elseif ($total %2 == 0) {
            return self::$notation_double . ($total/2);
        }

        return $total;


    }

    /**
     * Check if score exists
     * @param $score
     * @return bool
     */
    public static function checkIfScoreExists ($score) {

        if ($score == 50 || $score == 25 || $score == 0) return true;

        $possibleScores = array_merge(range(1,20));

        foreach ($possibleScores as $posScore) {
            if ($score == self::getScoreOfDart(self::$notation_double . $posScore) || $score == self::getScoreOfDart(self::$notation_triple . $posScore) || $score == $posScore) {
                return true;
            }
        }

        return false;

    }

    /**
     * Check if a specific score can be thrown by one dart
     * @param $score
     * @return bool
     */
    public static function canScore ($score) {
        if ($score == 50) {
            return true;
        } elseif ($score < 40 || $score == 40) {

            if ($score % 2 == 0) {
                return true; // Score is even - so its possible to throw
            } else {
                return false;
            }
        }

        return false;
    }


} 

课程链接:https://gist.github.com/YOUR1/8509498

【问题讨论】:

  • 应该在codereview上发布
  • 如果这被证明计算起来本质上很昂贵,请考虑简单地创建一个文本文件(或数据库表)来存储每个分数的出局。由于这样的表将包含少于 200 条记录,因此与每次需要答案时运行数字运算器相比,它可能是一种更有效的操作方式。
  • 实际上我认为这将是一个相当大的文件。当你得到低于 100 的分数时,会有很多出局。您需要一个快速的存储和检索解决方案。可能是内存中的还是数据库中的? SQLite?内存缓存?
  • 我喜欢这个问题,但您的代码只会根据分数计算出局数。不幸的是,您不能考虑玩家的首选组合!祝你好运。
  • Arkanon 是的,这也是我的第二个猜测,只需生成一次所有输出,然后将其存储在 SQL 表中。 @PeteR 如果我将输出存储在数据库中并为玩家制作一个关系表,具体输出和抛出时间,则可以做到这一点:)。

标签: php class permutation


【解决方案1】:

我使用了基本的置换生成,而且速度非常快(0.06 秒)。它当然可以优化,但我认为没有意义,因为它已经这么快了。

<?php

class DartUtils {

    private static $possible_points;

    public static function getPossibleThrowsForScore($score) {

        // generate all possible single throws and their score
        // I didn't want to write them all out
        // but it's certainly an option (and faster)
        self::$possible_points = array();
        for ($i = 1; $i <= 20; $i += 1) {
            self::$possible_points["S" . $i] = $i; // S = single
            self::$possible_points["D" . $i] = $i * 2;
            self::$possible_points["T" . $i] = $i * 3;
        }
        self::$possible_points["bull"] = 25;
        self::$possible_points["double bull"] = 50;
        // self::$possible_points["miss"] = 0;

        $throws = self::findSatisfyingThrowsForScore($score, 3, array());
        foreach ($throws as $i => $serialized_throw) {
            $throws[$i] = unserialize($serialized_throw);
        }
        return $throws;
    }

    private static function findSatisfyingThrowsForScore($score, $num_throws, $base_notation) {
        $possible_throws = array();
        foreach (self::$possible_points as $notation => $value) {
            if ($num_throws === 1) { // we've done all throws
                if ($score - $value === 0) { // we satisfied the score
                    $throw = array_merge($base_notation, array($notation));
                    sort($throw);
                    $possible_throws[] = serialize($throw);
                }
            } else {
                // so long as there are num_throws, recurse with all possible throws
                $possible_throws = array_merge($possible_throws,
                  self::findSatisfyingThrowsForScore($score - $value, $num_throws - 1, array_merge($base_notation, array($notation))));
            }
        }
        $possible_throws = array_unique($possible_throws);
        sort($possible_throws);
        return $possible_throws;
    }

}

var_dump(DartUtils::getPossibleThrowsForScore(140));

输出是:

array(21) {
  [0]=>
  array(3) {
    [0]=>
    string(3) "D10"
    [1]=>
    string(3) "T20"
    [2]=>
    string(3) "T20"
  }
  [1]=>
  array(3) {
    [0]=>
    string(3) "D13"
    [1]=>
    string(3) "T18"
    [2]=>
    string(3) "T20"
  }
  [2]=>
  array(3) {
    [0]=>
    string(3) "D13"
    [1]=>
    string(3) "T19"
    [2]=>
    string(3) "T19"
  }
  [3]=>
  array(3) {
    [0]=>
    string(3) "D15"
    [1]=>
    string(3) "T20"
    [2]=>
    string(11) "double bull"
  }
  [4]=>
  array(3) {
    [0]=>
    string(3) "D16"
    [1]=>
    string(3) "T16"
    [2]=>
    string(3) "T20"
  }
  [5]=>
  array(3) {
    [0]=>
    string(3) "D16"
    [1]=>
    string(3) "T17"
    [2]=>
    string(3) "T19"
  }
  [6]=>
  array(3) {
    [0]=>
    string(3) "D16"
    [1]=>
    string(3) "T18"
    [2]=>
    string(3) "T18"
  }
  [7]=>
  array(3) {
    [0]=>
    string(3) "D18"
    [1]=>
    string(3) "T18"
    [2]=>
    string(11) "double bull"
  }
  [8]=>
  array(3) {
    [0]=>
    string(3) "D19"
    [1]=>
    string(3) "T14"
    [2]=>
    string(3) "T20"
  }
  [9]=>
  array(3) {
    [0]=>
    string(3) "D19"
    [1]=>
    string(3) "T15"
    [2]=>
    string(3) "T19"
  }
  [10]=>
  array(3) {
    [0]=>
    string(3) "D19"
    [1]=>
    string(3) "T16"
    [2]=>
    string(3) "T18"
  }
  [11]=>
  array(3) {
    [0]=>
    string(3) "D19"
    [1]=>
    string(3) "T17"
    [2]=>
    string(3) "T17"
  }
  [12]=>
  array(3) {
    [0]=>
    string(3) "D20"
    [1]=>
    string(11) "double bull"
    [2]=>
    string(11) "double bull"
  }
  [13]=>
  array(3) {
    [0]=>
    string(3) "D20"
    [1]=>
    string(3) "D20"
    [2]=>
    string(3) "T20"
  }
  [14]=>
  array(3) {
    [0]=>
    string(3) "S20"
    [1]=>
    string(3) "T20"
    [2]=>
    string(3) "T20"
  }
  [15]=>
  array(3) {
    [0]=>
    string(3) "T10"
    [1]=>
    string(3) "T20"
    [2]=>
    string(11) "double bull"
  }
  [16]=>
  array(3) {
    [0]=>
    string(3) "T11"
    [1]=>
    string(3) "T19"
    [2]=>
    string(11) "double bull"
  }
  [17]=>
  array(3) {
    [0]=>
    string(3) "T12"
    [1]=>
    string(3) "T18"
    [2]=>
    string(11) "double bull"
  }
  [18]=>
  array(3) {
    [0]=>
    string(3) "T13"
    [1]=>
    string(3) "T17"
    [2]=>
    string(11) "double bull"
  }
  [19]=>
  array(3) {
    [0]=>
    string(3) "T14"
    [1]=>
    string(3) "T16"
    [2]=>
    string(11) "double bull"
  }
  [20]=>
  array(3) {
    [0]=>
    string(3) "T15"
    [1]=>
    string(3) "T15"
    [2]=>
    string(11) "double bull"
  }
}

我添加的投掷是:1-20 单、双或三、牛和双牛。如果有更多或特殊的投掷,您可以添加它们。

排序+序列化是一种快速删除重复项的技巧。出于抛出验证的目的,保留重复项实际上可能是有益的。

您可以考虑将符号作为字符串接受,例如:S10D20BULLDBULLDBULLT20。如果您为单身人士介绍S,您将永远不会感到困惑。 D112T20 有歧义,是D11S2T20 还是D1S12T20?字符串更易于使用,因此您甚至可以获得性能。将字符串符号拆分回它的各个部分有点棘手但可行。

请注意,我没有为&gt;1701 添加特殊检查,因为列表将是空的。这是您可以应用的优化。

您可以选择添加 miss 投掷,得分为0


我不太了解您的代码,它太复杂了。我认为您在符号之间进行排序和转换会浪费很多时间。正如您在我的解决方案中看到的那样,我快速构建结果集并同时进行分数计算和符号生成。


我还应该提到我对飞镖规则不太熟悉。我假设 bulldouble Bull 但如果 single Bullbull 是准确的,请随时纠正我。

【讨论】:

  • 感谢您的解决方案。我对其进行了一些修改,以将可能的输出保存到数据库中。
  • 顺便说一句;你应该编辑 self::findThrows($score - $value, $num_throws - 1, array_merge($base_notation, array($notation)))); to self::findSatisfyingThrowsForScore($score - $value, $num_throws - 1, array_merge($base_notation, array($notation))));
【解决方案2】:
  • 我发现的第一件事是,您的班级并没有返回给定分数的所有可能投出结果。
  • 第二个是真的很慢。

但这让我很感兴趣,所以我制作了自己的课程版本来解决这个问题。由于您在此处给出的现有问题对于手头的问题看起来有点复杂。只是想让它简单,简单也应该快。

我做了一些假设:

  1. 添加了 0 分的“未命中”拍摄(如果不需要,您可以将其注释掉)。
  2. 和你一样,将最高分设置为 170。
  3. 假定的类应该返回所有可能的抛出。

我使用包含所有可能拍摄及其得分的数组设置属性。并制定方法返回给定分数的所有可能的抛出。

我已经为我的班级设置了一个测试,以便为您提供一些成绩数据。我已经在一个循环中一个一个地运行了所有的分值,从 1 到 170,以获得每个可能的结帐结果。

这是我机器上的结果:

  • 170 总得分运行 (1 - 170)。
  • 250026总分结果结账。
  • 4 秒总运行时间。

平均每秒 42.5 个得分结果和每秒平均 62506.5 个结帐结果。

所以它返回所有可能的结果,并且速度更快。希望这会有所帮助。如果没有别的,至少可以让你知道如何改进你的课程。

<?php

$darts = new Darts();

$result = $darts->possibleCheckout(60); //gives 3767 results

//var_dump($result);
echo count($result) . "\n";



/**
 * PHP Dartgame calculating class
 * @author Aleksandar Popovic
 */
class Darts
{
    /**
     * All possible shoots and score per each
     * @var array
     */
    private $shoot_score = array(
        'miss' => 0,
        '1' => 1,
        '2' => 2,
        '3' => 3,
        '4' => 4,
        '5' => 5,
        '6' => 6,
        '7' => 7,
        '8' => 8,
        '9' => 9,
        '10' => 10,
        '11' => 11,
        '12' => 12,
        '13' => 13,
        '14' => 14,
        '15' => 15,
        '16' => 16,
        '17' => 17,
        '18' => 18,
        '19' => 19,
        '20' => 20,
        'D1' => 2,
        'D2' => 4,
        'D3' => 6,
        'D4' => 8,
        'D5' => 10,
        'D6' => 12,
        'D7' => 14,
        'D8' => 16,
        'D9' => 18,
        'D10' => 20,
        'D11' => 22,
        'D12' => 24,
        'D13' => 26,
        'D14' => 28,
        'D15' => 30,
        'D16' => 32,
        'D17' => 34,
        'D18' => 36,
        'D19' => 38,
        'D20' => 40,
        'T1' => 3,
        'T2' => 6,
        'T3' => 9,
        'T4' => 12,
        'T5' => 15,
        'T6' => 18,
        'T7' => 21,
        'T8' => 24,
        'T9' => 27,
        'T10' => 30,
        'T11' => 33,
        'T12' => 36,
        'T13' => 39,
        'T14' => 42,
        'T15' => 45,
        'T16' => 48,
        'T17' => 51,
        'T18' => 54,
        'T19' => 57,
        'T20' => 60,
        'Signle-Bull' => 25,
        'Double-Bull' => 50
    );

     /**
     * Maximum score
     * @var int
     */
    private $max_score = 170; // 3 x T20 is max right?


    /**
     * Return all possible checkouts for given score
     * @param int $current_score
     * @return array
     */
    public function possibleCheckout($current_score)
    {
        if ($current_score > $this->max_score || $current_score < 1)
        {
            return false;
        }

        $checkout = array();
        foreach ($this->shoot_score as $shoot1 => $score1)
        {
            if ($score1 > $current_score)
            {
                continue;
            }

            foreach ($this->shoot_score as $shoot2 => $score2)
            {
                if ($score1 + $score2 > $current_score)
                {
                    continue;
                }

                foreach ($this->shoot_score as $shoot3 => $score3)
                {
                    if ($score1 + $score2 + $score3 == $current_score)
                    {
                        $checkout[] = array($shoot1, $shoot2, $shoot3);
                        continue;
                    }
                }
            }
        }

        return $checkout;
    }
}

【讨论】:

  • 此外,如果您考虑到标准规则(在大多数国际比赛中使用的规则)指定结账时的最后一枪必须是双倍,您可能会进一步减少运行时间。 (并且 OP 确实考虑到了这一点)
  • 对不起,我对规则不是很熟悉。不知道。
【解决方案3】:

如果您想获得额外的性能提升,您必须将所有可能性的结果保存在数据库或文件中,并且只需读取值而不是每次都计算它们。

我会建议以下结构

Table (checkout)

sum (int) | dart1 (int) | dart2 (int) | dart3 (int)

60 | 20 | 20 | 31

所有飞镖指向下表

Table (dart)

pk (int) | bez (varchar)

20 | 20

31 | D10

【讨论】:

    【解决方案4】:

    不确定你的错误是什么性质,因为我不熟悉飞镖规则,但第 129 行肯定是错误的:

    if ($x <= 20 || $x == 50 || $x == 25 || ($x % 3 == 0) || ($x <= 40 && ($x % 2 == 0))) {
    

    要么您想测试 $xx,要么您不想重新测试已经引导您进入该行的条件。目前,只要 $xx 循环,最里面的循环就会一直被调用。那是性能杀手。当然,不管怎样,通常在嵌套循环中都有一个嵌套循环。

    按照 Arkanon 的建议,另一种方法可能是生成一个查找表,其中包含所有可能分数的答案。

    【讨论】:

    • 我完全被他的 for 循环弄糊涂了。唯一看起来准确的是有 3 个级别(每次投掷一个)。我的解决方案使用递归来生成 3 个级别,这允许一些巧妙的变量绑定。迭代解决方案(具有 3 个嵌套循环)当然是可能的。
    • 递归绝对可以替换 3 个嵌套循环,但我不确定它是否会明显(如果有的话)更有效。无论如何,我同意当前的循环代码没有意义,需要进行一些修改。
    【解决方案5】:

    这是我的尝试。

    输出按字典顺序按单个分数升序排序。

    • 如果您执行 foreach 循环,您将获得从第 1 步到第 n 步的结果。
    • 如果您按索引访问,您将以相反的顺序获得移动(1 = 最后移动,3 = 第一移动)。

    3 步的速度非常快,但如果你尝试超过 3 步,它就会崩溃。

    可以(理论上)计算任意移动次数的可能性,但您很快就会达到脚本的最大执行时间:)。

    可达到的最高分数为 180(3x 三倍 20)。您可能会浪费大量 CPU 来尝试更高的分数。它将产生一个空结果。

    class darts {
        const DATAFILE = "darts-scores.txt"; // score names & values database
        static $score_val;  // values of scores
        static $score_name; // names of scores
        static $score_num;  // # of score values
    
        static $res;        // search result
        static $tries;      // for statistics
    
        // internal search
        static private function moves ($score, $moves, $i=0, &$list = array())
        {
            self::$tries++; // update stats
    
            // search from the current scores only
            for ( ; $i != self::$score_num; $i++)
            {
                $val = self::$score_val[$i];
                if ($val > $score) break;  // no need to try these ones
                if ($moves == 1) // unrolled the recursion to improve performances
                {
                    if ($score == $val)
                    {
                        // found another combination
                        $list[$moves] = self::$score_name[$i];
                        self::$res[] = $list;
                    }
                }
                else // go down to seek the rest of the combination
                {
                    $list[$moves] = self::$score_name[$i];
                    self::moves ($score - $val, $moves-1, $i, $list);
                }
            }
        }
    
        // public search function
        static function moves_for_score ($score, $moves=3)
        {
            self::$res = array();
            self::$tries=0;
            self::moves ($score, $moves);
            return self::$res;
        }
    
        // turn results into a string
        static function flatten ($res)
        {
            return implode (", ",
                array_map (
                    function ($e){ return "[".implode(':',$e)."]"; },
                    $res));
        }
    
        // initialize scores table
        static function init ()
        {
            if (!file_exists (self::DATAFILE))
            {
                // you can change the names of the scores with these two lines
                $scores = array (
                    "miss" =>0, 
                    "bull"=>25, 
                    "double bull"=>50);
                $type = array (
                    1=>"S", 
                    2=>"D", 
                    3=>"T");
    
                // generate all scores
                for ($t = 1 ; $t <= 3 ; $t++)
                for ($i = 1 ; $i <= 20 ; $i++)
                {
                    $scores[$type[$t].$i] = $t * $i;
                }
                asort ($scores);
                foreach ($scores as $name=>$val) $out[] = "$name:$val";
                file_put_contents (self::DATAFILE, implode ("\n", $out));
            }
    
            // read score file
            $in = preg_split ("/[:\n]/", file_get_contents (self::DATAFILE));
            self::$score_num = count($in)/2;
            for ($i = 0 ; $i != self::$score_num ; $i++)
            {
                self::$score_name[$i] = $in[$i*2];
                self::$score_val [$i] = (int) $in[$i*2+1];
            }
        }
    }
    darts::init();
    
    ////////////////////////////////////////////////////
    // basic usage
    ////////////////////////////////////////////////////
    $score = 281;
    $moves = 5;
    $res = darts::moves_for_score ($score, $moves);
    
    echo "Sequences for $score in $moves moves: "
         .darts::flatten($res)
         ."<br>";
    echo "<pre>".print_r($res, true)."</pre><br>";
    
    ////////////////////////////////////////////////////
    // stress test
    ////////////////////////////////////////////////////
    
    echo "<br>Computing all possible sequences from 0 to 181...";
    $start = microtime(true);
    $tries = 0;
    $max = 0;
    for ($i = 0 ; $i <=180 ; $i++)
    {
        $res = darts::moves_for_score ($i);
        $flat = darts::flatten($res);
        if (strlen ($flat) > $max) 
        { 
            $max = strlen($flat); 
            $res_max = $res; 
            $i_max = $i;
        }
        $tries += darts::$tries;
    }
    echo "done in ".(microtime(true)-$start)."s, $tries tries<br>";
    echo "<br>Longest list of possibilities:<br>$i_max -> "
        .darts::flatten ($res_max)."<br>";
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-03-03
      • 1970-01-01
      • 1970-01-01
      • 2017-12-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多