【问题标题】:PHP for loop being way too slow with big inputs ( Digit Sum loop )PHP for 循环对于大输入来说太慢了(数字总和循环)
【发布时间】:2013-11-12 16:13:19
【问题描述】:

亲爱的,我想征求你的意见。

我在 PHP

中有以下代码/ for 循环
$number= 123
$counter = 0;
$digitsum = 6 /* This is calculated by another function */

for($i=1;$i<=$number-1;$i++) {
if(array_sum(preg_split('//',$i)) == $digitsum){
  $counter++;
       }    
}

echo $counter; 

所以基本上我的循环计算给定数字以下有多少数字具有相同的数字总和,然后显示有多少。尽管它可以完美地处理 5 位数字并在 1 秒内计算循环数,但我需要它来处理 18 位数字并在 1 秒内完成计数。

这甚至有可能实现吗?还是我应该寻求其他解决方案?

【问题讨论】:

  • preg_ 很慢。使用explode() 或将数字转换为字符串然后遍历字符串中的字符,您可能会得到更好的结果。
  • @ThomWiggers 是在正确的道路上。尝试使用str_split((string)$i);
  • 18 位数字? for loop 即使在你的循环中什么都不做也会花费很长时间......
  • str_split 一样慢,@Fu XU 那你建议用什么?
  • 这是一道数学题……$digitsum的取值范围是多少?

标签: php loops for-loop


【解决方案1】:

这是一个非常快速的解决方案。它仍然无法在 1 秒内处理 18 位数字,但它可以在 2 毫秒内完成@Edakos (6, 12345) 的答案中的示例。

function count_numbers_with_digit_sum( $sum, $max ) {
    if($sum < 0 || $max <= 0) {
        return 0;
    }
    $count = 0;
    $greatest_piece = (int) str_pad(1, strlen($max.""), "0");
    $first_digit = (int) substr($max, 0, 1);

    if( $max <= 9 && $sum <= 9 ) {
        $count++;
    }
    else {
        $count_to = max(1, ($sum > $first_digit) ? $first_digit : $sum );
        for( $i = 0; $i < $count_to; $i++ ) {
            $count += count_numbers_with_digit_sum( $sum - $i, $greatest_piece - 1 );
        }
        $count += count_numbers_with_digit_sum( $sum - $count_to, $max % $greatest_piece );
    }
    return $count;
}

$start = microtime(true);
echo count_numbers_with_digit_sum( 6, 12345);
echo "<br/>";
echo (int)((microtime(true) - $start)*1000) )." ms";

一些示例运行:

count_numbers_with_digit_sum( 9,  100    ) = 10      0 ms
count_numbers_with_digit_sum( 6,  123    ) = 10      0 ms
count_numbers_with_digit_sum( 6,  12345  ) = 130     2 ms
count_numbers_with_digit_sum( 23, 12345  ) = 530     40 ms
count_numbers_with_digit_sum( 34, 582973 ) = 13923   2287 ms
count_numbers_with_digit_sum( 9,  984930 ) = 2001    39 ms

这是它的工作原理。我们将S(x,y) 称为小于或等于y 且其数字之和等于x 的不同正整数的数量。例如,S(3,25) = 3 (3, 12, 21)。我们可以做以下逻辑。为简单起见,我们只看 x=6 和 y=123,就像在您的原始示例中一样。 S 将是我们不“使用”第一个(数百个)数字的整数数量,加上我们使用的整数数量。在这个场景中,百位的最大值为 1,所以...

S(6,123) = S(6,99) + S(5,23)

看十位数,我们可以使用0到6之间的任何值,所以:

S(6,99) =   S(6,9) // single digits
          + S(5,9) // 10-19
          + S(4,9) // 20-29
          + S(3,9) // 30-39
          + S(2,9) // 40-49
          + S(1,9) // 50-59
          + S(0,9) // 60

这非常适合递归。对于给定的y,我们查看所有最左边的数字都是 0(即未使用)的场景,例如 S(x,9)、S(x,99)、S(x,999) 等。 ,然后我们看看“最大化”最左边的数字。例如,

S(34, 12345) = S(34, 9999) + S(34, 999) + S(34, 99) + S(34, 9) + S(33, 2345)
             = 10          + 0          + 0         + 0        + S(33, 2345)
             = 10 + S(33, 999) + S(33, 99) + S(33, 9) + S(31, 345)
             = 10 + 0          + 0         + 0        + S(31, 345)
             = 10 + S(31, 99) + S(31, 9) + S(31, 45)
             = 10 + 0         + 0        + 0
             = 10

上面的函数实现了这个递归。您可以轻松地调整此函数以处理大“数字”作为字符串值:

function count_numbers_with_digit_sum_string( $sum, $max ) {
    return count_numbers_with_digit_sum_string_inner( $sum."", $max."" );
}

function count_numbers_with_digit_sum_string_inner( $sum, $max ) {
    if($sum < 0 || $max <= 0) {
        return 0;
    }
    $count = 0;
    $strlenmax = strlen($max);
    $strlensum = strlen($sum);
    $nines_less_than_max = (int) str_pad("", $strlenmax-1, "9");
    $first_digit = (int) substr($max, 0, 1);
    $last_two_digits_sum = (int) substr($sum, -2);

    if( $strlenmax <= 1 && $strlensum <= 1 ) {
        $count++;
    }
    else {
        $count_to = max( 1, ($strlensum > 1 || $sum > $first_digit) ? $first_digit : ( (int) $sum ) );
        for( $i = 0; $i < $count_to; $i++ ) {
            $count += count_numbers_with_digit_sum_string_inner( substr($sum, 0, -2) .($last_two_digits_sum - $i), $nines_less_than_max );
        }
        $count += count_numbers_with_digit_sum_string_inner( substr($sum, 0, -2) .($last_two_digits_sum - $count_to), substr($max, 1));
    }
    return $count;
}
$start = microtime(true);
echo count_numbers_with_digit_sum_string( 34, 12345);
echo "<br>".( (int)((microtime(true) - $start)*1000) )." ms";

这个版本大约是上面纯数字版本的两倍。

您可以通过预先计算和缓存值来大大加快速度。正如您在上面看到的,有很多术语,如S(x, 999)S(x,9999) 等。只有 10 个 x (0..9) 值“起作用”,其中 y = 9、19 个值 (0.. 18) 适用于 y = 99, 28 (0..27) 适用于 y = 999 等。如果你想缓存 1,558 个值(9、99 中 y 的 S(x,y) 的所有组合, 999,最多 999,999,999,999,999,999),您可以减少大量的递归并节省大量时间。有了足够大的缓存,我相信您可以将 18 位数字的时间缩短到一秒。这很容易做到,通过建立它(缓存 9 个值,然后是 99,然后是 999,等等)。如果以后有时间,我会发布概念证明。

希望有帮助!

(已编辑以阐明缓存计算,修复 $count_to 中的计算错误)

【讨论】:

  • 这是迄今为止最详细和最好的答案,并且对问题提供了一些很好的见解,因为代码没有那么快,但我会尝试通过缓存和预定计算来实现它。跨度>
【解决方案2】:

这应该更快,因为它不使用正则表达式:

<?php

$number = 123;
$counter = 0;
$digitsum = 6; /* This is calculated by another function */

for($i=1;$i<=$number-1;$i++) {
    if(array_sum(str_split($i)) == $digitsum) {
        $counter++; 
    }
}

echo $counter;

【讨论】:

  • 不,这只会输出0
  • 对不起,我更正了。现在它可以工作了(至少如果预期的输出是 9 :))
  • 但是对于大数字来说仍然很慢:(
  • 我改了。现在速度更快了
  • 遗憾的是它甚至还没有接近:(我不确定是否可以在 1 秒内从 999 999 999 999 999 999 计算结果(例如):S
【解决方案3】:

也许这就是你想要的

function foo($digits, $digitsum) {
    $sum = 0;
    if(!empty($digits)) {
        if($digitsum > 1) {
            $current = array_shift($digits);
            if(count($digits) > 0) {
                if($digitsum >= $current) {
                    $sum += foo($digits, $digitsum - $current);
                    for($i = 0; $i < $current; $i++) {
                        $sum += foo(array_fill(0, count($digits), 9), $digitsum - $i);
                    }
                } else {
                    for($i = 0; $i <= $digitsum; $i++) {
                        $sum += foo(array_fill(0, count($digits), 9), $digitsum - $i);
                    }
                }
            } elseif($current >= $digitsum) {
                $sum += 1;
            }
        } elseif($digitsum == 1) {
            $sum += count($digits);
        } else {
            $sum += 1;
        }
    }
    return $sum;
}
$number   = 123;
$digitsum = 6;

$digit = strlen($number);
$digits = str_split($number);
$counter = foo($digits, $digitsum);
var_dump($counter);

【讨论】:

  • 这是迄今为止最快的解决方案,但是它最多只能处理 10 位数字,并且需要 20 秒。 9 位数字在 1 秒内工作。所以我认为唯一的方法是先缓存一些值然后计算它..
  • 我还没有解决数学问题,我相信当我解决这个问题时它会更快:)
【解决方案4】:

您可以尝试以下选项 C:

选项 A:(您的尝试)

$number   = 12345;
$counter  = 0;
$digitsum = 6; 

$start = microtime();

for ($i = 1; $i <= $number - 1; $i++) {
    if (array_sum(preg_split('//', $i)) == $digitsum) {
        $counter++; 
    }
}

echo (int)((microtime() - $start)*1000); // ~ 42 miliseconds at writecodeonline.com

选项 B:(来自here

$start = microtime();

$i = 0;
while ($i++ < $number) {
    if (array_sum(str_split($i)) == $digitsum) {
        $counter++; 
    }
}

echo (int)((microtime() - $start)*1000); // ~ 19 miliseconds at writecodeonline.com

选项 C:(来自here

$start = microtime();

$i = 0;
while ($i++ < $number) {
    $sum = 0;
    $n = $i;
    do {
        $sum += $n % 10;
    } while ($sum <= $digitsum and $n = (int) $n / 10);

    if ($sum == $digitsum) {
        $counter++; 
    }
} 
echo (int)((microtime() - $start)*1000); // ~ 5 miliseconds at writecodeonline.com

【讨论】:

  • 选项 C(就像我发布的代码一样)能够在 1 秒内处理多达 6 个数字,在 3-4 秒内处理 7 个数字,并且从第 8 个数字超时。所以它只是超时:(还有其他想法吗?
猜你喜欢
  • 2016-05-28
  • 2016-01-01
  • 1970-01-01
  • 2020-09-09
  • 1970-01-01
  • 2015-01-28
  • 1970-01-01
  • 2018-08-08
  • 1970-01-01
相关资源
最近更新 更多