アレイをテトリスする


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/…(私は試してみましたが動作します)。
Richard Knop

1
わぁ!このような素晴らしい入力がたくさんあります。私は手元にある私の問題を解決するために1つ取りますが、正当化された受け入れられた答えを実際に選択するには、解決策を比較する必要があると思います。私がそれを実行するまでには少し時間がかかるかもしれませんが、私は確かにそうします。
ペッカ

面白いタイトル:D btw:指名されたモデレーターリストであなたを見つけられないのはなぜですか?@ペッカ
サリカン

2
2年間受け入れられた答えはありませんか?
ゴードン

1
。これはまったく受け入れ答えを持っていない:(そして、それは私が「アレイをtetrising」一瞬前にそれを思い出したし、Googleで検索するような素晴らしいタイトルだから@Pekkaは3年に近づい
カミロ・マーティンを

回答:


35

関数を書く longest_common_prefix2つの文字列を入力として受け取る記述します。次に、それを任意の順序で文字列に適用して、共通のプレフィックスに減らします。連想的で可換的であるため、結果の順序は関係ありません。

これは、たとえば、加算や最大公約数などの他のバイナリ演算の場合と同じです。


8
+1。最初の2つの文字列を比較した後、結果(共通パス)を使用して3番目の文字列と比較します。
ミラノバブスコフ

23

それらをトライデータ構造にロードします。親ノードから始めて、子の数が1より大きいかどうかを確認します。その魔法のノードを見つけたら、親ノード構造を解体し、現在のノードをルートにします。


10
データをトライツリー構造にロードする操作には、最長の共通プレフィックスを見つけるアルゴリズムが含まれているため、実際にツリー構造を使用する必要はありませんか?つまり、ツリーの作成中にそれを検出できた場合は、複数の子のツリーを確認してください。なぜ木なのか?つまり、すでにアレイから始めているということです。配列の代わりにトライを使用するだけにストレージを変更できる場合、それは理にかなっていると思います。
Ben Schwehn、2010

2
あなたが注意しているなら、私の解決策はトライを構築するよりも効率的だと思います。
starblue

この答えは間違っています。私と他の答えにはO(n)の簡単な解決策が投稿されています。
Ari Ronen、2010

@ el.pescado:トライは、最悪の場合、ソース文字列の長さとサイズが4倍になります。
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;
}

これは断然最良の解決策ですが、改善が必要でした。それは(おそらく超える必要以上の文字列の反復)を考慮に入れ、前の最長共通パスをしていない、と口座へのパスを取ることはありませんでした(のためにそう/usr/lib/usr/lib2、それは与えた/usr/libのではなく、最長共通パスとして/usr/)。私は(うまくいけば)両方を修正しました。
Gabe

7

さて、XORこの状況で使用して文字列の共通部分を見つけることができると考えてください。同じ2バイトをxorするたびに、出力としてnullバイトが返されます。だから私たちはそれを有利に使うことができます:

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

さて、それは過度のような2つの文字列をカットして/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;
});

これは、配列の最初の値を参照文字列として使用します。次に、参照文字列を反復処理し、各文字を同じ位置にある2番目の文字列の文字と比較します。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

各文字を1回だけ読み取ることで、接頭辞を最速で削除できます。

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

実際、文字ベースの比較が最速になります。他のすべてのソリューションは「高価な」演算子を使用し、最終的には(複数の)文字比較も行います。それは聖ヨエルの聖典でも言及されました
Jan Fabry

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

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

編集

最も効率的でエレガントな答えは、提供されたそれぞれの答えから関数とメソッドを取得することを含む可能性があります


1

Iは、あろう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

更新 Felix Klingが指摘したarray_intersectように、共通の要素を持つが順序が異なるパスは考慮しません...これを解決するためarray_intersect_assocに、array_intersect

更新 追加されたコードも、配列から共通パス(またはテトリス!)を削除します。


これはおそらく機能しません。考えてみましょう/a/b/c/d/d/c/b/a。同じ要素、異なるパス。
フェリックスクリング

@Felix Klingインデックスチェックも実行するarray_intersect_assocを使用するように更新しました
Brendan Bullen

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, "/"));
        }
}

これは機能しません。たとえば、このセットarray( '/ www / htdocs / 1 / sites / conf / abc / def'、 '/ www / htdocs / 1 / sites / htdocs / xyz'、 '/ www / htdocs / 1 / sitesjj / lib2 / abcdedd '、)。
Artefacto 2010

@Artefacto:あなたは正しかった。そのため、比較に常に末尾のスラッシュ「/」を含めるように変更しました。曖昧さをなくします。
マリオ

1

おそらく、Pythonのos.path.commonprefix(m)使用アルゴリズムを移植するとうまくいくでしょうか?

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

これは正常に動作します...マークベイカーに似ていますが、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私の答えにはウィキペディアへのリンクがあります...コメントする前にまず読んでみてください。
Richard Knop

結局、最初の2つのパスのみを比較すると思います。あなたの例ではこれは機能しますが、最初のパスを削除する/www/htdocs/1/sites/conf/と、一般的な一致として検出されます。また、アルゴリズムは文字列の任意の場所から始まる部分文字列を検索しますが、この質問では、場所0から開始できるため、はるかに簡単になります。
Jan Fabry、2010
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.