C, 618 564 bytes
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;}
E aqui está desvendado, por "legibilidade":
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;
}
Senhoras e senhores, cometi um erro horrível. É usado para ser mais bonita ... e Goto-less ... Pelo menos agora ele é rápido .
Definimos uma função recursiva Lque recebe como entrada uma matriz sde matrizes de caracteres e o número nde strings. A função gera a string resultante para stdout e, aliás, retorna o tamanho em caracteres dessa string.
A abordagem
Embora o código seja complicado, a estratégia aqui não é muito complexa. Começamos com um algoritmo recursivo bastante ingênuo, que descreverei com pseudocódigo:
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
Agora, esse algoritmo por si só é bastante atroz (mas pode caber em torno de ~ 230 bytes, eu descobri). Não é assim que se obtém resultados rápidos. Esse algoritmo escala incrivelmente mal com o comprimento da string. Este algoritmo faz , no entanto, escala razoavelmente bem com maior número de cordas. O último caso de teste seria resolvido virtualmente instantaneamente, já que nenhuma sequência de caracteres spossui caracteres cem comum. Foram implementados dois truques principais que resultaram em um incrível aumento de velocidade:
A cada chamada para L, verifique se recebemos essa mesma entrada antes. Como, na prática, as informações são passadas através de ponteiros para o mesmo conjunto de strings, na verdade não precisamos comparar strings, apenas locais, o que é ótimo. Se descobrimos que obtivemos essas informações antes, não há necessidade de executar os cálculos (na maioria das vezes, mas obter resultados torna isso um pouco mais complicado) e podemos nos safar apenas retornando o comprimento. Se não encontrarmos uma correspondência, salve esse conjunto de entrada / saída para comparar com chamadas futuras. No código C, o segundo forloop tenta encontrar correspondências para a entrada. Os ponteiros de entrada conhecidos são salvos Re os valores correspondentes de comprimento e saída de caracteres são armazenados emA. Esse plano teve um efeito drástico no tempo de execução, especialmente com seqüências mais longas.
Toda vez que encontramos os locais cem s, há uma chance de sabermos logo de cara que o que descobrimos não é o ideal. Se todos os locais caparecerem após algum local conhecido de outra letra, sabemos automaticamente que isso cnão leva a uma substring ideal, porque você pode colocar mais uma letra nela. Isso significa que, por um pequeno custo, podemos remover várias centenas de chamadas Lpara cadeias grandes. No código C acima, yé um conjunto de sinalizadores se soubermos automaticamente que esse caractere leva a uma sequência subótima e zé um conjunto de sinalizadores se encontrarmos um caractere que tenha aparências exclusivamente anteriores a qualquer outro caractere conhecido. As primeiras aparências atuais de caracteres são armazenadas emx. A implementação atual dessa idéia é um pouco confusa, mas quase dobrou o desempenho em muitos casos.
Com essas duas idéias, o que não terminou em uma hora agora levou cerca de 0,015 segundos.
Provavelmente existem muito mais pequenos truques que podem acelerar o desempenho, mas nesse momento comecei a me preocupar com minha capacidade de jogar tudo. Ainda não estou contente com o golfe, então provavelmente voltarei a isso mais tarde!
Horários
Aqui estão alguns códigos de teste, que eu convido você a experimentar 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;
}
Executei os casos de teste do OP em um laptop equipado com um chip Intel Core i7 de 1,7 GHz, com uma configuração de otimização de -Ofast. A simulação relatou um pico de 712 KB necessário. Aqui está um exemplo de execução de cada caso de teste, com tempos:
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
No golfe, atingi o desempenho de maneira bastante significativa e, como as pessoas pareciam gostar da velocidade bruta (0,013624 s para concluir todos os casos de teste combinados) da minha solução anterior de 618 bytes, deixarei aqui como referência:
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;}
O algoritmo em si é inalterado, mas o novo código baseia-se em divisões e em algumas operações bit a bit mais complicadas que acabam atrasando tudo.