【问题标题】:Tetris-ing an array俄罗斯方块
【发布时间】:2010-07-18 11:04:15
【问题描述】:

考虑以下数组:

/www/htdocs/1/sites/lib/abcdedd
/www/htdocs/1/sites/conf/xyz
/www/htdocs/1/sites/conf/abc/def
/www/htdocs/1/sites/htdocs/xyz
/www/htdocs/1/sites/lib2/abcdedd

什么是检测公共基本路径的最短和最优雅的方法 - 在这种情况下

/www/htdocs/1/sites/

并从数组中的所有元素中删除它?

lib/abcdedd
conf/xyz
conf/abc/def
htdocs/xyz
lib2/abcdedd

【问题讨论】:

  • 这可能值得一试:en.wikibooks.org/wiki/Algorithm_implementation/Strings/…(我试过了,效果很好)。
  • 哇!这么多精彩的输入。我会拿一个来解决我手头的问题,但我觉得要真正选择一个合理的接受答案,我必须比较解决方案。我可能需要一段时间才能开始这样做,但我一定会的。
  • 娱乐标题 :D 顺便说一句:为什么我在提名的版主名单上找不到你? @Pekka
  • 两年没有接受的答案?
  • @Pekka 已经快三年了,因为没有公认的答案:(而且这是一个很棒的标题,我刚才记得它并在谷歌上搜索“tetrising an array”。

标签: php string algorithm


【解决方案1】:

编写一个函数longest_common_prefix,将两个字符串作为输入。然后以任何顺序将其应用于字符串,以将它们减少为它们的公共前缀。由于它是关联和可交换的,因此顺序对结果并不重要。

这与其他二元运算(例如加法或最大公约数)相同。

【讨论】:

  • +1。比较前 2 个字符串后,使用结果(公共路径)与第 3 个字符串进行比较,依此类推。
【解决方案2】:

将它们加载到 trie 数据结构中。从父节点开始,查看哪个子节点的数量大于一个。找到那个魔法节点后,只需拆除父节点结构,将当前节点作为根节点即可。

【讨论】:

  • 将数据加载到您描述的特里树结构中的操作不会包含查找最长公共前缀的算法,因此实际上不需要使用树结构吗?即,当您在构建树时可以检测到时,为什么要检查树是否有多个孩子。那为什么是一棵树呢?我的意思是如果你已经从一个数组开始。如果您可以将存储更改为仅使用 trie 而不是数组,我想这是有道理的。
  • 我认为如果你小心点,那么我的解决方案比构建 trie 更有效。
  • 这个答案是错误的。在我的答案和其他答案中发布了 O(n) 的简单解决方案。
  • @el.pescado:在最坏的情况下,尝试的大小是源字符串的长度的二次方。
【解决方案3】:
$common = PHP_INT_MAX;
foreach ($a as $item) {
        $common = min($common, str_common($a[0], $item, $common));
}

$result = array();
foreach ($a as $item) {
        $result[] = substr($item, $common);
}
print_r($result);

function str_common($a, $b, $max)
{
        $pos = 0;
        $last_slash = 0;
        $len = min(strlen($a), strlen($b), $max + 1);
        while ($pos < $len) {
                if ($a{$pos} != $b{$pos}) return $last_slash;
                if ($a{$pos} == '/') $last_slash = $pos;
                $pos++;
        }
        return $last_slash;
}

【讨论】:

  • 这是迄今为止发布的最佳解决方案,但需要改进。它没有考虑以前最长的公共路径(可能迭代更多的字符串而不是必要的),也没有考虑路径(所以对于/usr/lib/usr/lib2 它给/usr/lib 作为最长公共路径,而不是/usr/)。我(希望)修复了两者。
【解决方案4】:

好吧,考虑到您可以在这种情况下使用XOR 来查找字符串的公共部分。每当您对两个相同的字节进行异或运算时,您都会得到一个空字节作为输出。所以我们可以利用它来发挥我们的优势:

$first = $array[0];
$length = strlen($first);
$count = count($array);
for ($i = 1; $i < $count; $i++) {
    $length = min($length, strspn($array[$i] ^ $first, chr(0)));
}

在单次循环之后,$length 变量将等于字符串数组之间最长的公共基础部分。然后,我们可以从第一个元素中提取公共部分:

$common = substr($array[0], 0, $length);

你有它。作为一个函数:

function commonPrefix(array $strings) {
    $first = $strings[0];
    $length = strlen($first);
    $count = count($strings);
    for ($i = 1; $i < $count; $i++) {
        $length = min($length, strspn($strings[$i] ^ $first, chr(0)));
    }
    return substr($first, 0, $length);
}

请注意,它确实使用了不止一次的迭代,但这些迭代是在库中完成的,因此在解释型语言中这将大大提高效率...

现在,如果您只需要完整路径,我们需要截断到最后一个 / 字符。所以:

$prefix = preg_replace('#/[^/]*$', '', commonPrefix($paths));

现在,它可能会过度剪切两个字符串,例如 /foo/bar/foo/bar/baz 将被剪切为 /foo。但是没有添加另一轮迭代来确定下一个字符是/ 还是字符串结尾,我看不到解决方法......

【讨论】:

    【解决方案5】:

    一种天真的方法是在/ 处分解路径并连续比较数组中的每个元素。所以例如第一个元素在所有数组中都是空的,因此将被删除,下一个元素将是www,在所有数组中都相同,因此将被删除,等等。

    类似(未测试

    $exploded_paths = array();
    
    foreach($paths as $path) {
        $exploded_paths[] = explode('/', $path);
    }
    
    $equal = true;
    $ref = &$exploded_paths[0]; // compare against the first path for simplicity
    
    while($equal) {   
        foreach($exploded_paths as $path_parts) {
            if($path_parts[0] !== $ref[0]) {
                $equal = false;
                break;
            }
        }
        if($equal) {
            foreach($exploded_paths as &$path_parts) {
                array_shift($path_parts); // remove the first element
            }
        }
    }
    

    之后,您只需再次内爆$exploded_paths 中的元素:

    function impl($arr) {
        return '/' . implode('/', $arr);
    }
    $paths = array_map('impl', $exploded_paths);
    

    这给了我:

    Array
    (
        [0] => /lib/abcdedd
        [1] => /conf/xyz
        [2] => /conf/abc/def
        [3] => /htdocs/xyz
        [4] => /conf/xyz
    )
    

    这可能无法很好地扩展;)

    【讨论】:

      【解决方案6】:

      好的,我不确定这是否防弹,但我认为它有效:

      echo array_reduce($array, function($reducedValue, $arrayValue) {
          if($reducedValue === NULL) return $arrayValue;
          for($i = 0; $i < strlen($reducedValue); $i++) {
              if(!isset($arrayValue[$i]) || $arrayValue[$i] !== $reducedValue[$i]) {
                  return substr($reducedValue, 0, $i);
              }
          }
          return $reducedValue;
      });
      

      这会将数组中的第一个值作为参考字符串。然后它将遍历参考字符串并将每个字符与第二个字符串在同一位置的字符进行比较。如果一个字符不匹配,参考字符串将被缩短到字符的位置,并比较下一个字符串。该函数将返回最短的匹配字符串。

      性能取决于给定的字符串。参考字符串越早越短,代码完成的速度就越快。不过,我真的不知道如何将其放入公式中。

      我发现 Artefacto 对字符串进行排序的方法提高了性能。添加

      asort($array);
      $array = array(array_shift($array), array_pop($array));
      

      array_reduce 之前会显着提高性能。

      还请注意,这将返回最长匹配的初始子字符串,它更通用,但不会为您提供公共路径。你必须跑

      substr($result, 0, strrpos($result, '/'));
      

      关于结果。然后您可以使用结果删除值

      print_r(array_map(function($v) use ($path){
          return str_replace($path, '', $v);
      }, $array));
      

      应该给出:

      [0] => /lib/abcdedd
      [1] => /conf/xyz/
      [2] => /conf/abc/def
      [3] => /htdocs/xyz
      [4] => /lib2/abcdedd
      

      欢迎反馈。

      【讨论】:

        【解决方案7】:

        您可以最快的方式删除前缀,每个字符只读取一次:

        function findLongestWord($lines, $delim = "/")
        {
            $max = 0;
            $len = strlen($lines[0]); 
        
            // read first string once
            for($i = 0; $i < $len; $i++) {
                for($n = 1; $n < count($lines); $n++) {
                    if($lines[0][$i] != $lines[$n][$i]) {
                        // we've found a difference between current token
                        // stop search:
                        return $max;
                    }
                }
                if($lines[0][$i] == $delim) {
                    // we've found a complete token:
                    $max = $i + 1;
                }
            }
            return $max;
        }
        
        $max = findLongestWord($lines);
        // cut prefix of len "max"
        for($n = 0; $n < count($lines); $n++) {
            $lines[$n] = substr(lines[$n], $max, $len);
        }
        

        【讨论】:

        【解决方案8】:

        这具有不具有线性时间复杂度的优点;但是,在大多数情况下,排序肯定不会是花费更多时间的操作。

        基本上,这里聪明的部分(至少我找不到它的错误)是在排序后您只需将第一条路径与最后一条路径进行比较。

        sort($a);
        $a = array_map(function ($el) { return explode("/", $el); }, $a);
        $first = reset($a);
        $last = end($a);
        for ($eqdepth = 0; $first[$eqdepth] === $last[$eqdepth]; $eqdepth++) {}
        array_walk($a,
            function (&$el) use ($eqdepth) {
                for ($i = 0; $i < $eqdepth; $i++) {
                    array_shift($el);
                }
             });
        $res = array_map(function ($el) { return implode("/", $el); }, $a);
        

        【讨论】:

          【解决方案9】:
          $values = array('/www/htdocs/1/sites/lib/abcdedd',
                          '/www/htdocs/1/sites/conf/xyz',
                          '/www/htdocs/1/sites/conf/abc/def',
                          '/www/htdocs/1/sites/htdocs/xyz',
                          '/www/htdocs/1/sites/lib2/abcdedd'
          );
          
          
          function splitArrayValues($r) {
              return explode('/',$r);
          }
          
          function stripCommon($values) {
              $testValues = array_map('splitArrayValues',$values);
          
              $i = 0;
              foreach($testValues[0] as $key => $value) {
                  foreach($testValues as $arraySetValues) {
                      if ($arraySetValues[$key] != $value) break 2;
                  }
                  $i++;
              }
          
              $returnArray = array();
              foreach($testValues as $value) {
                  $returnArray[] = implode('/',array_slice($value,$i));
              }
          
              return $returnArray;
          }
          
          
          $newValues = stripCommon($values);
          
          echo '<pre>';
          var_dump($newValues);
          echo '</pre>';
          

          编辑我使用 array_walk 重建数组的原始方法的变体

          $values = array('/www/htdocs/1/sites/lib/abcdedd',
                          '/www/htdocs/1/sites/conf/xyz',
                          '/www/htdocs/1/sites/conf/abc/def',
                          '/www/htdocs/1/sites/htdocs/xyz',
                          '/www/htdocs/1/sites/lib2/abcdedd'
          );
          
          
          function splitArrayValues($r) {
              return explode('/',$r);
          }
          
          function rejoinArrayValues(&$r,$d,$i) {
              $r = implode('/',array_slice($r,$i));
          }
          
          function stripCommon($values) {
              $testValues = array_map('splitArrayValues',$values);
          
              $i = 0;
              foreach($testValues[0] as $key => $value) {
                  foreach($testValues as $arraySetValues) {
                      if ($arraySetValues[$key] != $value) break 2;
                  }
                  $i++;
              }
          
              array_walk($testValues, 'rejoinArrayValues', $i);
          
              return $testValues;
          }
          
          
          $newValues = stripCommon($values);
          
          echo '<pre>';
          var_dump($newValues);
          echo '</pre>';
          

          编辑

          最有效和最优雅的答案可能涉及从每个提供的答案中获取函数和方法

          【讨论】:

            【解决方案10】:

            我会 explode 基于 / 的值,然后使用 array_intersect_assoc 来检测公共元素并确保它们在数组中具有正确的对应索引。生成的数组可以重新组合以生成公共路径。

            function getCommonPath($pathArray)
            {
                $pathElements = array();
            
                foreach($pathArray as $path)
                {
                    $pathElements[] = explode("/",$path);
                }
            
                $commonPath = $pathElements[0];
            
                for($i=1;$i<count($pathElements);$i++)
                {
                    $commonPath = array_intersect_assoc($commonPath,$pathElements[$i]);
                }
            
                if(is_array($commonPath) return implode("/",$commonPath);
                else return null;
            }
            
            function removeCommonPath($pathArray)
            {
                $commonPath = getCommonPath($pathArray());
            
                for($i=0;$i<count($pathArray);$i++)
                {
                    $pathArray[$i] = substr($pathArray[$i],str_len($commonPath));
                }
            
                return $pathArray;
            }
            

            这是未经测试的,但是,其想法是 $commonPath 数组只包含路径的元素,这些元素已包含在已与之比较的所有路径数组中。循环完成后,我们只需将其与 / 重新组合即可得到真正的$commonPath

            更新 正如 Felix Kling 所指出的,array_intersect 不会考虑具有共同元素但顺序不同的路径......为了解决这个问题,我使用了 array_intersect_assoc 而不是 array_intersect

            更新 添加了代码以从数组中删除公共路径(或俄罗斯方块!)。

            【讨论】:

            • 这可能行不通。考虑/a/b/c/d/d/c/b/a。相同的元素,不同的路径。
            • @Felix Kling 我已经更新为使用 array_intersect_assoc,它也执行索引检查
            【解决方案11】:

            如果仅从字符串比较的角度来看,问题可以简化。这可能比数组拆分更快:

            $longest = $tetris[0];  # or array_pop()
            foreach ($tetris as $cmp) {
                    while (strncmp($longest+"/", $cmp, strlen($longest)+1) !== 0) {
                            $longest = substr($longest, 0, strrpos($longest, "/"));
                    }
            }
            

            【讨论】:

            • 那行不通,例如使用此设置数组('/www/htdocs/1/sites/conf/abc/def', '/www/htdocs/1/sites/htdocs/xyz', '/www/htdocs/1/sitesjj/lib2/abcdedd ',)。
            • @Artefacto:你是对的。所以我只是简单地将它修改为在比较中始终包含一个斜杠“/”。使其不含糊。
            【解决方案12】:

            也许移植 Python 的 os.path.commonprefix(m) 使用的算法会起作用?

            def commonprefix(m):
                "Given a list of pathnames, returns the longest common leading component"
                if not m: return ''
                s1 = min(m)
                s2 = max(m)
                n = min(len(s1), len(s2))
                for i in xrange(n):
                    if s1[i] != s2[i]:
                        return s1[:i]
                return s1[:n]
            

            也就是说,呃……类似的东西

            function commonprefix($m) {
              if(!$m) return "";
              $s1 = min($m);
              $s2 = max($m);
              $n = min(strlen($s1), strlen($s2));
              for($i=0;$i<$n;$i++) if($s1[$i] != $s2[$i]) return substr($s1, 0, $i);
              return substr($s1, 0, $n);
            }
            

            之后,您可以将原始列表的每个元素以公共前缀的长度作为起始偏移量。

            【讨论】:

              【解决方案13】:

              我会把我的帽子扔进擂台……

              function longestCommonPrefix($a, $b) {
                  $i = 0;
                  $end = min(strlen($a), strlen($b));
                  while ($i < $end && $a[$i] == $b[$i]) $i++;
                  return substr($a, 0, $i);
              }
              
              function longestCommonPrefixFromArray(array $strings) {
                  $count = count($strings);
                  if (!$count) return '';
                  $prefix = reset($strings);
                  for ($i = 1; $i < $count; $i++)
                      $prefix = longestCommonPrefix($prefix, $strings[$i]);
                  return $prefix;
              }
              
              function stripPrefix(&$string, $foo, $length) {
                  $string = substr($string, $length);
              }
              

              用法:

              $paths = array(
                  '/www/htdocs/1/sites/lib/abcdedd',
                  '/www/htdocs/1/sites/conf/xyz',
                  '/www/htdocs/1/sites/conf/abc/def',
                  '/www/htdocs/1/sites/htdocs/xyz',
                  '/www/htdocs/1/sites/lib2/abcdedd',
              );
              
              $longComPref = longestCommonPrefixFromArray($paths);
              array_walk($paths, 'stripPrefix', strlen($longComPref));
              print_r($paths);
              

              【讨论】:

                【解决方案14】:

                嗯,这里已经有一些解决方案了,只是因为它很有趣:

                $values = array(
                    '/www/htdocs/1/sites/lib/abcdedd',
                    '/www/htdocs/1/sites/conf/xyz',
                    '/www/htdocs/1/sites/conf/abc/def', 
                    '/www/htdocs/1/sites/htdocs/xyz',
                    '/www/htdocs/1/sites/lib2/abcdedd' 
                );
                
                function findCommon($values){
                    $common = false;
                    foreach($values as &$p){
                        $p = explode('/', $p);
                        if(!$common){
                            $common = $p;
                        } else {
                            $common = array_intersect_assoc($common, $p);
                        }
                    }
                    return $common;
                }
                function removeCommon($values, $common){
                    foreach($values as &$p){
                        $p = explode('/', $p);
                        $p = array_diff_assoc($p, $common);
                        $p = implode('/', $p);
                    }
                
                    return $values;
                }
                
                echo '<pre>';
                print_r(removeCommon($values, findCommon($values)));
                echo '</pre>';
                

                输出:

                Array
                (
                    [0] => lib/abcdedd
                    [1] => conf/xyz
                    [2] => conf/abc/def
                    [3] => htdocs/xyz
                    [4] => lib2/abcdedd
                )
                

                【讨论】:

                  【解决方案15】:
                  $arrMain = array(
                              '/www/htdocs/1/sites/lib/abcdedd',
                              '/www/htdocs/1/sites/conf/xyz',
                              '/www/htdocs/1/sites/conf/abc/def',
                              '/www/htdocs/1/sites/htdocs/xyz',
                              '/www/htdocs/1/sites/lib2/abcdedd'
                  );
                  function explodePath( $strPath ){ 
                      return explode("/", $strPath);
                  }
                  
                  function removePath( $strPath)
                  {
                      global $strCommon;
                      return str_replace( $strCommon, '', $strPath );
                  }
                  $arrExplodedPaths = array_map( 'explodePath', $arrMain ) ;
                  
                  //Check for common and skip first 1
                  $strCommon = '';
                  for( $i=1; $i< count( $arrExplodedPaths[0] ); $i++)
                  {
                      for( $j = 0; $j < count( $arrExplodedPaths); $j++ )
                      {
                          if( $arrExplodedPaths[0][ $i ] !== $arrExplodedPaths[ $j ][ $i ] )
                          {
                              break 2;
                          } 
                      }
                      $strCommon .= '/'.$arrExplodedPaths[0][$i];
                  }
                  print_r( array_map( 'removePath', $arrMain ) );
                  

                  这很好用...类似于 mark baker 但使用 str_replace

                  【讨论】:

                    【解决方案16】:

                    可能太天真和幼稚,但它确实有效。我用过this algorithm:

                    <?php
                    
                    function strlcs($str1, $str2){
                        $str1Len = strlen($str1);
                        $str2Len = strlen($str2);
                        $ret = array();
                    
                        if($str1Len == 0 || $str2Len == 0)
                            return $ret; //no similarities
                    
                        $CSL = array(); //Common Sequence Length array
                        $intLargestSize = 0;
                    
                        //initialize the CSL array to assume there are no similarities
                        for($i=0; $i<$str1Len; $i++){
                            $CSL[$i] = array();
                            for($j=0; $j<$str2Len; $j++){
                                $CSL[$i][$j] = 0;
                            }
                        }
                    
                        for($i=0; $i<$str1Len; $i++){
                            for($j=0; $j<$str2Len; $j++){
                                //check every combination of characters
                                if( $str1[$i] == $str2[$j] ){
                                    //these are the same in both strings
                                    if($i == 0 || $j == 0)
                                        //it's the first character, so it's clearly only 1 character long
                                        $CSL[$i][$j] = 1; 
                                    else
                                        //it's one character longer than the string from the previous character
                                        $CSL[$i][$j] = $CSL[$i-1][$j-1] + 1; 
                    
                                    if( $CSL[$i][$j] > $intLargestSize ){
                                        //remember this as the largest
                                        $intLargestSize = $CSL[$i][$j]; 
                                        //wipe any previous results
                                        $ret = array();
                                        //and then fall through to remember this new value
                                    }
                                    if( $CSL[$i][$j] == $intLargestSize )
                                        //remember the largest string(s)
                                        $ret[] = substr($str1, $i-$intLargestSize+1, $intLargestSize);
                                }
                                //else, $CSL should be set to 0, which it was already initialized to
                            }
                        }
                        //return the list of matches
                        return $ret;
                    }
                    
                    
                    $arr = array(
                    '/www/htdocs/1/sites/lib/abcdedd',
                    '/www/htdocs/1/sites/conf/xyz',
                    '/www/htdocs/1/sites/conf/abc/def',
                    '/www/htdocs/1/sites/htdocs/xyz',
                    '/www/htdocs/1/sites/lib2/abcdedd'
                    );
                    
                    // find the common substring
                    $longestCommonSubstring = strlcs( $arr[0], $arr[1] );
                    
                    // remvoe the common substring
                    foreach ($arr as $k => $v) {
                        $arr[$k] = str_replace($longestCommonSubstring[0], '', $v);
                    }
                    var_dump($arr);
                    

                    输出:

                    array(5) {
                      [0]=>
                      string(11) "lib/abcdedd"
                      [1]=>
                      string(8) "conf/xyz"
                      [2]=>
                      string(12) "conf/abc/def"
                      [3]=>
                      string(10) "htdocs/xyz"
                      [4]=>
                      string(12) "lib2/abcdedd"
                    }
                    

                    :)

                    【讨论】:

                    • @Doomsday 我的回答中有一个维基百科的链接...在发表评论之前先尝试阅读它。
                    • 我认为最后你只比较前两条路径。在您的示例中,这有效,但如果您删除第一个路径,它将找到 /www/htdocs/1/sites/conf/ 作为常见匹配项。此外,该算法搜索从字符串中任何位置开始的子字符串,但是对于这个问题,您知道您可以从位置 0 开始,这使得它更简单。
                    猜你喜欢
                    • 1970-01-01
                    • 2013-03-17
                    • 1970-01-01
                    • 1970-01-01
                    • 2018-06-06
                    • 1970-01-01
                    • 1970-01-01
                    • 2010-12-30
                    • 2011-07-30
                    相关资源
                    最近更新 更多