Tetris-ing una matriz

99

Considere la siguiente 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

¿Cuál es la forma más corta y elegante de detectar la ruta base común ? En este caso

/www/htdocs/1/sites/

y eliminándolo de todos los elementos de la matriz?

lib/abcdedd
conf/xyz
conf/abc/def
htdocs/xyz
lib2/abcdedd
Pekka
fuente
4
Podría valer la pena intentarlo: en.wikibooks.org/wiki/Algorithm_implementation/Strings/… (Lo probé y funciona).
Richard Knop
1
¡Awwww! Tanta aportación brillante. Tomaré uno para resolver mi problema en cuestión, pero siento que para elegir realmente una respuesta aceptada justificada, tendré que comparar las soluciones. Puede que tarde un poco en hacer eso, pero ciertamente lo haré.
Pekka
entretenido título: D por cierto: ¿por qué no puedo encontrarte en la lista de moderadores nominados? @Pekka
The Surrican
2
sin respuesta aceptada durante dos años?
Gordon
1
@Pekka Acercarse a tres años desde que esto no tiene una respuesta aceptada :( Y es un título tan asombroso que lo recordé hace un momento y busqué en Google "tetrising an array".
Camilo Martin

Respuestas:

35

Escribe una función longest_common_prefixque tome dos cadenas como entrada. Luego aplíquelo a las cadenas en cualquier orden para reducirlas a su prefijo común. Dado que es asociativo y conmutativo, el orden no importa para el resultado.

Esto es lo mismo que para otras operaciones binarias como, por ejemplo, la suma o el máximo común divisor.

estrella azul
fuente
8
+1. Después de comparar las 2 primeras cadenas, utilice el resultado (ruta común) para comparar con la 3ª cadena y así sucesivamente.
Milan Babuškov
23

Cárguelos en una estructura de datos trie. Comenzando desde el nodo padre, vea cuál tiene un hijo más importante que uno. Una vez que encuentre ese nodo mágico, simplemente desmantele la estructura del nodo principal y tenga el nodo actual como raíz.

fanfarrón
fuente
10
¿No incluiría la operación que carga los datos en la estructura de árbol trie que describe un poco el algoritmo para encontrar el prefijo común más largo, haciendo así innecesario el uso de una estructura de árbol? Es decir, ¿por qué comprobar si hay varios hijos en el árbol cuando se puede detectar mientras se construye? Entonces, ¿por qué un árbol? Quiero decir, si ya comienzas con una matriz. Si puede cambiar el almacenamiento para usar un trie en lugar de matrices, supongo que tiene sentido.
Ben Schwehn
2
Creo que si tienes cuidado, mi solución es más eficiente que construir un trie.
starblue
Esta respuesta es incorrecta. Hay soluciones triviales publicadas en mi y otras respuestas que son O (n).
Ari Ronen
@ el.pescado: Los intentos tienen un tamaño cuadrádico con la longitud de la cadena de origen en el peor de los casos.
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
fuente
Esta es, con mucho, la mejor solución publicada, pero necesitaba mejoras. No tuvo en cuenta la ruta común más larga anterior (posiblemente iterando sobre más de la cadena de lo necesario), y no tuvo en cuenta las rutas (por lo tanto, for /usr/liby /usr/lib2dio /usr/libcomo la ruta común más larga, en lugar de /usr/). Yo (con suerte) arreglé ambos.
Gabe
7

Bueno, considerando que se puede utilizar XORen esta situación para encontrar las partes comunes de la cadena. Cada vez que xo dos bytes que son iguales, obtiene un byte nulo como salida. Entonces podemos usar eso a nuestro 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)));
}

Después de ese bucle único, la $lengthvariable será igual a la parte base común más larga entre la matriz de cadenas. Luego, podemos extraer la parte común del primer elemento:

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

Y ahí lo tienes. Como una función:

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

Tenga en cuenta que usa más de una iteración, pero esas iteraciones se realizan en bibliotecas, por lo que en los lenguajes interpretados esto tendrá una gran ganancia de eficiencia ...

Ahora, si solo desea rutas completas, debemos truncar al último /carácter. Entonces:

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

Ahora, puede cortar demasiado dos cuerdas como /foo/bary /foo/bar/bazse cortará /foo. Pero por debajo de la adición de una nueva ronda de iteración para determinar si el siguiente carácter es o bien / o al final de la cadena, no puede ver una forma de evitar eso ...

ircmaxell
fuente
3

Un enfoque ingenuo sería explotar las rutas en el /y comparar sucesivamente todos los elementos de las matrices. Entonces, por ejemplo, el primer elemento estaría vacío en todas las matrices, por lo que se eliminará, el siguiente elemento será www, es el mismo en todas las matrices, por lo que se eliminará, etc.

Algo como (no probado)

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

Luego solo tienes que implosionar los elementos $exploded_pathsnuevamente:

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

Lo que me da:

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

Esto podría no escalar bien;)

Felix Kling
fuente
3

Ok, no estoy seguro de que sea a prueba de balas, pero creo 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;
});

Esto tomará el primer valor de la matriz como cadena de referencia. Luego iterará sobre la cadena de referencia y comparará cada carácter con el carácter de la segunda cadena en la misma posición. Si un carácter no coincide, la cadena de referencia se acortará a la posición del carácter y se comparará la siguiente cadena. Entonces, la función devolverá la cadena coincidente más corta.

El rendimiento depende de las cuerdas dadas. Cuanto antes se acorte la cadena de referencia, más rápido finalizará el código. Sin embargo, realmente no tengo ni idea de cómo poner eso en una fórmula.

Descubrí que el enfoque de Artefacto para clasificar las cuerdas aumenta el rendimiento. Añadiendo

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

antes de array_reduceque aumentará significativamente el rendimiento.

También tenga en cuenta que esto devolverá la subcadena inicial coincidente más larga , que es más versátil pero no le dará la ruta común . Tienes que correr

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

en el resultado. Y luego puedes usar el resultado para eliminar los valores

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

que debería dar:

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

Comentarios bienvenidos.

Gordon
fuente
3

Puede eliminar el prefijo de la manera más rápida, leyendo cada carácter solo una 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);
}
Día del Juicio Final
fuente
De hecho, una comparación basada en personajes será la más rápida. Todas las demás soluciones utilizan operadores "costosos" que al final también harán comparaciones de (múltiples) caracteres. ¡Incluso se menciona en las escrituras del Santo Joel !
Jan Fabry
2

Esto tiene la ventaja de no tener una complejidad de tiempo lineal; sin embargo, en la mayoría de los casos, la operación definitivamente no será la que lleve más tiempo.

Básicamente, la parte inteligente (al menos no pude encontrar una falla) aquí es que después de clasificar solo tendrá que comparar la primera ruta con la última.

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
fuente
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 de mi método original usando un array_walk para reconstruir la matriz

$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

Es probable que la respuesta más eficiente y elegante implique tomar funciones y métodos de cada una de las respuestas proporcionadas

Mark Baker
fuente
1

Yo usaría explodelos valores basados ​​en / y luego los usaría array_intersect_assocpara detectar los elementos comunes y asegurarme de que tengan el índice correspondiente correcto en la matriz. La matriz resultante podría recombinarse para producir la ruta común.

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

Esto no está probado, pero la idea es que la $commonPathmatriz solo contenga los elementos de la ruta que han estado contenidos en todas las matrices de ruta que se han comparado con ella. Cuando el ciclo está completo, simplemente lo recombinamos con / para obtener el verdadero$commonPath

Actualización Como señaló Felix Kling, array_intersectno consideraré caminos que tengan elementos comunes pero en diferentes órdenes ... Para resolver esto, usé en array_intersect_assoclugar dearray_intersect

Actualización Código agregado para eliminar la ruta común (¡o tetris!) De la matriz también.

Brendan Bullen
fuente
Probablemente esto no funcione. Considere /a/b/c/dy /d/c/b/a. Mismos elementos, diferentes caminos.
Felix Kling
@Felix Kling He actualizado para usar array_intersect_assoc, que también realiza una verificación de índice
Brendan Bullen
1

El problema se puede simplificar si solo se ve desde el ángulo de comparación de cuerdas. Esto probablemente sea más rápido que la división de matrices:

$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
fuente
Eso no funcionará, por ejemplo, con esta matriz de conjunto ('/ www / htdocs / 1 / sites / conf / abc / def', '/ www / htdocs / 1 / sites / htdocs / xyz', '/ www / htdocs / 1 / sitesjj / lib2 / abcdedd ',).
Artefacto
@Artefacto: Tenías razón. Así que simplemente lo modifiqué para incluir siempre una barra inclinada "/" en la comparación. Lo hace no ambiguo.
mario
1

¿Quizás la portabilidad del algoritmo que os.path.commonprefix(m)usa Python funcionaría?

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]

Eso es, eh ... 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);
}

Después de eso, puede simplemente subescribir cada elemento de la lista original con la longitud del prefijo común como el desplazamiento inicial.

AKX
fuente
1

Tiraré mi sombrero al ring ...

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
fuente
1

Bueno, ya hay algunas soluciones aquí pero, solo porque fue 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>';

Salida:

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

Esto funciona bien ... similar a Mark Baker pero usa str_replace

KoolKabin
fuente
0

Probablemente demasiado ingenuo y novato, pero funciona. He usado 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);

Salida:

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
fuente
@Doomsday Hay un enlace a wikipedia en mi respuesta ... intenta leerlo primero antes de comentar.
Richard Knop
Creo que al final solo comparas los dos primeros caminos. En su ejemplo, esto funciona, pero si elimina la primera ruta, encontrará /www/htdocs/1/sites/conf/una coincidencia común. Además, el algoritmo busca subcadenas que comienzan en cualquier lugar de la cadena, pero para esta pregunta, sabe que puede comenzar en la ubicación 0, lo que lo hace mucho más simple.
Jan Fabry