C, 618 564 bajtów
d,M,N,A[9999][2];char*(R[9999][20]),b[1000];L(char**s,n){char*j[20],c,a=0;int x[n],y=n-1,z,i,t,m=0,w=1;for(;y;)x[y--]=999;for(;y<N;y++){for(i=0;i<n&&s[i]==R[y][i];i++);if(i/n){a=A[y][0];m=A[y][1];w=0;if(m+d<M||!a)goto J;else{c=a;goto K;}}}for(c=97;w&&c<'{';c++){K:t=1,y=1,z=1;for(i=0;i<n;j[i++]++){for(j[i]=s[i];*j[i]-c;j[i]++)t&=!!*j[i];y&=j[i]-s[i]>x[i]?z=0,1:0;}t&=!y;I:if(t){if(z)for(i=0;i<n;i++)x[i]=j[i]-s[i];d++,t+=L(j,n),d--,m=t>m?a=c,t:m;}}if(w){for(y=0;y<n;y++)R[N][y]=s[y];A[N][0]=a;A[N++][1]=m;}J:if(d+m>=M)M=d+m,b[d]=a;if(!d)N=0,M=0,puts(b);return m;}
I tutaj jest wyjaśnione, dla „czytelności”:
d,M,N,A[9999][2];
char*(R[9999][20]),b[1000];
L(char**s,n){
char*j[20],c,a=0;
int x[n],y=n-1,z,i,t,m=0,w=1;
for(;y;)
x[y--]=999;
for(;y<N;y++){
for(i=0;i<n&&s[i]==R[y][i];i++);
if(i/n){
a=A[y][0];
m=A[y][1];
w=0;
if(m+d<M||!a)
goto J;
else{
c=a;
goto K;
}
}
}
for(c=97;w&&c<'{';c++){
K:
t=1,
y=1,
z=1;
for(i=0;i<n;j[i++]++){
for(j[i]=s[i];*j[i]-c;j[i]++)
t&=!!*j[i];
y&=j[i]-s[i]>x[i]?z=0,1:0;
}
t&=!y;
I:
if(t){
if(z)
for(i=0;i<n;i++)
x[i]=j[i]-s[i];
d++,
t+=L(j,n),
d--,
m=t>m?a=c,t:m;
}
}
if(w){
for(y=0;y<n;y++)R[N][y]=s[y];
A[N][0]=a;
A[N++][1]=m;
}
J:
if(d+m>=M)
M=d+m,b[d]=a;
if(!d)
N=0,M=0,puts(b);
return m;
}
Panie i panowie! Popełniłem straszny błąd. Jest używany do być ładniejsza ... I goto mniej ... Przynajmniej teraz jest szybka .
Definiujemy funkcję rekurencyjną, Lktóra przyjmuje jako dane wejściowe tablicę stablic znaków i liczbę nłańcuchów. Funkcja wyświetla wynikowy ciąg znaków na standardowe wyjście i przypadkowo zwraca rozmiar znaków tego ciągu.
Podejście
Chociaż kod jest zawiły, strategia tutaj nie jest zbyt skomplikowana. Zaczynamy od dość naiwnego algorytmu rekurencyjnego, który opiszę za pomocą pseudokodu:
Function L (array of strings s, number of strings n), returns length:
Create array of strings j of size n;
For each character c in "a-z",
For each integer i less than n,
Set the i'th string of j to the i'th string of s, starting at the first appearance of c in s[i]. (e.g. j[i][0] == c)
If c does not occur in the i'th string of s, continue on to the next c.
end For
new_length := L( j, n ) + 1; // (C) t = new_length
if new_length > best_length
best_character := c; // (C) a = best_character
best_length := new_length; // (C) m = best_length
end if
end For
// (C) d = current_depth_in_recursion_tree
if best_length + current_depth_in_recursion_tree >= best_found
prepend best_character to output_string // (C) b = output_string
// (C) M = best_found, which represents the longest common substring found at any given point in the execution.
best_found = best_length + current_depth;
end if
if current_depth_in_recursion_tree == 0
reset all variables, print output_string
end if
return best_length
Ten algorytm sam w sobie jest dość okropny (znalazłem, że można go zmieścić w około ~ 230 bajtach). Nie w ten sposób uzyskuje się szybkie wyniki. Ten algorytm skaluje się niezwykle słabo przy długości łańcucha. Algorytm ten jest jednak dość dobrze skalować z większej liczby łańcuchów. Ostatni przypadek testowy zostałby rozwiązany praktycznie natychmiast, ponieważ żadne ciągi znaków nie smają cwspólnych znaków . Zaimplementowałem dwie główne sztuczki, które spowodowały niesamowity wzrost prędkości:
Przy każdym wezwaniu do Lsprawdź, czy otrzymaliśmy już tę samą informację. Ponieważ w praktyce informacja jest przekazywana dookoła poprzez wskaźniki do tego samego zestawu strun, nie faktycznie trzeba porównać ciągi, tylko miejsca, które jest świetne. Jeśli okaże się, że już otrzymaliśmy te informacje, nie trzeba przeprowadzać obliczeń (przez większość czasu, ale uzyskanie danych wyjściowych sprawia, że jest to trochę bardziej skomplikowane) i możemy uciec od samego powrotu długości. Jeśli nie znajdziemy dopasowania, zapisz ten zestaw danych wejściowych / wyjściowych, aby porównać z przyszłymi połączeniami. W kodzie C druga forpętla próbuje znaleźć dopasowania do danych wejściowych. Znane wskaźniki wejściowe są zapisywane R, a odpowiadające im wartości długości i znaków są przechowywane wA. Ten plan miał drastyczny wpływ na środowisko wykonawcze, szczególnie w przypadku dłuższych łańcuchów.
Za każdym razem znaleźć lokalizacje cw s, istnieje szansa, wiemy, tuż nietoperza, że to, co odkryliśmy, nie jest optymalna. Jeśli każda lokalizacja cpojawi się po znanej lokalizacji innej litery, automatycznie wiemy, że cnie prowadzi to do optymalnego podciągu, ponieważ można w niej zmieścić jeszcze jedną literę. Oznacza to, że za niewielką opłatą możemy potencjalnie usunąć kilkaset wezwań do Ldużych ciągów. W powyższym kodzie C yjest zestaw flag, jeśli automatycznie wiemy, że ten znak prowadzi do nieoptymalnego ciągu, i zjest zestawem flag, jeśli znajdziemy znak, który ma wyłącznie wcześniejszy wygląd niż jakikolwiek inny znany znak. Obecne najwcześniejsze pojawienia się znaków są przechowywane wx. Obecna implementacja tego pomysłu jest nieco niechlujna, ale w wielu przypadkach niemal podwojona wydajność.
Dzięki tym dwóm pomysłom, co nie zakończyło się w ciągu godziny, zajęło teraz około 0,015 sekundy.
Prawdopodobnie istnieje wiele innych małych sztuczek, które mogą przyspieszyć wydajność, ale w tym momencie zacząłem martwić się o moją umiejętność gry w golfa. Wciąż nie jestem zadowolony z golfa, więc prawdopodobnie wrócę do tego później!
Czasy
Oto kod testowy, który zapraszam do wypróbowania online :
#include "stdio.h"
#include "time.h"
#define SIZE_ARRAY(x) (sizeof(x) / sizeof(*x))
int main(int argc, char** argv) {
/* Our test case */
char* test7[] = {
"nqrualgoedlf",
"jgqorzglfnpa",
"fgttvnogldfx",
"pgostsulyfug",
"sgnhoyjlnfvr",
"wdttgkolfkbt"
};
printf("Test 7:\n\t");
clock_t start = clock();
/* The call to L */
int size = L(test7, SIZE_ARRAY(test7));
double dt = ((double)(clock() - start)) / CLOCKS_PER_SEC;
printf("\tSize: %d\n", size);
printf("\tElapsed time: %lf s\n", dt);
return 0;
}
Testy OP przeprowadziłem na laptopie wyposażonym w procesor Intel Core i7 1,7 GHz z ustawieniem optymalizacji na -Ofast. Symulacja wykazała, że wymagany szczyt to 712 KB. Oto przykładowy przebieg każdego przypadku testowego z czasami:
Test 1:
a
Size: 1
Elapsed time: 0.000020 s
Test 2:
x
Size: 1
Elapsed time: 0.000017 s
Test 3:
hecbpyhogntqppcqgkxchpsieuhbmcbhuqdjbrqmclchqyfhtdvdoysuhrrl
Size: 60
Elapsed time: 0.054547 s
Test 4:
ihicvaoodsnktkrar
Size: 17
Elapsed time: 0.007459 s
Test 5:
krkk
Size: 4
Elapsed time: 0.000051 s
Test 6:
code
Size: 4
Elapsed time: 0.000045 s
Test 7:
golf
Size: 4
Elapsed time: 0.000040 s
Test 8:
Size: 0
Elapsed time: 0.000029 s
Total time: 0.062293 s
Podczas gry w golfa dość znacząco poprawiłem wydajność, a ponieważ ludzie wydawali się lubić brutalną prędkość (0,013624 s, aby ukończyć wszystkie przypadki testowe łącznie) mojego poprzedniego 618-bajtowego rozwiązania, zostawię to tutaj w celach informacyjnych:
d,M,N,A[9999][2];char*(R[9999][20]),b[1000];L(char**s,n){char*j[20],c,a=0;int x[n],y,z,i,t,m=0,w=1;for(y=0;y<n;y++)x[y]=999;for(y=0;y<N;y++){for(i=0;i<n;i++)if(s[i]!=R[y][i])break;if(i==n){a=A[y][0];m=A[y][1];w=0;if(m+d<M||!a)goto J;else{c=a;goto K;}}}for(c=97;w&&c<'{';c++){K:t=1,y=1,z=1;for(i=0;i<n;j[i++]++){for(j[i]=s[i];*j[i]-c;j[i]++)if(!*j[i]){t=0;goto I;}if(j[i]-s[i]>x[i])z=0;if(j[i]-s[i]<x[i])y=0;}if(y){t=0;}I:if(t){if(z){for(i=0;i<n;i++){x[i]=j[i]-s[i];}}d++,t+=L(j,n),d--,m=t>m?(a=c),t:m;}}if(w){for(y=0;y<n;y++)R[N][y]=s[y];A[N][0]=a;A[N++][1]=m;}J:if(d+m>=M)M=d+m,b[d]=a;if(!d)N=0,M=0,puts(b);return m;}
Sam algorytm pozostaje niezmieniony, ale nowy kod opiera się na podziałach i trudniejszych operacjach bitowych, które ostatecznie spowalniają cały proces.