C, 618 564 octets
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;}
Et ici, il est démêlé, pour la "lisibilité":
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;
}
Mesdames et messieurs, j'ai fait une horrible erreur. Il permet d'être plus joli ... Et goto-less ... Au moins , il est maintenant rapide .
Nous définissons une fonction récursive L
qui prend en entrée un tableau s
de tableaux de caractères et le nombre n
de chaînes. La fonction renvoie la chaîne résultante sur stdout et renvoie accessoirement la taille en caractères de cette chaîne.
L'approche
Bien que le code soit alambiqué, la stratégie ici n'est pas trop complexe. Nous commençons par un algorithme récursif assez naïf, que je décrirai avec un pseudocode:
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
Maintenant, cet algorithme en lui-même est assez atroce (mais peut tenir dans environ 230 octets, j'ai trouvé). Ce n'est pas ainsi que l'on obtient des résultats rapides. Cet algorithme évolue incroyablement mal avec la longueur de la chaîne. Cet algorithme ne , cependant, échelle assez bien avec un plus grand nombre de chaînes. Le dernier cas de test serait résolu pratiquement instantanément, car aucune chaîne de caractères s
n'a de caractère c
en commun. J'ai mis en œuvre deux astuces principales ci-dessus qui ont entraîné une augmentation de vitesse incroyable:
À chaque appel à L
, vérifiez si nous avons déjà reçu cette même entrée. Étant donné que dans la pratique, les informations sont transmises via des pointeurs vers le même ensemble de chaînes, nous n'avons pas réellement à comparer des chaînes, uniquement des emplacements, ce qui est génial. Si nous constatons que nous avons obtenu ces informations auparavant, il n'est pas nécessaire de parcourir les calculs (la plupart du temps, mais obtenir des résultats rend les choses un peu plus compliquées) et nous pouvons nous en sortir en renvoyant simplement la longueur. Si nous ne trouvons pas de correspondance, enregistrez cet ensemble d'entrées / sorties pour le comparer aux appels futurs. Dans le code C, la deuxième for
boucle tente de trouver des correspondances avec l'entrée. Les pointeurs d'entrée connus sont enregistrés dans R
, et la longueur correspondante et les valeurs de sortie des caractères sont stockées dansA
. Ce plan a eu un effet drastique sur l'exécution, en particulier avec des chaînes plus longues.
Chaque fois que nous trouvons les emplacements c
dans s
, il y a une chance que nous savons dès le départ que ce que nous avons trouvé est pas optimale. Si chaque emplacement de c
apparaît après un emplacement connu d'une autre lettre, nous savons automatiquement que cela c
ne conduit pas à une sous-chaîne optimale, car vous pouvez y insérer une lettre de plus. Cela signifie que pour un petit coût, nous pouvons potentiellement supprimer plusieurs centaines d'appels à L
de grandes chaînes. Dans le code C ci-dessus, y
est un indicateur défini si nous savons automatiquement que ce caractère conduit à une chaîne sous-optimale, et z
est un indicateur défini si nous trouvons un caractère qui a des apparences exclusivement antérieures à tout autre caractère connu. Les premières apparitions actuelles des personnages sont stockées dansx
. L'implémentation actuelle de cette idée est un peu compliquée, mais les performances ont presque doublé dans de nombreux cas.
Avec ces deux idées, ce qui n'a pas fini en une heure prend maintenant environ 0,015 seconde.
Il y a probablement beaucoup plus de petites astuces qui peuvent accélérer les performances, mais à ce stade, j'ai commencé à m'inquiéter de ma capacité à tout jouer au golf. Je ne suis toujours pas satisfait du golf, donc j'y reviendrai probablement plus tard!
Timings
Voici un code de test, que je vous invite à essayer en ligne :
#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;
}
J'ai exécuté les cas de test de l'OP sur un ordinateur portable équipé d'une puce Intel Core i7 à 1,7 GHz, avec un paramètre d'optimisation de -Ofast
. La simulation a signalé un pic de 712 Ko requis. Voici un exemple d'exécution de chaque scénario de test, avec des horaires:
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 golf, j'ai atteint les performances de manière assez significative, et comme les gens semblaient aimer la vitesse brute (0,013624 s pour terminer tous les cas de test combinés) de ma précédente solution de 618 octets, je vais la laisser ici pour référence:
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;}
L'algorithme lui-même est inchangé, mais le nouveau code repose sur des divisions et des opérations au niveau du bit plus délicates qui finissent par ralentir le tout.