Tetris-ing un tableau

99

Considérez le tableau suivant:

/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

quel est le moyen le plus court et le plus élégant de détecter le chemin de base commun - dans ce cas

/www/htdocs/1/sites/

et le supprimer de tous les éléments du tableau?

lib/abcdedd
conf/xyz
conf/abc/def
htdocs/xyz
lib2/abcdedd
Pekka
la source
4
Cela vaut peut-être la peine d'essayer: en.wikibooks.org/wiki/Algorithm_implementation/Strings/… (je l'ai essayé et ça marche).
Richard Knop
1
Awwww! Une telle contribution brillante. J'en prendrai une pour résoudre mon problème actuel, mais je pense que pour vraiment choisir une réponse acceptée justifiée, je devrai comparer les solutions. Il faudra peut-être un certain temps avant que j'y parvienne, mais je le ferai certainement.
Pekka
titre divertissant: D btw: pourquoi ne puis-je pas vous trouver sur la liste des modérateurs nommés? @Pekka
The Surrican
2
pas de réponse acceptée pendant deux ans?
Gordon
1
@Pekka Se rapproche de trois ans depuis que cela n'a pas de réponse acceptée :( Et c'est un titre tellement génial que je m'en suis souvenu il y a un instant et j'ai googlé "tétriser un tableau".
Camilo Martin

Réponses:

35

Ecrire une fonction longest_common_prefix qui prend deux chaînes en entrée. Appliquez-le ensuite aux chaînes dans n'importe quel ordre pour les réduire à leur préfixe commun. Puisqu'il est associatif et commutatif, l'ordre n'a pas d'importance pour le résultat.

C'est la même chose que pour d'autres opérations binaires comme par exemple l'addition ou le plus grand diviseur commun.

starblue
la source
8
+1. Après avoir comparé les 2 premières chaînes, utilisez le résultat (chemin commun) pour comparer avec la 3ème chaîne et ainsi de suite.
Milan Babuškov
23

Chargez-les dans une structure de données trie. À partir du nœud parent, voyez lequel des enfants compte plus d'un. Une fois que vous avez trouvé ce nœud magique, démontez simplement la structure du nœud parent et ayez le nœud actuel en tant que racine.

bragboy
la source
10
L'opération qui charge les données dans l'arborescence trie que vous décrivez n'inclurait-elle pas en quelque sorte l'algorithme pour trouver le préfixe commun le plus long, rendant ainsi inutile l'utilisation d'une arborescence? C'est à dire pourquoi vérifier l'arbre pour plusieurs enfants alors que vous pouviez le détecter lors de la construction de l'arbre. Pourquoi alors un arbre? Je veux dire si vous commencez déjà avec un tableau. Si vous pouvez changer le stockage en utilisant simplement un trie au lieu de tableaux, je suppose que cela a du sens.
Ben Schwehn
2
Je pense que si vous faites attention, ma solution est plus efficace que la construction d'un trie.
starblue
Cette réponse est fausse. Il y a des solutions triviales affichées dans my et d'autres réponses qui sont O (n).
Ari Ronen
@ el.pescado: Les essais sont de taille quadradique avec la longueur de la chaîne source dans le pire des cas.
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
la source
C'est de loin la meilleure solution publiée, mais elle devait être améliorée. Il n'a pas pris en compte le chemin commun précédent le plus long (éventuellement itérer sur plus de chaîne que nécessaire), et n'a pas pris en compte les chemins (donc pour /usr/libet /usr/lib2il a donné /usr/libcomme chemin commun le plus long, plutôt que /usr/). J'ai (espérons-le) corrigé les deux.
Gabe
7

Eh bien, étant donné que vous pouvez utiliser XORdans cette situation pour trouver les parties communes de la chaîne. Chaque fois que vous xor deux octets identiques, vous obtenez un octet nul en sortie. Nous pouvons donc utiliser cela à notre avantage:

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

Après cette boucle unique, la $lengthvariable sera égale au plus long départ de base commun entre le tableau de chaînes. Ensuite, nous pouvons extraire la partie commune du premier élément:

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

Et voila. En tant que fonction:

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

Notez qu'il utilise plus d'une itération, mais ces itérations sont effectuées dans des bibliothèques, donc dans les langages interprétés, cela aura un énorme gain d'efficacité ...

Maintenant, si vous ne voulez que des chemins complets, nous devons tronquer au dernier /caractère. Alors:

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

Maintenant, il peut trop couper deux chaînes telles que /foo/baret /foo/bar/bazseront coupées /foo. Mais à moins d'ajouter un autre tour d'itération pour déterminer si le prochain caractère est l'un / ou l' autre ou la fin de la chaîne, je ne vois pas de moyen de contourner cela ...

ircmaxell
la source
3

Une approche naïve consisterait à faire exploser les chemins au niveau de /et à comparer successivement chaque élément des tableaux. Ainsi, par exemple, le premier élément serait vide dans tous les tableaux, il sera donc supprimé, le prochain élément serawww , il est le même dans tous les tableaux, il est donc supprimé, etc.

Quelque chose comme (non testé)

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

Ensuite, il vous suffit d'imploser à $exploded_pathsnouveau les éléments :

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

Ce qui me donne:

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

Cela pourrait ne pas bien évoluer;)

Félix Kling
la source
3

Ok, je ne suis pas sûr que ce soit à l'épreuve des balles, mais je pense que cela fonctionne:

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

Cela prendra la première valeur du tableau comme chaîne de référence. Ensuite, il itérera sur la chaîne de référence et comparera chaque caractère avec le caractère de la deuxième chaîne à la même position. Si un caractère ne correspond pas, la chaîne de référence sera raccourcie à la position du caractère et la chaîne suivante est comparée. La fonction renverra alors la chaîne correspondante la plus courte.

Les performances dépendent des chaînes données. Plus la chaîne de référence raccourcit tôt, plus le code se terminera rapidement. Je ne sais vraiment pas comment mettre cela dans une formule.

J'ai trouvé que l'approche d'Artefacto pour trier les cordes augmente les performances. Ajouter

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

avant le array_reduceaugmentera considérablement les performances.

Notez également que cela renverra la plus longue sous - chaîne initiale correspondante , qui est plus polyvalente mais ne vous donnera pas le chemin commun . Tu dois courir

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

sur le résultat. Et puis vous pouvez utiliser le résultat pour supprimer les valeurs

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

ce qui devrait donner:

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

Vos commentaires sont les bienvenus.

Gordon
la source
3

Vous pouvez supprimer le préfixe de la manière la plus rapide, en ne lisant chaque caractère qu'une seule fois:

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);
}
jour du Jugement dernier
la source
En effet, une comparaison basée sur les caractères sera la plus rapide. Toutes les autres solutions utilisent des opérateurs «coûteux» qui, à la fin, feront également des comparaisons (multiples) de caractères. Cela a même été mentionné dans les écritures du Saint Joël !
Jan Fabry
2

Cela présente l'avantage de ne pas avoir de complexité temporelle linéaire; cependant, dans la plupart des cas, le tri ne sera certainement pas l'opération qui prendra plus de temps.

Fondamentalement, la partie intelligente (du moins je n'ai pas trouvé de défaut) ici est qu'après le tri, vous n'aurez qu'à comparer le premier chemin avec le dernier.

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
la source
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>';

EDIT Variante de ma méthode originale utilisant un array_walk pour reconstruire le tableau

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

ÉDITER

La réponse la plus efficace et la plus élégante impliquera probablement de prendre des fonctions et des méthodes de chacune des réponses fournies

Mark Baker
la source
1

Je voudrais explodeles valeurs basées sur le /, puis les utiliser array_intersect_assocpour détecter les éléments communs et m'assurer qu'ils ont le bon index correspondant dans le tableau. Le tableau résultant pourrait être recombiné pour produire le chemin commun.

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

Ceci n'est pas testé, mais l'idée est que le $commonPathtableau ne contient que les éléments du chemin qui ont été contenus dans tous les tableaux de chemins qui ont été comparés avec lui. Lorsque la boucle est terminée, nous la recombinons simplement avec / pour obtenir le vrai$commonPath

Mise à jour Comme l'a souligné Felix Kling, array_intersectne considérera pas les chemins qui ont des éléments communs mais dans des ordres différents ... Pour résoudre cela, j'ai utilisé à la array_intersect_assocplace dearray_intersect

Mise à jour Ajout du code pour supprimer le chemin commun (ou tetris it!) Du tableau également.

Brendan Bullen
la source
Cela ne fonctionnera probablement pas. Considérez /a/b/c/det /d/c/b/a. Mêmes éléments, chemins différents.
Felix Kling
@Felix Kling J'ai mis à jour pour utiliser array_intersect_assoc qui effectue également une vérification d'index
Brendan Bullen
1

Le problème peut être simplifié s'il est simplement vu sous l'angle de comparaison des chaînes. C'est probablement plus rapide que le fractionnement de tableau:

$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
la source
Cela ne fonctionnera pas par exemple avec ce tableau d'ensemble ('/ www / htdocs / 1 / sites / conf / abc / def', '/ www / htdocs / 1 / sites / htdocs / xyz', '/ www / htdocs / 1 / sitesjj / lib2 / abcdedd ',).
Artefacto
@Artefacto: Vous aviez raison. Je l'ai donc simplement modifié pour toujours inclure une barre oblique "/" dans la comparaison. Le rend non ambigu.
mario
1

Peut-être que le portage de l'algorithme os.path.commonprefix(m)utilisé par Python fonctionnerait?

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]

C'est, euh ... quelque chose comme

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

Après cela, vous pouvez simplement sous-chaque élément de la liste d'origine avec la longueur du préfixe commun comme décalage de départ.

AKX
la source
1

Je jetterai mon chapeau dans le 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);
}

Usage:

$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
la source
1

Eh bien, il y a déjà des solutions ici mais, juste parce que c'était amusant:

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

Production:

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

Cela fonctionne bien ... similaire à Mark Baker mais utilise str_replace

KoolKabin
la source
0

Probablement trop naïf et noobish mais ça marche. J'ai utilisé cet algorithme :

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

Production:

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
la source
@Doomsday Il y a un lien vers wikipedia dans ma réponse ... essayez de le lire avant de commenter.
Richard Knop
Je pense qu'en fin de compte, vous ne comparez que les deux premiers chemins. Dans votre exemple, cela fonctionne, mais si vous supprimez le premier chemin, il trouvera /www/htdocs/1/sites/conf/une correspondance commune. En outre, l'algorithme recherche des sous-chaînes commençant n'importe où dans la chaîne, mais pour cette question, vous savez que vous pouvez commencer à l'emplacement 0, ce qui le rend beaucoup plus simple.
Jan Fabry