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;}
Y aquí está descifrado, por "legibilidad":
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;
}
Damas y caballeros, he cometido un error horrible. Se utiliza para ser más bonita ... y Goto-menos ... Al menos ahora es rápida .
Definimos una función recursiva L
que toma como entrada una matriz s
de matrices de caracteres y el número n
de cadenas. La función genera la cadena resultante en stdout y, por cierto, devuelve el tamaño en caracteres de esa cadena.
El enfoque
Aunque el código es complicado, la estrategia aquí no es demasiado compleja. Comenzamos con un algoritmo recursivo bastante ingenuo, que describiré con 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
Ahora, este algoritmo por sí solo es bastante atroz (pero he encontrado unos 230 bytes). No es así como se obtienen resultados rápidos. Este algoritmo escala increíblemente mal con la longitud de la cadena. Este algoritmo tiene , sin embargo, la escala bastante bien con un mayor número de cadenas. El último caso de prueba se resolvería prácticamente al instante, ya que las cadenas no s
tienen ningún carácter c
en común. Hubo dos trucos principales que implementé anteriormente que resultaron en un increíble aumento de velocidad:
En cada llamada a L
, verifique si se nos ha dado esta misma entrada antes. Como en la práctica la información se transmite a través de punteros al mismo conjunto de cadenas, en realidad no tenemos que comparar cadenas, solo ubicaciones, lo cual es genial. Si descubrimos que obtuvimos esta información antes, no hay necesidad de ejecutar los cálculos (la mayoría de las veces, pero obtener resultados hace que esto sea un poco más complicado) y podemos salir con solo devolver la longitud. Si no encontramos una coincidencia, guarde este conjunto de entrada / salida para compararlo con futuras llamadas. En el código C, el segundo for
ciclo intenta encontrar coincidencias con la entrada. Los punteros de entrada conocidos se guardan R
y los valores de salida de caracteres y longitud correspondientes se almacenan enA
. Este plan tuvo un efecto drástico en el tiempo de ejecución, especialmente con cadenas más largas.
Cada vez que encontramos las ubicaciones de c
in s
, existe la posibilidad de que sepamos de inmediato que lo que hemos encontrado no es óptimo. Si cada ubicación de c
aparece después de alguna ubicación conocida de otra letra, sabemos automáticamente que esto c
no conduce a una subcadena óptima, porque puede incluir una letra más en ella. Esto significa que por un pequeño costo, potencialmente podemos eliminar varios cientos de llamadas a L
cadenas grandes. En el código C anterior, y
hay un conjunto de indicadores si sabemos automáticamente que este carácter conduce a una cadena subóptima, y z
es un conjunto de indicadores si encontramos un carácter que tiene apariencias exclusivamente anteriores a cualquier otro carácter conocido. Las primeras apariencias actuales de los personajes se almacenan enx
. La implementación actual de esta idea es un poco desordenada, pero casi duplica el rendimiento en muchos casos.
Con estas dos ideas, lo que no terminó en una hora ahora tomó alrededor de 0.015 segundos.
Probablemente hay muchos más trucos que pueden acelerar el rendimiento, pero en este punto comencé a preocuparme por mi capacidad para jugar golf todo. Todavía no estoy contento con el golf, ¡así que probablemente volveré a esto más tarde!
Tiempos
Aquí hay un código de prueba, que te invito a probar en línea :
#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;
}
Ejecuté los casos de prueba del OP en una computadora portátil equipada con un chip Intel Core i7 de 1.7 GHz, con una configuración de optimización de -Ofast
. La simulación informó un pico de 712 KB requerido. Aquí hay un ejemplo de cada caso de prueba, con tiempos:
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
En el golf, alcancé el rendimiento de manera bastante significativa, y dado que a la gente parecía gustarle la velocidad bruta (0.013624 s para completar todos los casos de prueba combinados) de mi solución anterior de 618 bytes, lo dejaré aquí como referencia:
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;}
El algoritmo en sí no ha cambiado, pero el nuevo código se basa en divisiones y algunas operaciones bit a bit más complicadas que terminan ralentizando todo.