Poiché la lunghezza è elencata come criterio, ecco la versione giocata a 1681 caratteri (probabilmente potrebbe essere ancora migliorata del 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 versione ungolf, che utilizza nomi e metodi di pacchetto e non fornisce avvisi o estende le classi solo per alias loro, è:
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;
}
}
}
Come puoi vedere, l'analisi dei costi di gestione è O(filelength + num_words * hash + V * n * (n + hash) + E * hash)
. Se accetterai la mia supposizione che l'inserimento / la ricerca di una tabella hash sia un tempo costante, questo è quello O(filelength + V n^2 + E)
. Le statistiche particolari dei grafici in SOWPODS significano che O(V n^2)
domina davvero O(E)
per la maggior parten
.
Output di esempio:
IDOLA, IDOLI, IDILI, ODILI, ODALI, OVALI, OVELLI, FORNI, ANCHE, ETENE, BENE, PELLI, PELLE, SPINE, SPINE, 13
WICCA, PROSY, OY
BRINY, BRINS, TRINS, TAINS, TARNS, YARNS, YAWNS, YAWPS, YAPPS, 7
GALES, GAS, GASTS, GESTS, GESTE, GESSE, DESSE, 5
SURES, DURE, 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, SEERSS, BIRRE, BRERS, BRERE, BREME, CREME, CREPE, 7
Questa è una delle 6 coppie con il percorso più lungo più breve:
GAINEST, FAINEST, FAIREST, SAIREST, SAIDEST, SADDEST, MADDEST, MIDDEST, MILDEST, WILDEST, WILIEST, WALIEST, WANIEST, CANIEST, CANTEST, CONTEST, CONFEST, CONFESS, CONFER, CONKERS, COOKER, COOPERS, COPPERS, POPPET PAPAVERI, PAPAVERI, PAPAVERI, MOPSIE, MOUSIES, MOUSSES, POUSSES, PLUSSES, PLISSES, PRISSES, PRESSE, PREASES, UREASES, UNEASES, UNCASES, UNCASED, UNBASED, UNBATED, UNMETED, UNMEWED, INCONTRATI INDICI, INDENI, INDENTI, INCENTI, INCEST, INFEST, INFECT, INJECTS, 56
E una delle coppie di 8 lettere solubili nel peggiore dei casi:
ENROBING, UNROBING, UNROPING, UNCOPING, UNCAPING, UNCAGING, ENCAGING, ENRAGING, ENRACING, ENLACING, UNLACING, UNLAYING, UPLAYING, SPLAYING, SPRAYING, STRAYING, STROYING, STROKING, STUMING STUMING, STUMING, STUMING, STUMING CRIMPING, CRISPING, CRISPINS, CRISPENS, CRISPERS, CRIMPERS, CRAMPERS, CLAMPERS, CLASPERS, CLASHERS, SLASHERS, SLATHERS, SLITHERS, SMITHERS, SMOTHERS, SOOTHERS, SOUTHERS, MOUTHERS, MOUCHERS, COUCHERS, COACHERS, PUNTI PRANZI, LYNCHERS, LYNCHETS, LINCHETS, 52
Ora che penso di aver rimosso tutti i requisiti della domanda, la mia discussione.
Per un CompSci la domanda si riduce ovviamente al percorso più breve in un grafico G i cui vertici sono parole e i cui bordi collegano le parole che differiscono in una lettera. Generare il grafico in modo efficiente non è banale: in realtà ho un'idea che devo rivisitare per ridurre la complessità a O (V n hash + E). Il modo in cui lo faccio implica la creazione di un grafico che inserisce vertici extra (corrispondenti a parole con un carattere jolly) ed è omeomorfo al grafico in questione. Ho preso in considerazione l'uso di quel grafico anziché ridurlo a G - e suppongo che dal punto di vista del golf avrei dovuto farlo - sulla base del fatto che un nodo jolly con più di 3 bordi riduce il numero di bordi nel grafico e il il tempo di esecuzione standard nel caso peggiore degli algoritmi del percorso più breve èO(V heap-op + E)
.
Tuttavia, la prima cosa che ho fatto è stata eseguire alcune analisi dei grafici G per diverse lunghezze di parole e ho scoperto che sono estremamente rare per parole di 5 o più lettere. Il grafico a 5 lettere ha 12478 vertici e 40759 bordi; l'aggiunta di nodi di collegamento peggiora il grafico. Quando hai fino a 8 lettere ci sono meno spigoli dei nodi e 3/7 delle parole sono "distanti". Quindi ho respinto l'idea di ottimizzazione in quanto non molto utile.
L'idea che si è rivelata utile è stata quella di esaminare il mucchio. Posso onestamente dire che ho implementato alcuni cumuli moderatamente esotici in passato, ma nessuno così esotico come questo. Uso A-star (poiché C non offre alcun vantaggio dato l'heap che sto usando) con l'ovvia euristica del numero di lettere diverse dal target e un po 'di analisi mostra che in qualsiasi momento non ci sono più di 3 priorità diverse nel mucchio. Quando apro un nodo la cui priorità è (costo + euristica) e guardo i suoi vicini, sto prendendo in considerazione tre casi: 1) il costo del vicino è costo + 1; l'euristico del vicino è euristico-1 (perché la lettera che cambia diventa "corretta"); 2) costo + 1 e euristico + 0 (perché la lettera che cambia va da "sbagliato" a "ancora sbagliato"; 3) costo + 1 e euristico + 1 (perché la lettera che cambia va da "corretta" a "sbagliata"). Quindi, se rilasso il vicino, lo inserirò con la stessa priorità, priorità + 1 o priorità + 2. Di conseguenza, posso utilizzare un array a 3 elementi di elenchi collegati per l'heap.
Dovrei aggiungere una nota sulla mia ipotesi che le ricerche di hash siano costanti. Molto bene, potresti dire, ma per quanto riguarda i calcoli dell'hash? La risposta è che li sto ammortizzando via: java.lang.String
memorizza nella sua cache hashCode()
, quindi il tempo totale impiegato per calcolare gli hash è O(V n^2)
(nel generare il grafico).
C'è un altro cambiamento che influenza la complessità, ma la domanda se si tratta di un'ottimizzazione o meno dipende dalle tue ipotesi sulle statistiche. (L'IMO che definisce "la migliore soluzione Big O" come criterio è un errore perché non esiste una migliore complessità, per un semplice motivo: non esiste una singola variabile). Questa modifica influisce sul passaggio di generazione del grafico. Nel codice sopra, è:
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);
}
}
}
Questo è O(V * n * (n + hash) + E * hash)
. Ma la O(V * n^2)
parte deriva dalla generazione di una nuova stringa di n caratteri per ciascun collegamento e dal calcolo del suo hashcode. Questo può essere evitato con una classe di supporto:
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;
}
}
Quindi diventa la prima metà della generazione del grafico
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;
}
}
Usando la struttura dell'hashcode possiamo generare i collegamenti in O(V * n)
. Tuttavia, questo ha un effetto a catena. Inerente alla mia ipotesi che le ricerche di hash siano un tempo costante è un presupposto che il confronto di oggetti per l'uguaglianza sia economico. Tuttavia, il test di uguaglianza di Link è O(n)
nel peggiore dei casi. Il caso peggiore è quando si verifica una collisione dell'hash tra due collegamenti uguali generati da parole diverse, ovvero si verificano O(E)
tempi nella seconda metà della generazione del grafico. A parte questo, tranne nel caso improbabile di una collisione hash tra collegamenti non uguali, siamo a posto. Quindi ci siamo scambiati O(V * n^2)
per O(E * n * hash)
. Vedi il mio precedente punto sulle statistiche.
HOUSE
aGORGE
è segnalato come 2. Mi rendo conto che ci sono 2 parole intermedi, quindi ha senso, ma # delle operazioni sarebbe più intuitivo.