Tetris-ing um array

99

Considere a seguinte matriz:

/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

qual é a maneira mais curta e elegante de detectar o caminho de base comum - neste caso

/www/htdocs/1/sites/

e removê-lo de todos os elementos da matriz?

lib/abcdedd
conf/xyz
conf/abc/def
htdocs/xyz
lib2/abcdedd
Pekka
fonte
4
Pode valer a pena tentar: en.wikibooks.org/wiki/Algorithm_implementation/Strings/… (Eu tentei e funciona).
Richard Knop
1
Awwww! Muitas contribuições brilhantes. Vou pegar uma para resolver meu problema em mãos, mas sinto que para realmente escolher uma resposta aceita e justificada, terei que comparar as soluções. Pode demorar um pouco até que eu comece a fazer isso, mas certamente o farei.
Pekka
título divertido: D btw: por que não consigo te encontrar na lista de moderadores indicados? @Pekka
The Surrican
2
nenhuma resposta aceita por dois anos?
Gordon de
1
@Pekka Quase três anos sem resposta aceita :( E é um título tão incrível que me lembrei há pouco e pesquisei "tetrising an array" no Google.
Camilo Martin

Respostas:

35

Escreva uma função longest_common_prefixque receba duas strings como entrada. Em seguida, aplique-o às strings em qualquer ordem para reduzi-las a seu prefixo comum. Por ser associativa e comutativa, a ordem não importa para o resultado.

Este é o mesmo que para outras operações binárias como, por exemplo, adição ou maior divisor comum.

azul da estrela
fonte
8
+1. Depois de comparar as 2 primeiras strings, use o resultado (caminho comum) para comparar com a terceira string e assim por diante.
Milan Babuškov
23

Carregue-os em uma estrutura de dados trie. Começando pelo nó pai, veja qual é a contagem de filhos maior do que um. Depois de encontrar esse nó mágico, basta desmontar a estrutura do nó pai e ter o nó atual como raiz.

fanfarrão
fonte
10
A operação que carrega os dados na estrutura de árvore trie que você descreve não incluiria o algoritmo para encontrar o prefixo comum mais longo, tornando desnecessário o uso de uma estrutura de árvore? Ou seja, por que verificar a árvore para vários filhos quando você pode detectar isso ao construir a árvore. Por que então uma árvore? Quero dizer, se você já começar com uma matriz. Se você pode alterar o armazenamento para usar apenas um trie em vez de arrays, acho que faz sentido.
Ben Schwehn
2
Acho que, se você for cuidadoso, minha solução será mais eficiente do que construir um teste.
starblue
Esta resposta está errada. Existem soluções triviais postadas em minhas e em outras respostas que são O (n).
Ari Ronen
@ el.pescado: As tentativas são quadrádicas em tamanho com o comprimento da string de origem no pior caso.
Billy ONeal
10
$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;
}
Sjoerd
fonte
Esta é de longe a melhor solução postada, mas precisava de melhorias. Ele não levou em consideração o caminho comum mais longo anterior (possivelmente iterando mais da string do que o necessário) e não levou em consideração os caminhos (portanto, para /usr/libe /usr/lib2deu /usr/libcomo o caminho comum mais longo, em vez de /usr/). Eu (espero) consertei ambos.
Gabe
7

Bem, considerando que você pode usar XORnesta situação para encontrar as partes comuns da corda. Sempre que você xou dois bytes iguais, obtém um byte nulo como saída. Portanto, podemos usar isso a nosso favor:

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

Após esse único loop, a $lengthvariável será igual à parte de base comum mais longa entre a matriz de strings. Então, podemos extrair a parte comum do primeiro elemento:

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

E aí está. Como uma função:

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);
}

Note que ele usa mais de uma iteração, mas essas iterações são feitas em bibliotecas, então em linguagens interpretadas isso terá um grande ganho de eficiência ...

Agora, se você quiser apenas caminhos completos, precisamos truncar para o último /caractere. Assim:

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

Agora, ele pode cortar excessivamente duas cordas, como /foo/bare /foo/bar/bazserá cortado /foo. Mas, além de adicionar outra rodada de iteração para determinar se o próximo caractere é um / ou outro , não consigo ver uma maneira de contornar isso ...

ircmaxell
fonte
3

Uma abordagem ingênua seria explodir os caminhos na /comparação sucessiva de todos os elementos nas matrizes. Então, por exemplo, o primeiro elemento estaria vazio em todas as matrizes, então ele será removido, o próximo elemento seráwww , é o mesmo em todos os arrays, então ele será removido, etc.

Algo como (não testado)

$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
        }
    }
}

Depois, você só precisa implodir os elementos $exploded_pathsnovamente:

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

O que me dá:

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

Isso pode não escalar bem;)

Felix Kling
fonte
3

Ok, não tenho certeza se isso é à prova de balas, mas acho que funciona:

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;
});

Isso tomará o primeiro valor da matriz como string de referência. Em seguida, ele itera sobre a string de referência e compara cada caractere com o caractere da segunda string na mesma posição. Se um char não corresponder, a string de referência será encurtada para a posição do char e a próxima string será comparada. A função retornará a string correspondente mais curta.

O desempenho depende das cordas fornecidas. Quanto mais cedo a string de referência ficar mais curta, mais rápido o código terminará. Eu realmente não tenho ideia de como colocar isso em uma fórmula.

Descobri que a abordagem da Artefacto para classificar as cordas aumenta o desempenho. Adicionando

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

antes de o array_reduce aumentará significativamente o desempenho.

Observe também que isso retornará a substring inicial correspondente mais longa , que é mais versátil, mas não fornecerá o caminho comum . Voce tem que correr

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

no resultado. E então você pode usar o resultado para remover os valores

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

que deve dar:

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

Feedback bem-vindo.

Gordon
fonte
3

Você pode remover o prefixo da maneira mais rápida, lendo cada caractere apenas uma vez:

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);
}
Apocalipse
fonte
Na verdade, uma comparação baseada em caracteres será a mais rápida. Todas as outras soluções usam operadores "caros" que, no final, também farão (múltiplas) comparações de caracteres. Foi até mencionado nas escrituras do Santo Joel !
Jan Fabry
2

Isso tem a vantagem de não ter complexidade de tempo linear; entretanto, na maioria dos casos, a classificação definitivamente não será a operação demorando mais.

Basicamente, a parte inteligente (pelo menos não consegui encontrar uma falha nisso) aqui é que depois de classificar você só terá que comparar o primeiro caminho com o último.

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);
Artefacto
fonte
2
$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>';

EDITAR Variante do meu método original usando um array_walk para reconstruir o array

$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>';

EDITAR

A resposta mais eficiente e elegante provavelmente envolve a obtenção de funções e métodos de cada uma das respostas fornecidas

Mark Baker
fonte
1

Gostaria de explodeusar os valores com base em / e, em seguida, usar array_intersect_assocpara detectar os elementos comuns e garantir que eles tenham o índice correspondente correto na matriz. A matriz resultante pode ser recombinada para produzir o caminho comum.

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;
}

Isso não foi testado, mas a ideia é que a $commonPathmatriz sempre contém apenas os elementos do caminho que foram contidos em todas as matrizes de caminho que foram comparadas a ela. Quando o loop está completo, simplesmente o recombinamos com / para obter o verdadeiro$commonPath

Atualização Como apontado por Felix Kling, array_intersectnão considerarei caminhos que tenham elementos comuns, mas em ordens diferentes ... Para resolver isso, usei em array_intersect_assocvez dearray_intersect

Atualizar o código adicionado para remover o caminho comum (ou tetris it!) Do array também.

Brendan Bullen
fonte
Isso provavelmente não funcionará. Considere /a/b/c/de /d/c/b/a. Mesmos elementos, caminhos diferentes.
Felix Kling
@Felix Kling Eu atualizei para usar array_intersect_assoc que também executa uma verificação de índice
Brendan Bullen
1

O problema pode ser simplificado apenas visto do ângulo de comparação das cordas. Provavelmente, isso é mais rápido do que a divisão de matriz:

$longest = $tetris[0];  # or array_pop()
foreach ($tetris as $cmp) {
        while (strncmp($longest+"/", $cmp, strlen($longest)+1) !== 0) {
                $longest = substr($longest, 0, strrpos($longest, "/"));
        }
}
mario
fonte
Isso não funcionará, por exemplo, com este array definido ('/ www / htdocs / 1 / sites / conf / abc / def', '/ www / htdocs / 1 / sites / htdocs / xyz', '/ www / htdocs / 1 / sitesjj / lib2 / abcdedd ',).
Artefacto
@Artefacto: Você estava certo. Portanto, eu simplesmente o modifiquei para sempre incluir uma barra final "/" na comparação. Torna não ambíguo.
mario
1

Talvez a portabilidade do algoritmo os.path.commonprefix(m)usado pelo Python funcione?

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]

Isso é, uh ... algo como

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);
}

Depois disso, você pode apenas substring cada elemento da lista original com o comprimento do prefixo comum como o deslocamento inicial.

AKX
fonte
1

Vou jogar meu chapéu no ringue ...

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);
}

Uso:

$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);
rik
fonte
1

Bem, já existem algumas soluções aqui, mas, só porque era divertido:

$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>';

Resultado:

Array
(
    [0] => lib/abcdedd
    [1] => conf/xyz
    [2] => conf/abc/def
    [3] => htdocs/xyz
    [4] => lib2/abcdedd
)
acm
fonte
0
$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 ) );

Isso funciona bem ... semelhante ao mark baker, mas usa str_replace

KoolKabin
fonte
0

Provavelmente muito ingênuo e noob, mas funciona. Eu usei este algoritmo :

<?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);

Resultado:

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"
}

:)

Richard Knop
fonte
@Doomsday Há um link para a wikipedia em minha resposta ... tente lê-lo antes de comentar.
Richard Knop
Acho que no final você só compara os dois primeiros caminhos. Em seu exemplo, isso funciona, mas se você remover o primeiro caminho, ele será encontrado /www/htdocs/1/sites/conf/como uma correspondência comum. Além disso, o algoritmo procura substrings começando em qualquer lugar na string, mas para esta pergunta você sabe que pode começar na localização 0, o que torna muito mais simples.
Jan Fabry