Tetris-ing sebuah array

99

Pertimbangkan array berikut:

/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

apa cara terpendek dan paling elegan untuk mendeteksi jalur basis umum - dalam hal ini

/www/htdocs/1/sites/

dan menghapusnya dari semua elemen dalam array?

lib/abcdedd
conf/xyz
conf/abc/def
htdocs/xyz
lib2/abcdedd
Pekka
sumber
4
Ini mungkin patut untuk dicoba: en.wikibooks.org/wiki/Algorithm_implementation/Strings/… (Saya mencobanya dan berhasil).
Richard Knop
1
Awwww! Banyak sekali masukan yang brilian. Saya akan mengambil satu untuk memecahkan masalah saya, tetapi saya merasa bahwa untuk benar-benar memilih jawaban yang diterima yang dibenarkan, saya harus membandingkan solusinya. Mungkin perlu beberapa saat sampai saya bisa melakukan itu, tetapi saya pasti akan melakukannya.
Pekka
judul yang menghibur: D btw: kenapa aku tidak bisa menemukanmu di daftar nominasi moderator? @Pekka
The Suran
2
tidak ada jawaban yang diterima selama dua tahun?
Gordon
1
@Pekka Hampir tiga tahun sejak ini tidak ada jawaban yang diterima :( Dan itu adalah judul yang luar biasa sehingga saya ingat beberapa saat yang lalu dan mencari di Google "tetrising array".
Camilo Martin

Jawaban:

35

Tulis fungsi longest_common_prefixyang menggunakan dua string sebagai masukan. Kemudian terapkan ke string dalam urutan apa pun untuk menguranginya menjadi awalan umum. Karena asosiatif dan komutatif, urutan tidak menjadi masalah untuk hasilnya.

Ini sama dengan operasi biner lainnya seperti misalnya penjumlahan atau pembagi persekutuan terbesar.

starblue
sumber
8
+1. Setelah membandingkan 2 string pertama, gunakan result (jalur umum) untuk membandingkan dengan string ke-3 dan seterusnya.
Milan Babuškov
23

Muat mereka ke dalam struktur data trie. Mulai dari simpul induk, lihat mana yang memiliki anak terhitung lebih dari satu. Setelah Anda menemukan simpul ajaib itu, cukup bongkar struktur simpul induk dan miliki simpul saat ini sebagai root.

bragboy
sumber
10
Bukankah operasi yang memuat data ke dalam struktur pohon trie yang Anda gambarkan agak menyertakan algoritme untuk menemukan awalan umum terpanjang, sehingga membuat penggunaan struktur pohon sebenarnya tidak diperlukan? Yaitu mengapa memeriksa pohon untuk beberapa anak ketika Anda dapat mendeteksinya saat membangun pohon. Lalu mengapa sebatang pohon? Maksud saya jika Anda sudah mulai dengan array. Jika Anda dapat mengubah penyimpanan menjadi hanya menggunakan trie daripada array, saya kira itu masuk akal.
Ben Schwehn
2
Saya pikir jika Anda berhati-hati maka solusi saya lebih efisien daripada membangun trie.
starblue
Jawaban ini salah. Ada solusi sepele yang diposting di jawaban saya dan lainnya yaitu O (n).
Ari Ronen
@ el.pescado: Percobaan dalam ukuran kuadradik dengan panjang string sumber dalam kasus terburuk.
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
sumber
Sejauh ini, ini adalah solusi terbaik yang diposting, tetapi perlu perbaikan. Itu tidak memperhitungkan jalur umum terpanjang sebelumnya (mungkin mengulang lebih banyak string daripada yang diperlukan), dan tidak memperhitungkan jalur (jadi untuk /usr/libdan /usr/lib2itu memberi /usr/libsebagai jalur umum terpanjang, daripada /usr/). Saya (semoga) memperbaiki keduanya.
Gabe
7

Nah, mengingat bahwa Anda dapat menggunakan XORdalam situasi ini untuk menemukan bagian-bagian umum dari string. Setiap kali Anda x atau dua byte yang sama, Anda mendapatkan nullbyte sebagai output. Jadi kita bisa menggunakannya untuk keuntungan kita:

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

Setelah loop tunggal itu, $lengthvariabel akan sama dengan basepart umum terpanjang di antara array string. Kemudian, kita dapat mengekstrak bagian umum dari elemen pertama:

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

Dan begitulah. Sebagai fungsi:

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

Perhatikan bahwa itu menggunakan lebih dari satu iterasi, tetapi iterasi itu dilakukan di perpustakaan, jadi dalam bahasa yang ditafsirkan ini akan memiliki keuntungan efisiensi yang besar ...

Sekarang, jika Anda hanya menginginkan jalur lengkap, kita perlu memotong ke /karakter terakhir . Begitu:

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

Sekarang, itu mungkin terlalu memotong dua string seperti /foo/bardan /foo/bar/bazakan dipotong /foo. Tetapi singkatnya menambahkan putaran iterasi lain untuk menentukan apakah karakter berikutnya adalah salah satu / atau akhir string, saya tidak dapat melihat jalan keluarnya ...

ircmaxell
sumber
3

Pendekatan yang naif akan meledakkan jalur di /dan secara berturut-turut membandingkan setiap elemen dalam array. Jadi misalnya elemen pertama akan kosong di semua larik, jadi itu akan dihapus, elemen berikutnya akan www, itu sama di semua larik, jadi itu dihapus, dll.

Sesuatu seperti (belum dicoba)

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

Setelah itu Anda hanya perlu meledakkan elemen $exploded_pathslagi:

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

Yang memberi saya:

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

Ini mungkin tidak berskala dengan baik;)

Felix Kling
sumber
3

Oke, saya tidak yakin ini anti peluru, tapi menurut saya ini berhasil:

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

Ini akan mengambil nilai pertama dalam array sebagai string referensi. Kemudian itu akan mengulangi string referensi dan membandingkan setiap karakter dengan karakter dari string kedua pada posisi yang sama. Jika sebuah karakter tidak cocok, string referensi akan disingkat menjadi posisi karakter tersebut dan string berikutnya akan dibandingkan. Fungsi ini akan mengembalikan string pencocokan terpendek.

Performa tergantung pada string yang diberikan. Semakin awal string referensi semakin pendek, semakin cepat kode akan selesai. Saya benar-benar tidak tahu bagaimana memasukkannya ke dalam formula.

Saya menemukan bahwa pendekatan Artefacto untuk mengurutkan string meningkatkan kinerja. Menambahkan

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

sebelum array_reducesecara signifikan meningkatkan kinerja.

Perhatikan juga bahwa ini akan mengembalikan substring awal yang paling lama cocok , yang lebih serbaguna tetapi tidak akan memberi Anda jalur yang sama . Kamu harus lari

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

pada hasil. Dan kemudian Anda dapat menggunakan hasilnya untuk menghapus nilainya

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

yang seharusnya memberi:

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

Umpan balik diterima.

Gordon
sumber
3

Anda dapat menghapus awalan dengan cara tercepat, membaca setiap karakter hanya sekali:

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);
}
Kiamat
sumber
Memang, perbandingan berbasis karakter akan menjadi yang tercepat. Semua solusi lain menggunakan operator "mahal" yang pada akhirnya juga akan melakukan (banyak) perbandingan karakter. Itu bahkan disebutkan dalam kitab suci Yoel Suci !
Jan Fabry
2

Keuntungannya adalah tidak memiliki kompleksitas waktu linier; Namun, untuk kebanyakan kasus, jenis ini pasti tidak akan memakan waktu lebih lama.

Pada dasarnya, bagian pintar (setidaknya saya tidak dapat menemukan kesalahan dengannya) di sini adalah bahwa setelah menyortir Anda hanya perlu membandingkan jalur pertama dengan yang terakhir.

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
sumber
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 Varian dari metode asli saya menggunakan array_walk untuk membangun kembali 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>';

EDIT

Jawaban yang paling efisien dan elegan kemungkinan besar melibatkan pengambilan fungsi dan metode dari setiap jawaban yang diberikan

Mark Baker
sumber
1

Saya akan explodenilai berdasarkan / dan kemudian digunakan array_intersect_assocuntuk mendeteksi elemen umum dan memastikan mereka memiliki indeks yang sesuai dalam array. Array yang dihasilkan dapat digabungkan kembali untuk menghasilkan jalur yang sama.

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

Ini belum teruji, tetapi, idenya adalah bahwa $commonPathlarik hanya pernah berisi elemen jalur yang telah dimuat dalam semua larik lintasan yang telah dibandingkan dengannya. Ketika loop selesai, kita hanya menggabungkannya kembali dengan / untuk mendapatkan true$commonPath

Perbarui Seperti yang ditunjukkan oleh Felix Kling, array_intersecttidak akan mempertimbangkan jalur yang memiliki elemen umum tetapi dalam urutan yang berbeda ... Untuk mengatasi ini, saya menggunakan array_intersect_assocbukannyaarray_intersect

Perbarui kode yang ditambahkan untuk menghapus jalur umum (atau tetris itu!) Dari array juga.

Brendan Bullen
sumber
Ini mungkin tidak akan berhasil. Pertimbangkan /a/b/c/ddan /d/c/b/a. Elemen yang sama, jalur yang berbeda.
Felix Kling
@Felix Kling Saya telah memperbarui untuk menggunakan arrayintersect_assoc yang juga melakukan pemeriksaan indeks
Brendan Bullen
1

Masalah tersebut dapat disederhanakan jika dilihat dari sudut perbandingan senar. Ini mungkin lebih cepat daripada pemisahan array:

$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
sumber
Itu tidak akan berfungsi misalnya dengan set array ini ('/ www / htdocs / 1 / sites / conf / abc / def', '/ www / htdocs / 1 / sites / htdocs / xyz', '/ www / htdocs / 1 / sitesjj / lib2 / abcdedd ',).
Artefacto
@Artefacto: Anda benar. Jadi saya hanya memodifikasinya untuk selalu menyertakan garis miring "/" dalam perbandingan. Menjadikannya tidak ambigu.
mario
1

Mungkin porting algoritma yang digunakan Python os.path.commonprefix(m)akan berhasil?

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]

Itu adalah, uh ... sesuatu seperti itu

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

Setelah itu Anda hanya dapat membuat substr setiap elemen dari daftar asli dengan panjang awalan umum sebagai offset awal.

AKX
sumber
1

Aku akan melempar topiku ke dalam 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);
}

Pemakaian:

$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
sumber
1

Nah, sudah ada beberapa solusi di sini tapi, hanya karena menyenangkan:

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

Keluaran:

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

Ini berfungsi dengan baik ... mirip dengan mark baker tetapi menggunakan str_replace

KoolKabin
sumber
0

Mungkin terlalu naif dan noobish tapi berhasil. Saya telah menggunakan algoritma ini :

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

Keluaran:

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
sumber
@Doomsday Ada link ke wikipedia di jawaban saya ... coba baca dulu sebelum berkomentar.
Richard Knop
Saya pikir pada akhirnya Anda hanya membandingkan dua jalur pertama. Dalam contoh Anda ini berfungsi, tetapi jika Anda menghapus jalur pertama, itu akan ditemukan /www/htdocs/1/sites/conf/sebagai kecocokan umum. Selain itu, algoritme mencari substring yang dimulai di mana saja dalam string, tetapi untuk pertanyaan ini Anda tahu bahwa Anda bisa mulai dari lokasi 0, yang membuatnya lebih sederhana.
Jan Fabry