La longueur étant répertoriée comme critère, voici la version golfée à 1681 caractères (pourrait encore être améliorée de 10%):
import java.io.*;import java.util.*;public class W{public static void main(String[]
a)throws Exception{int n=a.length<1?5:a[0].length(),p,q;String f,t,l;S w=new S();Scanner
s=new Scanner(new
File("sowpods"));while(s.hasNext()){f=s.next();if(f.length()==n)w.add(f);}if(a.length<1){String[]x=w.toArray(new
String[0]);Random
r=new Random();q=x.length;p=r.nextInt(q);q=r.nextInt(q-1);f=x[p];t=x[p>q?q:q+1];}else{f=a[0];t=a[1];}H<S>
A=new H(),B=new H(),C=new H();for(String W:w){A.put(W,new
S());for(p=0;p<n;p++){char[]c=W.toCharArray();c[p]='.';l=new
String(c);A.get(W).add(l);S z=B.get(l);if(z==null)B.put(l,z=new
S());z.add(W);}}for(String W:A.keySet()){C.put(W,w=new S());for(String
L:A.get(W))for(String b:B.get(L))if(b!=W)w.add(b);}N m,o,ñ;H<N> N=new H();N.put(f,m=new
N(f,t));N.put(t,o=new N(t,t));m.k=0;N[]H=new
N[3];H[0]=m;p=H[0].h;while(0<1){if(H[0]==null){if(H[1]==H[2])break;H[0]=H[1];H[1]=H[2];H[2]=null;p++;continue;}if(p>=o.k-1)break;m=H[0];H[0]=m.x();if(H[0]==m)H[0]=null;for(String
v:C.get(m.s)){ñ=N.get(v);if(ñ==null)N.put(v,ñ=new N(v,t));if(m.k+1<ñ.k){if(ñ.k<ñ.I){q=ñ.k+ñ.h-p;N
Ñ=ñ.x();if(H[q]==ñ)H[q]=Ñ==ñ?null:Ñ;}ñ.b=m;ñ.k=m.k+1;q=ñ.k+ñ.h-p;if(H[q]==null)H[q]=ñ;else{ñ.n=H[q];ñ.p=ñ.n.p;ñ.n.p=ñ.p.n=ñ;}}}}if(o.b==null)System.out.println(f+"\n"+t+"\nOY");else{String[]P=new
String[o.k+2];P[o.k+1]=o.k-1+"";m=o;for(q=m.k;q>=0;q--){P[q]=m.s;m=m.b;}for(String
W:P)System.out.println(W);}}}class N{String s;int k,h,I=(1<<30)-1;N b,p,n;N(String S,String
d){s=S;for(k=0;k<d.length();k++)if(d.charAt(k)!=S.charAt(k))h++;k=I;p=n=this;}N
x(){N r=n;n.p=p;p.n=n;n=p=this;return r;}}class S extends HashSet<String>{}class H<V>extends
HashMap<String,V>{}
La version non golfée, qui utilise des noms et des méthodes de package et ne donne pas d'avertissement ou n'étend les classes que pour les alias, est:
package com.akshor.pjt33;
import java.io.*;
import java.util.*;
// WordLadder partially golfed and with reduced dependencies
//
// Variables used in complexity analysis:
// n is the word length
// V is the number of words (vertex count of the graph)
// E is the number of edges
// hash is the cost of a hash insert / lookup - I will assume it's constant, but without completely brushing it under the carpet
public class WordLadder2
{
private Map<String, Set<String>> wordsToWords = new HashMap<String, Set<String>>();
// Initialisation cost: O(V * n * (n + hash) + E * hash)
private WordLadder2(Set<String> words)
{
Map<String, Set<String>> wordsToLinks = new HashMap<String, Set<String>>();
Map<String, Set<String>> linksToWords = new HashMap<String, Set<String>>();
// Cost: O(Vn * (n + hash))
for (String word : words)
{
// Cost: O(n*(n + hash))
for (int i = 0; i < word.length(); i++)
{
// Cost: O(n + hash)
char[] ch = word.toCharArray();
ch[i] = '.';
String link = new String(ch).intern();
add(wordsToLinks, word, link);
add(linksToWords, link, word);
}
}
// Cost: O(V * n * hash + E * hash)
for (Map.Entry<String, Set<String>> from : wordsToLinks.entrySet()) {
String src = from.getKey();
wordsToWords.put(src, new HashSet<String>());
for (String link : from.getValue()) {
Set<String> to = linksToWords.get(link);
for (String snk : to) {
// Note: equality test is safe here. Cost is O(hash)
if (snk != src) add(wordsToWords, src, snk);
}
}
}
}
public static void main(String[] args) throws IOException
{
// Cost: O(filelength + num_words * hash)
Map<Integer, Set<String>> wordsByLength = new HashMap<Integer, Set<String>>();
BufferedReader br = new BufferedReader(new FileReader("sowpods"), 8192);
String line;
while ((line = br.readLine()) != null) add(wordsByLength, line.length(), line);
if (args.length == 2) {
String from = args[0].toUpperCase();
String to = args[1].toUpperCase();
new WordLadder2(wordsByLength.get(from.length())).findPath(from, to);
}
else {
// 5-letter words are the most interesting.
String[] _5 = wordsByLength.get(5).toArray(new String[0]);
Random rnd = new Random();
int f = rnd.nextInt(_5.length), g = rnd.nextInt(_5.length - 1);
if (g >= f) g++;
new WordLadder2(wordsByLength.get(5)).findPath(_5[f], _5[g]);
}
}
// O(E * hash)
private void findPath(String start, String dest) {
Node startNode = new Node(start, dest);
startNode.cost = 0; startNode.backpointer = startNode;
Node endNode = new Node(dest, dest);
// Node lookup
Map<String, Node> nodes = new HashMap<String, Node>();
nodes.put(start, startNode);
nodes.put(dest, endNode);
// Heap
Node[] heap = new Node[3];
heap[0] = startNode;
int base = heap[0].heuristic;
// O(E * hash)
while (true) {
if (heap[0] == null) {
if (heap[1] == heap[2]) break;
heap[0] = heap[1]; heap[1] = heap[2]; heap[2] = null; base++;
continue;
}
// If the lowest cost isn't at least 1 less than the current cost for the destination,
// it can't improve the best path to the destination.
if (base >= endNode.cost - 1) break;
// Get the cheapest node from the heap.
Node v0 = heap[0];
heap[0] = v0.remove();
if (heap[0] == v0) heap[0] = null;
// Relax the edges from v0.
int g_v0 = v0.cost;
// O(hash * #neighbours)
for (String v1Str : wordsToWords.get(v0.key))
{
Node v1 = nodes.get(v1Str);
if (v1 == null) {
v1 = new Node(v1Str, dest);
nodes.put(v1Str, v1);
}
// If it's an improvement, use it.
if (g_v0 + 1 < v1.cost)
{
// Update the heap.
if (v1.cost < Node.INFINITY)
{
int bucket = v1.cost + v1.heuristic - base;
Node t = v1.remove();
if (heap[bucket] == v1) heap[bucket] = t == v1 ? null : t;
}
// Next update the backpointer and the costs map.
v1.backpointer = v0;
v1.cost = g_v0 + 1;
int bucket = v1.cost + v1.heuristic - base;
if (heap[bucket] == null) {
heap[bucket] = v1;
}
else {
v1.next = heap[bucket];
v1.prev = v1.next.prev;
v1.next.prev = v1.prev.next = v1;
}
}
}
}
if (endNode.backpointer == null) {
System.out.println(start);
System.out.println(dest);
System.out.println("OY");
}
else {
String[] path = new String[endNode.cost + 1];
Node t = endNode;
for (int i = t.cost; i >= 0; i--) {
path[i] = t.key;
t = t.backpointer;
}
for (String str : path) System.out.println(str);
System.out.println(path.length - 2);
}
}
private static <K, V> void add(Map<K, Set<V>> map, K key, V value) {
Set<V> vals = map.get(key);
if (vals == null) map.put(key, vals = new HashSet<V>());
vals.add(value);
}
private static class Node
{
public static int INFINITY = Integer.MAX_VALUE >> 1;
public String key;
public int cost;
public int heuristic;
public Node backpointer;
public Node prev = this;
public Node next = this;
public Node(String key, String dest) {
this.key = key;
cost = INFINITY;
for (int i = 0; i < dest.length(); i++) if (dest.charAt(i) != key.charAt(i)) heuristic++;
}
public Node remove() {
Node rv = next;
next.prev = prev;
prev.next = next;
next = prev = this;
return rv;
}
}
}
Comme vous pouvez le voir, l'analyse des coûts de fonctionnement est O(filelength + num_words * hash + V * n * (n + hash) + E * hash)
. Si vous acceptez mon hypothèse qu'une insertion / recherche de table de hachage est à temps constant, c'est O(filelength + V n^2 + E)
. Les statistiques particulières des graphiques dans SOWPODS signifient que O(V n^2)
cela domine vraiment O(E)
pour la plupart n
.
Exemples de sorties:
IDOLA, IDOLS, IDYLS, ODYLS, ODALS, OVALS, OVELS, FOURS, EVENS, ETENS, STENS, SKENS, SKINS, SPINS, SPINE, 13
WICCA, PROSY, OY
BRINY, BRINS, TRINS, TAINS, TARNS, YARNS, YAWNS, YAWPS, YAPPS, 7
GALES, GAZ, GASTS, GESTS, GESTE, GESSE, DESSE, 5
SURES, DURES, DUNES, DINES, DINGS, DINGY, 4
LICHT, LIGHT, BIGHT, BIGOT, BIGOS, BIROS, GIROS, GIRNS, GURNS, GUANS, GUANA, RUANA, 10
SARGE, SERGE, SERRE, SERRS, SEERS, DEERS, DYERS, OYERS, OVERS, OVELS, OVALS, ODALS, ODYLS, IDYLS, 12
KEIRS, SEIRS, SEERS, BEERS, BRERS, BRERE, BREME, CREME, CREPE, 7
C'est l'une des 6 paires avec le chemin le plus long et le plus court:
GAINEST, FAINEST, FAIREST, SAIREST, SAIDEST, SADDEST, MADDEST, MIDDEST, MILDEST, WILDEST, WILIEST, WALIEST, WANIEST, CANIEST, CANTEST, CONTEST, CONFEST, CONFESS, CONFERS, CONKERS, COOKERS, COOPERS, COPPERS, POPPERS, POPPERS, POPPERS, POPPERS POPPITS, POPPIES, POPSIES, MOPSIES, MOUSIES, MOUSSES, POUSSES, PLUSSES, PLISSES, PRISSES, PRESSES, PREASES, UREASES, UNASES, UNCASES, UNASAS, UNBASED, UNBATED, UNMATED, UNMETED, UNMEWED, ENDEWED, ENDEWED INDEX, INDENES, INDENTS, INCENTS, INCESTS, INFESTS, INFECTS, INJECTS, 56
Et l'une des paires de 8 lettres solubles les plus défavorables:
ENROBING, UNROBING, UNROPING, UNCOPING, UNCAPING, UNCAGING, ENCAGING, ENRAGING, ENRACING, ENLACING, UNLACING, UNLAYING, UPLAYING, SPLAYING, SPRAYING, STRAYING, STOUMING, STOUMING, STOUMING CRIMPING, CRISPING, CRISPINS, CRISPENS, CRISPERS, CRIMPERS, CRAMPERS, CLAMPERS, CLASPERS, CLASHERS, SLASHERS, SLATHERS, SLITHERS, SMITHERS, SMOTHERS, SWOTHERS, SUDERS, MOUTHERS, MOUCHERS, COUCHERS, PACHERS, POACHERS, POACHERS, POACHERS LUNCHERS, LYNCHERS, LYNCHETS, LINCHETS, 52
Maintenant que je pense avoir éliminé toutes les exigences de la question, ma discussion.
Pour un CompSci, la question se réduit évidemment au chemin le plus court dans un graphe G dont les sommets sont des mots et dont les bords relient des mots différents dans une lettre. Générer le graphique efficacement n'est pas anodin - j'ai en fait une idée que je dois revoir pour réduire la complexité à O (V n hash + E). La façon dont je le fais consiste à créer un graphique qui insère des sommets supplémentaires (correspondant aux mots avec un caractère générique) et qui est homéomorphe au graphique en question. J'ai envisagé d'utiliser ce graphique plutôt que de le réduire à G - et je suppose que d'un point de vue golfique j'aurais dû le faire - sur la base qu'un nœud générique avec plus de 3 arêtes réduit le nombre d'arêtes dans le graphique, et le le temps d'exécution standard du pire cas des algorithmes de chemin le plus court est O(V heap-op + E)
.
Cependant, la première chose que j'ai faite a été d'exécuter des analyses des graphiques G pour différentes longueurs de mots, et j'ai découvert qu'elles sont extrêmement rares pour les mots de 5 lettres ou plus. Le graphique à 5 lettres a 12478 sommets et 40759 arêtes; l'ajout de nœuds de lien aggrave le graphique. Au moment où vous avez jusqu'à 8 lettres, il y a moins de bords que de nœuds, et 3/7 des mots sont "distants". J'ai donc rejeté cette idée d'optimisation car elle n'était pas vraiment utile.
L'idée qui s'est avérée utile était d'examiner le tas. Je peux honnêtement dire que j'ai mis en œuvre des tas modérément exotiques dans le passé, mais aucun aussi exotique que cela. J'utilise une étoile A (puisque C n'apporte aucun avantage compte tenu du tas que j'utilise) avec l'heuristique évidente du nombre de lettres différentes de la cible, et un peu d'analyse montre qu'à tout moment il n'y a pas plus de 3 priorités différentes dans le tas. Lorsque je fais apparaître un nœud dont la priorité est (coût + heuristique) et que je regarde ses voisins, je considère trois cas: 1) le coût du voisin est le coût + 1; l'heuristique du voisin est l'heuristique-1 (car la lettre qu'elle change devient "correcte"); 2) coût + 1 et heuristique + 0 (parce que la lettre qu'il change passe de "faux" à "toujours faux"; 3) coût + 1 et heuristique + 1 (car la lettre qu'il change passe de «correcte» à «fausse»). Donc, si je détends le voisin, je vais l'insérer à la même priorité, priorité + 1 ou priorité + 2. En conséquence, je peux utiliser un tableau à 3 éléments de listes liées pour le tas.
Je devrais ajouter une note sur mon hypothèse selon laquelle les recherches de hachage sont constantes. Très bien, direz-vous, mais qu'en est-il des calculs de hachage? La réponse est que je les amortis: java.lang.String
met en cache son hashCode()
, donc le temps total passé à calculer les hachages est O(V n^2)
(pour générer le graphique).
Il y a un autre changement qui affecte la complexité, mais la question de savoir s'il s'agit d'une optimisation ou non dépend de vos hypothèses sur les statistiques. (IMO mettant "la meilleure solution Big O" comme critère est une erreur car il n'y a pas de meilleure complexité, pour une raison simple: il n'y a pas une seule variable). Cette modification affecte l'étape de génération du graphique. Dans le code ci-dessus, c'est:
Map<String, Set<String>> wordsToLinks = new HashMap<String, Set<String>>();
Map<String, Set<String>> linksToWords = new HashMap<String, Set<String>>();
// Cost: O(Vn * (n + hash))
for (String word : words)
{
// Cost: O(n*(n + hash))
for (int i = 0; i < word.length(); i++)
{
// Cost: O(n + hash)
char[] ch = word.toCharArray();
ch[i] = '.';
String link = new String(ch).intern();
add(wordsToLinks, word, link);
add(linksToWords, link, word);
}
}
// Cost: O(V * n * hash + E * hash)
for (Map.Entry<String, Set<String>> from : wordsToLinks.entrySet()) {
String src = from.getKey();
wordsToWords.put(src, new HashSet<String>());
for (String link : from.getValue()) {
Set<String> to = linksToWords.get(link);
for (String snk : to) {
// Note: equality test is safe here. Cost is O(hash)
if (snk != src) add(wordsToWords, src, snk);
}
}
}
Voilà O(V * n * (n + hash) + E * hash)
. Mais la O(V * n^2)
partie vient de la génération d'une nouvelle chaîne de n caractères pour chaque lien, puis du calcul de son code de hachage. Cela peut être évité avec une classe d'assistance:
private static class Link
{
private String str;
private int hash;
private int missingIdx;
public Link(String str, int hash, int missingIdx) {
this.str = str;
this.hash = hash;
this.missingIdx = missingIdx;
}
@Override
public int hashCode() { return hash; }
@Override
public boolean equals(Object obj) {
Link l = (Link)obj; // Unsafe, but I know the contexts where I'm using this class...
if (this == l) return true; // Essential
if (hash != l.hash || missingIdx != l.missingIdx) return false;
for (int i = 0; i < str.length(); i++) {
if (i != missingIdx && str.charAt(i) != l.str.charAt(i)) return false;
}
return true;
}
}
Ensuite, la première moitié de la génération du graphique devient
Map<String, Set<Link>> wordsToLinks = new HashMap<String, Set<Link>>();
Map<Link, Set<String>> linksToWords = new HashMap<Link, Set<String>>();
// Cost: O(V * n * hash)
for (String word : words)
{
// apidoc: The hash code for a String object is computed as
// s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
// Cost: O(n * hash)
int hashCode = word.hashCode();
int pow = 1;
for (int j = word.length() - 1; j >= 0; j--) {
Link link = new Link(word, hashCode - word.charAt(j) * pow, j);
add(wordsToLinks, word, link);
add(linksToWords, link, word);
pow *= 31;
}
}
En utilisant la structure du code de hachage, nous pouvons générer les liens dans O(V * n)
. Cependant, cela a un effet d'entraînement. Inhérent à mon hypothèse que les recherches de hachage sont à temps constant est une hypothèse selon laquelle la comparaison des objets pour l'égalité est bon marché. Cependant, le test d'égalité de Link est O(n)
dans le pire des cas. Le pire des cas est lorsque nous avons une collision de hachage entre deux liens égaux générés à partir de mots différents - c'est-à-dire qu'elle se produit O(E)
fois dans la seconde moitié de la génération du graphe. En dehors de cela, sauf dans le cas peu probable d'une collision de hachage entre des liens non égaux, nous sommes bons. Nous avons donc échangé O(V * n^2)
pour O(E * n * hash)
. Voir mon point précédent sur les statistiques.
HOUSE
àGORGE
soit signalée comme 2. Je me rends compte qu'il y a 2 mots intermédiaires, donc cela a du sens, mais le nombre d'opérations serait plus intuitif.