배열 테트리스


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
앗! 많은 훌륭한 입력. 내 문제를 해결하기 위해 하나를 가져갈 것이지만, 정당한 대답을 선택하려면 해결책을 비교해야한다고 생각합니다. 그렇게하는 데 시간이 좀 걸릴 수 있지만 확실히 할 것입니다.
Pekka

재미있는 제목 : D btw : 왜 내가 추천 된 중재자 목록에서 당신을 찾을 수 없습니까? @Pekka
Surrican

2
2 년 동안 받아 들여진 대답이 없습니까?
Gordon

1
@Pekka 3 년 가까이 가까워지는 이유는 대답이 받아 들여지지 않았기 때문입니다 :( 그리고 그것은 내가 잠시 전에 그것을 기억하고 "배열 테트리스"를봤을 정도로 멋진 제목입니다.
Camilo Martin

답변:


35

longest_common_prefix두 개의 문자열을 입력으로 받는 함수 를 작성하십시오 . 그런 다음 임의의 순서로 문자열에 적용하여 공통 접두사로 줄이십시오. 연관적이고 교환 적이므로 순서는 결과에 중요하지 않습니다.

이것은 덧셈이나 최대 공약수와 같은 다른 이진 연산과 동일합니다.


8
+1. 처음 두 문자열을 비교 한 후 결과 (공통 경로)를 사용하여 세 번째 문자열과 비교합니다.
Milan Babuškov

23

트라이 데이터 구조에로드합니다. 부모 노드에서 시작하여 자식이 1보다 큰지 확인하십시오. 매직 노드를 찾으면 부모 노드 구조를 해체하고 현재 노드를 루트로 만드십시오.


10
설명하는 트리 트리 구조에 데이터를로드하는 작업에 가장 긴 공통 접두사를 찾는 알고리즘이 포함되어 있으므로 실제로 트리 구조를 사용할 필요가 없습니까? 즉, 나무를 만드는 동안 발견 할 수있을 때 여러 자녀가 ​​있는지 나무를 확인하는 이유입니다. 그럼 왜 나무? 이미 배열로 시작한다면 말입니다. 어레이 대신 트라이를 사용하도록 스토리지를 변경할 수 있다면 의미가 있다고 생각합니다.
Ben Schwehn

2
나는 당신이 조심한다면 내 솔루션이 트라이를 구축하는 것보다 더 효율적이라고 생각합니다.
starblue

이 대답은 틀 렸습니다. 내 및 O (n) 인 다른 답변에 게시 된 사소한 솔루션이 있습니다.
Ari Ronen

@ el.pescado : 시도는 최악의 경우 소스 문자열의 길이와 함께 크기가 2 차적입니다.
Billy ONeal 2012 년

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 할 때마다 출력으로 nullbyte가 표시됩니다. 그래서 우리는 그것을 우리의 이점으로 사용할 수 있습니다.

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

이것은 배열의 첫 번째 값을 참조 문자열로 사용합니다. 그런 다음 참조 문자열을 반복하고 각 문자를 동일한 위치에있는 두 번째 문자열의 문자와 비교합니다. 문자가 일치하지 않으면 참조 문자열이 문자 위치로 단축되고 다음 문자열이 비교됩니다. 이 함수는 일치하는 가장 짧은 문자열을 반환합니다.

성능은 주어진 문자열에 따라 다릅니다. 참조 문자열이 짧아 질수록 코드가 더 빨리 완료됩니다. 나는 그것을 공식에 ​​넣는 방법을 정말로 모른다.

문자열을 정렬하는 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);
}

실제로 문자 기반 비교가 가장 빠릅니다. 다른 모든 솔루션은 결국 (다중) 문자 비교를 수행하는 "비싼"연산자를 사용합니다. 그것은 심지어 Holy Joel의 경전에 언급되었습니다 !
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

@Felix 클링 또한 인덱스 검사를 수행 array_intersect_assoc 사용하도록 업데이트 한
브랜든 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, "/"));
        }
}

예를 들어이 집합 배열 ( '/ www / htdocs / 1 / sites / conf / abc / def', '/ www / htdocs / 1 / sites / htdocs / xyz', '/ www / htdocs / 1)에서는 작동하지 않습니다. / sitesjj / lib2 / abcdedd ',).
Artefacto

@Artefacto : 당신이 옳았습니다. 따라서 비교시 항상 후행 슬래시 "/"를 포함하도록 수정했습니다. 모호하지 않게 만듭니다.
mario

1

아마도 파이썬이 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 ) );

이것은 잘 작동합니다 ... mark 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 내 대답에 위키 백과에 대한 링크가 있습니다 ... 댓글을 달기 전에 먼저 읽어보십시오.
Richard Knop

결국에는 처음 두 경로 만 비교한다고 생각합니다. 귀하의 예에서는 작동하지만 첫 번째 경로를 제거하면 /www/htdocs/1/sites/conf/공통 일치로 찾습니다 . 또한 알고리즘은 문자열에서 시작하는 부분 문자열을 검색하지만이 질문에 대해서는 위치 0에서 시작할 수 있으므로 훨씬 간단합니다.
Jan Fabry
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.