删除 PHP 中区间数组中的封闭区间

Removing enclosed intervals in an array of intervals in PHP

我有这样一个区间数组,按下界排序($a[$i] <= $a[$i+1] 对应每个 $i),key l 是下限,键 h 是上限,我想删除所有间隔被较大间隔包围的行。

$a[0] = array('l' => 123, 'h'=>241);
$a[1] = array('l' => 250, 'h'=>360);
$a[2] = array('l' => 280, 'h'=>285);
$a[3] = array('l' => 310, 'h'=>310);
$a[4] = array('l' => 390, 'h'=>400);

所以我想得到的结果是

$a[0] = array('l' => 123, 'h'=>241);
$a[1] = array('l' => 250, 'h'=>360);
$a[2] = array('l' => 390, 'h'=>400);

这就是我尝试的

function dup($a){
  $c = count($a)-1;
  for ($i = $c; $i > 0; $i --){
    while ($a[$i]['h'] <= $a[$i-1]['h']){
        unset($a[$i]);
    }
  }
$a = array_values($a);
}

一种简单的方法,也许不是您想要的,但至少应该为您指明正确的方向。如果需要我可以完善它,只是有点忙,不想留下未回答的问题..

$out = [];

foreach ($a as $valA)
{
  $found = false;

  foreach ($a as $valB)
  {
    if (($valA['l'] > $valB['l']) && ($valA['h'] < $valB['h']))
    {
      $found = true;
      break;
    }
  }

  if (!$found)
  {
    $out[] = $valA;
  }
}

这完全未经测试,但最终应该只有 $out 中唯一的(大)范围。我在评论中提到的重叠未处理。

问题是 while 循环中缺少 break

function dup($a){
  $c = count($a)-1;
  for ($i = $c; $i > 0; $i --){
     while ($a[$i]['h'] <= $a[$i-1]['h']){
      unset($a[$i]);
      break; //here
     }
  }
  $a = array_values($a);
}

这是代码

function sort_by_low($item1,$item2){
    if($item1['l'] == $item2['l'])
        return 0;
    return ($item1['l']>$item2['l'])? -1:1;
}

usort($a,'sort_by_low');

for($i=0; $i<count($a); $i++){
  for($j=$i+1; $j<count($a);$j++){
     if($a[$i][l]<=$a[$j]['l'] && $a[$i][h]>=$a[$j]['h']){
        unset($a[$j]);
     }
  }
}

$a=array_values($a);

这是工作代码(已测试)

$result = array();
usort($a, function ($item1, $item2) {
    if ($item1['l'] == $item2['l']) return 0;
    return $item1['l'] < $item2['l'] ? -1 : 1;
});

foreach ($a as $element) {
    $exists = false;
    foreach ($result as $r) {
        if (($r['l'] < $element['l'] && $r['h'] > $element['h'])) {
            $exists = true;
            break;
        }
    }
    if (!$exists) {
        $result[] = $element;
    }
}

$result 将包含所需的结果

想到的第一个答案是由其他贡献者给出的不同变体:对于每个间隔,在每个间隔上循环寻找更大的封闭间隔。它易于理解和编写,而且确实有效。

这基本上是 n2 顺序,这意味着对于 n 个间隔,我们将进行 n*n 次循环。可以有一些技巧来优化它:

  • 当我们在嵌套循环中找到一个封闭区间时中断,如 user3137702 的回答,因为如果我们找到至少一个封闭区间,继续下去是没有用的
  • 避免在嵌套循环中循环相同的区间,因为我们知道区间不能严格包含在自身中(不重要)
  • 避免在嵌套循环中对已排除的间隔进行循环(可能会产生重大影响)
  • 按width = (h - l)升序循环区间(全局循环),因为越小的区间越有可能被其他区间包围,我们越早消除区间,下一个循环越有效(在我看来也很重要)
  • 按宽度降序搜索封闭区间(嵌套循环),因为较大的区间更有可能包含其他区间(我认为它也会产生重大影响)
  • 可能还有很多暂时没有想到的事情

现在让我说:

  • 如果我们时不时要计算几个间隔,那么优化并不重要,而当前接受的 user3137702 的答案可以解决问题
  • 为了开发合适的算法,无论如何有必要研究我们必须处理的数据的特征:在我们面前的情况下,区间分布如何?是否有很多封闭区间?这可以帮助从上面的列表中选择最有用的技巧。

出于教育目的,我想知道我们是否可以开发一种不同的算法来避免 n*n 顺序,随着您增加要计算的间隔数,运行宁时间必然会很快逐渐恶化。

"Virtual rule"算法

我想象这个算法我称之为"virtual rule"。

  • 在虚拟规则上放置区间的起点和终点
  • 运行 沿规则升序通过点
  • 在运行期间,注册开放与否
  • 当一个间隔开始和结束而另一个间隔之前打开并且仍然打开时,我们可以说它是封闭的
  • 所以当一个间隔结束时,检查它是否在其他当前打开的间隔之一之后打开,以及它是否在此间隔之前严格关闭。如果是,则附上!

我不认为这是最好的解决方案。但我们可以假设这比基本方法更快,因为尽管在循环期间要进行许多测试,但这是 n 顺序。

代码示例

我写了评论以使其尽可能清楚。

<?php
    function removeEnclosedIntervals_VirtualRule($a, $debug = false)
    {
        $rule = array();

        // place one point on a virtual rule for each low or up bound, refering to the interval's index in $a
        // virtual rule has 2 levels because there can be more than one point for a value
        foreach($a as $i => $interval)
        {
            $rule[$interval['l']][] = array('l', $i);
            $rule[$interval['h']][] = array('h', $i);
        }

        // used in the foreach loop
        $open = array();
        $enclosed = array();

        // loop through the points on the ordered virtual rule
        ksort($rule);
        foreach($rule as $points)
        {
            // Will register open intervals
            // When an interval starts and ends while another was opened before and is still open, it is enclosed

            // starts
            foreach($points as $point)
                if($point[0] == 'l')
                    $open[$point[1]] = $point[1]; // register it as open

            // ends
            foreach($points as $point)
            {
                if($point[0] == 'h')
                {
                    unset($open[$point[1]]); // UNregister it as open

                    // was it opened after a still open interval ?
                    foreach($open as $i)
                    {
                        if($a[$i]['l'] < $a[$point[1]]['l'])
                        {
                            // it is enclosed.

                            // is it *strictly* enclosed ?
                            if($a[$i]['h'] > $a[$point[1]]['h'])
                            {
                                // so this interval is strictly enclosed
                                $enclosed[$point[1]] = $point[1];

                                if($debug)
                                    echo debugPhrase(
                                        $point[1], //           $iEnclosed
                                        $a[$point[1]]['l'], //  $lEnclosed
                                        $a[$point[1]]['h'], //  $hEnclosed

                                        $i, //                  $iLarger
                                        $a[$i]['l'], //         $lLarger
                                        $a[$i]['h'] //          $hLarger
                                    );

                                break;
                            }
                        }
                    }
                }
            }
        }

        // obviously
        foreach($enclosed as $i)
            unset($a[$i]);

        return $a;
    }
?>

以基本方法为基准

  • 它 运行s 测试 运行domly 生成的间隔
  • 基本方法毫无疑问有效。比较这两种方法的结果可以让我预防 "VirtualRule" 方法有效,因为据我测试,它返回相同的结果

    // * include removeEnclosingIntervals_VirtualRule function *
    
    // arbitrary range for intervals start and end
    // Note that it could be interesting to do benchmarking with different MIN and MAX values !
    define('MIN', 0);
    define('MAX', 500);
    
    // Benchmarking params
    define('TEST_MAX_NUMBER', 100000);
    define('TEST_BY_STEPS_OF', 100);
    
    // from http://php.net/manual/en/function.microtime.php
    // used later for benchmarking purpose
    function microtime_float()
    {
        list($usec, $sec) = explode(" ", microtime());
        return ((float)$usec + (float)$sec);
    }
    
    function debugPhrase($iEnclosed, $lEnclosed, $hEnclosed, $iLarger, $lLarger, $hLarger)
    {
        return '('.$iEnclosed.')['.$lEnclosed.' ; '.$hEnclosed.'] is strictly enclosed at least in ('.$iLarger.')['.$lLarger.' ; '.$hLarger.']'.PHP_EOL;
    }
    
    // 2 foreach loops solution (based on user3137702's *damn good* work ;) and currently accepted answer)
    function removeEnclosedIntervals_Basic($a, $debug = false)
    {
        foreach ($a as $i => $valA)
        {
            $found = false;
    
            foreach ($a as $j => $valB)
            {
                if (($valA['l'] > $valB['l']) && ($valA['h'] < $valB['h']))
                {
                    $found = true;
    
                    if($debug)
                        echo debugPhrase(
                            $i, //                  $iEnclosed
                            $a[$i]['l'], //         $lEnclosed
                            $a[$i]['h'], //         $hEnclosed
    
                            $j, //                  $iLarger
                            $a[$j]['l'], //         $lLarger
                            $a[$j]['h'] //          $hLarger
                        );
    
                    break;
                }
            }
    
            if (!$found)
            {
                $out[$i] = $valA;
            }
        }
    
        return $out;
    }
    
    // runs a benchmark with $number intervals
    function runTest($number)
    {
        // Generating a random set of intervals with values between MIN and MAX
        $randomSet = array();
        for($i=0; $i<$number; $i++)
            // avoiding self-closing intervals
            $randomSet[] = array(
                'l' => ($l = mt_rand(MIN, MAX-2)),
                'h' => mt_rand($l+1, MAX)
            );
    
        /* running the two methods and comparing results and execution time */
    
        // Basic method
        $start = microtime_float();
        $Basic_result = removeEnclosedIntervals_Basic($randomSet);
        $end = microtime_float();
        $Basic_time = $end - $start;
    
        // VirtualRule
        $start = microtime_float();
        $VirtualRule_result = removeEnclosedIntervals_VirtualRule($randomSet);
        $end = microtime_float();
        $VirtualRule_time = $end - $start;
    
        // Basic method works for sure.
        // If results are the same, comparing execution time. If not, sh*t happened !
        if(md5(var_export($VirtualRule_result, true)) == md5(var_export($VirtualRule_result, true)))
            echo $number.';'.$Basic_time.';'.$VirtualRule_time.PHP_EOL;
        else
        {
            echo '/;/;/;Work harder, results are not the same ! Cant say anything !'.PHP_EOL;
            stop;
        }
    }
    
    // CSV header
    echo 'Number of intervals;Basic method exec time (s);VirtualRule method exec time (s)'.PHP_EOL;
    
    for($n=TEST_BY_STEPS_OF; $n<TEST_MAX_NUMBER; $n+=TEST_BY_STEPS_OF)
    {
        runTest($n);
        flush();
    }
    

结果(对我来说)

  • 正如我所想,获得了明显不同的性能。
  • 我 运行 在 PHP5 的酷睿 i7 计算机和 PHP7 的(旧)AMD 四核计算机上进行了测试。在我的系统上,这两个版本之间的性能存在明显差异!原则上可以用 PHP 版本的差异来解释,因为 运行ning PHP5 的计算机功能更强大...