Тетріс-ний масив


99

Розглянемо наступний масив:

/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

4
Це, можливо, варто спробувати: en.wikibooks.org/wiki/Algorithm_implementation/Strings/… (я спробував це і працює).
Річард Ноп

1
Awwww! Такий великий блискучий вклад. Я візьму один, щоб вирішити свою проблему під рукою, але я вважаю, що, щоб дійсно вибрати обґрунтовану прийняту відповідь, мені доведеться порівняти рішення. Це може зайняти деякий час, поки я не обійдуся цим, але, безумовно, буду.
Pekka

розважальна назва: D btw: чому я не можу знайти вас у списку номінованих модераторів? @Pekka
The Surrican

2
не прийнято відповіді протягом двох років?
Гордон

1
@Pekka Наблизившись до трьох років, оскільки на це немає прийнятої відповіді :( І це такий дивовижний заголовок, що я запам'ятав його на мить і гугл "тетризуючи масив".
Каміло Мартін,

Відповіді:


35

Напишіть функцію, longest_common_prefixяка приймає два рядки як вхідні дані. Потім застосуйте його до рядків у будь-якому порядку, щоб зменшити їх до загального префікса. Оскільки він асоціативний і комутативний, порядок не має значення для результату.

Це те саме, що і для інших двійкових операцій, наприклад, наприклад, додавання або найбільший спільний дільник.


8
+1. Після порівняння перших 2 рядків використовуйте результат (загальний шлях) для порівняння з 3-м рядком тощо.
Мілан Бабушков

23

Завантажте їх у структуру даних трие. Починаючи з батьківського вузла, дивіться, який у дітей більше, ніж один. Як тільки ви знайдете цей магічний вузол, просто демонтуйте структуру батьківського вузла та отримайте поточний вузол як корінь.


10
Невже операція, яка завантажує дані в структуру дерева трійки, яку ви описуєте, ніби не включала алгоритм пошуку найдовшого загального префікса, таким чином, фактично використовуючи структуру дерева непотрібною? Тобто навіщо перевіряти дерево на декількох дітей, коли ви могли це виявити, будуючи дерево. Чому тоді взагалі дерево? Я маю на увазі, якщо ви вже починаєте з масиву. Якщо ви можете змінити сховище просто на використання трие замість масивів, я думаю, це має сенс.
Бен Швен

2
Я думаю, що якщо ви обережні, то моє рішення є більш ефективним, ніж побудова трійки.
starblue

Ця відповідь неправильна. Є тривіальні рішення, розміщені в моїх та інших відповідях, які є O (n).
Арі Ронен

@ el.pescado: Випробування мають квадратичний розмір, а в гіршому - довжина вихідного рядка.
Біллі 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;
}

Це далеко не найкраще рішення, однак воно потребувало вдосконалення. Він не враховував попередній найдовший загальний шлях (можливо, повторюючи більше рядка, ніж потрібно), і не враховував шляхи (тому для цього він /usr/libі /usr/lib2надав /usr/libяк найдовший загальний шлях, а не /usr/). Я (сподіваюся) виправив обох.
Гейб

7

Ну, враховуючи, що ви можете використовувати 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. Але, якщо не додати ще один ітераційний раунд, щоб визначити, чи наступний символ є / або кінцевим рядком, я не можу побачити шлях із цим ...


3

Наївним підходом було б підірвати шляхи на /та послідовно порівнювати кожен елемент масивів. Так, наприклад, перший елемент буде порожнім у всіх масивах, тому він буде видалений, наступним буде елемент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
)

Це може бути недостатньо масштабним;)


3

Гаразд, я не впевнений, що це куленепробивна, але я думаю, що це працює:

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

Це буде приймати перше значення в масиві як опорний рядок. Тоді він повториться по еталонному рядку та порівняє кожну таблицю із знаком другого рядка в тому самому положенні. Якщо знак char не відповідає, опорний рядок буде скорочено до позиції char і наступний рядок порівнюється. Функція поверне найкоротший збіг рядка.

Продуктивність залежить від наведених рядків. Чим раніше еталонний рядок стає коротшим, тим швидше закінчиться код. Я дійсно не маю поняття, як це вкласти у формулу.

Я виявив, що підхід 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

Відгуки Ласкаво просимо.


3

Ви можете видалити префікс найшвидше, прочитавши кожен символ лише один раз:

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

Дійсно, порівняння на основі характеру буде найшвидшим. Всі інші рішення використовують «дорогі» оператори, які врешті-решт також виконають (множинні) порівняння символів. Про це навіть згадували у Писаннях Святого Йоїла !
Ян Фабрі

2

Це має перевагу в тому, що не має лінійної часової складності; однак, у більшості випадків подібний вид точно не буде операцією, яка потребує більше часу.

В основному, розумна частина (принаймні, я не могла знайти вину в цьому) тут полягає в тому, що після сортування вам доведеться лише порівняти перший шлях з останнім.

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

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 Варіант мого оригінального методу, що використовує 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>';

EDIT

Найбільш ефективна та елегантна відповідь, ймовірно, передбачає використання функцій та методів з кожної із поданих відповідей


1

Я би 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

Оновлення Як вказував Фелікс Клінг, array_intersectне розглядатиме шляхи, які мають загальні елементи, але в різних порядках ... Для вирішення цього питання я використовував array_intersect_assocзамістьarray_intersect

Оновіть Доданий код, щоб також видалити загальний шлях (або тетрис!) З масиву.


Це, ймовірно, не вийде. Розглянемо /a/b/c/dі /d/c/b/a. Ті самі елементи, різні стежки.
Фелікс Клінг

@Felix Kling Я оновив, щоб використовувати array_intersect_assoc, який також виконує перевірку індексу
Брендан Буллен

1

Проблему можна спростити, якщо її просто розглядати з кута порівняння рядків. Це, мабуть, швидше, ніж розбиття масивів:

$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: Ви мали рацію. Тому я просто змінив його, щоб завжди включати кінцевий проріз "/" у порівняння. Робить це неоднозначним.
Маріо

1

Можливо, перенесення алгоритму os.path.commonprefix(m)використання Python буде спрацьовувати?

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

Після цього ви можете просто підкреслити кожен елемент вихідного списку з довжиною загального префікса як зміщення старту.


1

Я кину шапку на ринг ...

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

1

Ну, тут вже є рішення, але просто тому, що це було весело:

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

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

Це чудово працює ... схоже на маркер baker, але використовує str_replace


0

Напевно, занадто наївний і нобіш, але це працює. Я використовував цей алгоритм :

<?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 У моїй відповіді є посилання на wikipedia ... спробуйте прочитати її спочатку, перш ніж коментувати.
Річард Ноп

Я думаю, врешті-решт ти лише порівняєш перші два шляхи. У вашому прикладі це працює, але якщо ви видалите перший шлях, він знайдеться /www/htdocs/1/sites/conf/як загальна відповідність. Також алгоритм шукає підрядки, починаючи з будь-якого місця в рядку, але для цього питання ви знаєте, що ви можете почати з розташування 0, що робить його набагато простішим.
Ян Фабрі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.