Comment puis-je déterminer la plus longue semblables partie de plusieurs chaînes?

comme dans le titre, j'essaie de trouver un moyen de déterminer par programmation la plus longue portion de similarité entre plusieurs cordes.

exemple:

  • file:///home/gms8994/Music/t.A.T.u./
  • file:///home/gms8994/Music/nina%20sky/
  • file:///home/gms8994/Music/A%20Perfect%20Circle/

idéalement, je récupérerais file:///home/gms8994/Music/ , parce que c'est la partie la plus longue qui est commune pour les 3 cordes.

Spécifiquement, je suis à la recherche d'une solution Perl, mais une solution dans n'importe quelle langue (ou même pseudo-langue) suffirait.

des commentaires: oui, seulement au début; mais il y a la possibilité d'avoir une autre entrée dans la liste, qui serait ignorée pour cette question.

8
demandé sur Hynek -Pichi- Vychodil 2009-02-01 04:12:10

7 réponses

Edit: je suis désolé pour l'erreur. Mon dommage que j'ai supervisé que l'utilisation de my variable à l'intérieur countit(x, q{}) est une grosse erreur. Cette chaîne est évaluée à l'intérieur du module Benchmark et @str y était vide. Cette solution n'est pas aussi rapide que j'ai présenté. Voir la correction ci-dessous. Je suis désolé encore une fois.

Perl peut être rapide:

use strict;
use warnings;

package LCP;

sub LCP {
    return '' unless @_;
    return $_[0] if @_ == 1;
    my $i          = 0;
    my $first      = shift;
    my $min_length = length($first);
    foreach (@_) {
        $min_length = length($_) if length($_) < $min_length;
    }
INDEX: foreach my $ch ( split //, $first ) {
        last INDEX unless $i < $min_length;
        foreach my $string (@_) {
            last INDEX if substr($string, $i, 1) ne $ch;
        }
    }
    continue { $i++ }
    return substr $first, 0, $i;
}

# Roy's implementation
sub LCP2 {
    return '' unless @_;
    my $prefix = shift;
    for (@_) {
        chop $prefix while (! /^\Q$prefix\E/);
        }
    return $prefix;
}

1;

Test suite:

#!/usr/bin/env perl

use strict;
use warnings;

Test::LCP->runtests;

package Test::LCP;

use base 'Test::Class';
use Test::More;
use Benchmark qw(:all :hireswallclock);

sub test_use : Test(startup => 1) {
    use_ok('LCP');
}

sub test_lcp : Test(6) {
    is( LCP::LCP(),      '',    'Without parameters' );
    is( LCP::LCP('abc'), 'abc', 'One parameter' );
    is( LCP::LCP( 'abc', 'xyz' ), '', 'None of common prefix' );
    is( LCP::LCP( 'abcdefgh', ('abcdefgh') x 15, 'abcdxyz' ),
        'abcd', 'Some common prefix' );
    my @str = map { chomp; $_ } <DATA>;
    is( LCP::LCP(@str),
        'file:///home/gms8994/Music/', 'Test data prefix' );
    is( LCP::LCP2(@str),
        'file:///home/gms8994/Music/', 'Test data prefix by LCP2' );
    my $t = countit( 1, sub{LCP::LCP(@str)} );
    diag("LCP: ${\($t->iters)} iterations took ${\(timestr($t))}");
    $t = countit( 1, sub{LCP::LCP2(@str)} );
    diag("LCP2: ${\($t->iters)} iterations took ${\(timestr($t))}");
}

__DATA__
file:///home/gms8994/Music/t.A.T.u./
file:///home/gms8994/Music/nina%20sky/
file:///home/gms8994/Music/A%20Perfect%20Circle/

résultat de la série de tests:

1..7
ok 1 - use LCP;
ok 2 - Without parameters
ok 3 - One parameter
ok 4 - None of common prefix
ok 5 - Some common prefix
ok 6 - Test data prefix
ok 7 - Test data prefix by LCP2
# LCP: 22635 iterations took 1.09948 wallclock secs ( 1.09 usr +  0.00 sys =  1.09 CPU) @ 20766.06/s (n=22635)
# LCP2: 17919 iterations took 1.06787 wallclock secs ( 1.07 usr +  0.00 sys =  1.07 CPU) @ 16746.73/s (n=17919)

cela signifie que la solution de Perl pure en utilisant substr est environ 20% plus rapide que solution de Roy à votre cas d'essai et une recherche de préfixe prend environ 50us. Il n'est pas nécessaire d'utiliser XS à moins que vos données ou vos attentes de performance soient plus grandes.

8
répondu Hynek -Pichi- Vychodil 2017-05-23 10:28:34

la référence déjà donnée par Brett Daniel pour L'entrée Wikipedia sur " le plus long problème de substrat commun " est une très bonne référence générale (avec pseudocode) pour votre question comme indiqué. Cependant, l'algorithme peut être exponentielle. Et il semble que vous pourriez réellement vouloir un algorithme pour le plus long préfixe commun qui est un algorithme beaucoup plus simple.

Voici celui que j'utilise pour le plus long préfixe commun (et une référence à L'URL originale):

use strict; use warnings;
sub longest_common_prefix {
    # longest_common_prefix( $|@ ): returns $
    # URLref: http://linux.seindal.dk/2005/09/09/longest-common-prefix-in-perl
    # find longest common prefix of scalar list
    my $prefix = shift;
    for (@_) {
        chop $prefix while (! /^\Q$prefix\E/);
        }
    return $prefix;
}

my @str = map {chomp; $_} <DATA>;
print longest_common_prefix(@ARGV), "\n";
__DATA__
file:///home/gms8994/Music/t.A.T.u./
file:///home/gms8994/Music/nina%20sky/
file:///home/gms8994/Music/A%20Perfect%20Circle/

si vous voulez vraiment une mise en œuvre LCSS, se référer à ces discussions ( plus long substrat commun et plus long suite commune ) à PerlMonks.org. Tree:: Suffix serait probablement la meilleure solution générale pour vous et implements, à ma connaissance, le meilleur algorithme. Malheureusement les constructions récentes sont cassées. Mais, un sous-programme de travail existe dans les discussions référencées sur PerlMonks dans ce post par Région limbique (reproduit ici avec vos données).

#URLref: http://www.perlmonks.org/?node_id=549876
#by Limbic~Region
use Algorithm::Loops 'NestedLoops';
use List::Util 'reduce';

use strict; use warnings;

sub LCS{
    my @str = @_;
    my @pos;
    for my $i (0 .. $#str) {
        my $line = $str[$i];
        for (0 .. length($line) - 1) {
            my $char= substr($line, $_, 1);
            push @{$pos[$i]{$char}}, $_;
        }
    }
    my $sh_str = reduce {length($a) < length($b) ? $a : $b} @str;
    my %map;
    CHAR:
    for my $char (split //, $sh_str) {
        my @loop;
        for (0 .. $#pos) {
            next CHAR if ! $pos[$_]{$char};
            push @loop, $pos[$_]{$char};
        }
        my $next = NestedLoops([@loop]);
        while (my @char_map = $next->()) {
            my $key = join '-', @char_map;
            $map{$key} = $char;
        }
    }
    my @pile;
    for my $seq (keys %map) {
        push @pile, $map{$seq};
        for (1 .. 2) {
            my $dir = $_ % 2 ? 1 : -1;
            my @offset = split /-/, $seq;
            $_ += $dir for @offset;
            my $next = join '-', @offset;
            while (exists $map{$next}) {
                $pile[-1] = $dir > 0 ?
                    $pile[-1] . $map{$next} : $map{$next} . $pile[-1];
                $_ += $dir for @offset;
                $next = join '-', @offset;
            }
        }
    }
    return reduce {length($a) > length($b) ? $a : $b} @pile;
}

my @str = map {chomp; $_} <DATA>;
print LCS(@str), "\n";
__DATA__
file:///home/gms8994/Music/t.A.T.u./
file:///home/gms8994/Music/nina%20sky/
file:///home/gms8994/Music/A%20Perfect%20Circle/
5
répondu rivy 2009-02-01 03:15:38

On dirait que vous voulez que le k-sous-chaîne commune algorithme . Il est exceptionnellement simple à programmer, et un bon exemple de programmation dynamique.

3
répondu Brett Daniel 2009-02-01 01:38:42

mon premier instinct est de lancer une boucle, en prenant le caractère suivant de chaque chaîne, jusqu'à ce que les caractères ne soient pas égaux. Gardez un compte de quelle position dans la chaîne vous êtes et puis prenez une soustraction (de l'une des trois chaînes) de 0 à la position avant que les caractères ne sont pas égaux.

En Perl, vous devez diviser la chaîne en caractères en utilisant quelque chose comme

@array = split(//, $string);

(fractionnement sur un vide jeux de caractères de chaque personnage dans son propre élément du tableau)

puis faire une boucle, peut-être dans l'ensemble:

$n =0;
@array1 = split(//, $string1);
@array2 = split(//, $string2);
@array3 = split(//, $string3);

while($array1[$n] == $array2[$n] && $array2[$n] == $array3[$n]){
 $n++; 
}

$sameString = substr($string1, 0, $n); #n might have to be n-1

ou du moins quelque chose dans ce genre. Pardonnez-moi si ça ne marche pas, mon Perl est un peu rouillé.

3
répondu Perchik 2009-02-01 01:48:22

si vous utilisez google pour" chaîne commune la plus longue " vous obtiendrez de bons pointeurs pour le cas général où les séquences ne doivent pas commencer au début des chaînes. Par exemple, http://en.wikipedia.org/wiki/Longest_common_substring_problem .

Mathematica se trouve avoir une fonction pour ce Construit en: http://reference.wolfram.com/mathematica/ref/LongestCommonSubsequence.html (notez qu'ils signifient suite contigu , c'est-à-dire la soustraction, ce qui est ce que vous voulez.)

si vous ne vous souciez que du préfixe commun le plus long, alors il devrait être beaucoup plus rapide de boucler simplement i de 0 jusqu'à ce que les ième caractères ne correspondent pas tous et renvoient substr(s, 0, i-1).

2
répondu dreeves 2009-02-01 14:47:52

de http://forums.macosxhints.com/showthread.php?t=33780

my @strings =
    (
      'file:///home/gms8994/Music/t.A.T.u./',
      'file:///home/gms8994/Music/nina%20sky/',
      'file:///home/gms8994/Music/A%20Perfect%20Circle/',
    );

my $common_part = undef;
my $sep = chr(0);  # assuming it's not used legitimately
foreach my $str ( @strings ) {

    # First time through loop -- set common
    # to whole
    if ( !defined $common_part ) {
        $common_part = $str;
        next;
    }

    if ("$common_part$sep$str" =~ /^(.*).*$sep.*$/)
    {
        $common_part = ;
    }
}

print "Common part = $common_part\n";
1
répondu Hissohathair 2009-02-01 12:00:58

plus vite que ci-dessus, utilise la fonction XOR binaire native de perl, adaptée de la solution de perlmongers (le $+[0] n'a pas fonctionné pour moi):

sub common_suffix {
    my $comm = shift @_;
    while ($_ = shift @_) {
        $_ = substr($_,-length($comm)) if (length($_) > length($comm));
        $comm = substr($comm,-length($_)) if (length($_) < length($comm));
        if (( $_ ^ $comm ) =~ /("151900920"*)$/) {
            $comm = substr($comm, -length());
        } else {
            return undef;
        }
    }
    return $comm;
}


sub common_prefix {
    my $comm = shift @_;
    while ($_ = shift @_) {
        $_ = substr($_,0,length($comm)) if (length($_) > length($comm));
        $comm = substr($comm,0,length($_)) if (length($_) < length($comm));
        if (( $_ ^ $comm ) =~ /^("151900920"*)/) {
            $comm = substr($comm,0,length());
        } else {
            return undef;
        }
    }
    return $comm;
}
1
répondu Erik Aronesty 2012-02-28 21:15:45