Da die Länge als Kriterium aufgeführt ist, ist hier die Golfversion mit 1681 Zeichen (könnte wahrscheinlich noch um 10% verbessert werden):
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>{}
Die ungolfed-Version, die Paketnamen und -methoden verwendet und keine Warnungen ausgibt oder Klassen erweitert, nur um sie zu aliasen, lautet:
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;
}
}
}
Wie Sie sehen können, ist die Analyse der laufenden Kosten O(filelength + num_words * hash + V * n * (n + hash) + E * hash)
. Wenn Sie meine Annahme akzeptieren, dass das Einfügen / Nachschlagen einer Hash-Tabelle eine konstante Zeit ist, dann ist das so O(filelength + V n^2 + E)
. Die besonderen Statistiken der Graphen in SOWPODS bedeuten, dass O(V n^2)
sie O(E)
für die meisten wirklich dominieren n
.
Beispielausgaben:
IDOLA, IDOLS, IDYLS, ODYLS, ODALS, OVALS, OVELS, OVENS, EVENS, ETENS, STENS, SKENS, SKINS, SPINS, SPINE, 13
WICCA, PROSY, OY
BRINY, BRINS, TRINS, TAINS, TARNS, YARNS, YAWNS, YAWPS, YAPPS, 7
GALES, GASES, GASTS, GESTS, GESTE, GESSE, DESSE, 5
SURES, DURES, DUNEN, DINES, DINGS, DINGY, 4
LICHT, LICHT, BICHT, BIGOT, BIGOS, BIROS, GIROS, MÄDCHEN, GURNS, GUANS, GUANA, RUANA, 10
SARGE, SERGE, SERRE, SERRS, SEER, DEERS, DYERS, OYERS, OVERS, OVELS, OVALS, ODALS, ODYLS, IDYLS, 12
KEIRS, SEIRS, SEHER, BIER, BRER, BRERE, BREME, CREME, CREPE, 7
Dies ist eines der 6 Paare mit dem längsten kürzesten Weg:
GAINEST, FAINEST, FAIREST, SAIREST, SAIDEST, SADDEST, MADDEST, MIDDEST, MILDEST, WILDEST, WILIEST, WALIEST, WANIEST, CANIEST, CANTEST, WETTBEWERB, CONFEST, CONFESS, CONFERS, COOPERS, COOPERS, COP, POPPITS, POPPIES, POPSIES, MOPSIES, MOUSIES, MOUSSES, POUSSES, PLUSSES, PLISSES, PRISSES, PRESSES, PREASES, UREASES, UNEASES, UNCASES, UNCASED, UNBASED, UNVERMITTELT, UNVERMITTELT, VERMITTELT, ENDET INDEXES, INDENES, INDENTS, INCENTS, INCESTS, INFESTS, INFECTS, INJECTS, 56
Und eines der schlimmsten löslichen 8-Buchstaben-Paare:
Anstechen, Ausstechen, Ausstechen, Ausstechen, Ausstechen, Ausstechen, Ausstechen, Ausstechen, Ausstechen, Ausstechen, Ausstechen, Ausstechen, Aufspielen, Ausstechen, Stapfen, Stolpern, Stolpern CRIMPING, CRISPING, CRISPINS, CRISPENS, CRISPERS, CRIMPERS, CRAMPERS, CLAMPERS, CLASPERS, CLASHERS, SLASHERS, SLATHERS, SLITHERS, SMITHERS, SMOTHERS, SOOTHERS, SOUTHERS, POCHERS, MOUTHERS, MOUCHERS LUNCHERS, LYNCHERS, LYNCHETS, LINCHETS, 52
Jetzt, wo ich denke, dass ich alle Anforderungen der Frage aus dem Weg habe, meine Diskussion.
Für einen CompSci reduziert sich die Frage offensichtlich auf den kürzesten Weg in einem Graphen G, dessen Eckpunkte Wörter sind und dessen Kanten Wörter verbinden, die sich in einem Buchstaben unterscheiden. Das effiziente Generieren des Diagramms ist nicht trivial - ich habe tatsächlich eine Idee, die ich überdenken muss, um die Komplexität auf O (V n -Hash + E) zu reduzieren. Die Art und Weise, wie ich das mache, besteht darin, ein Diagramm zu erstellen, das zusätzliche Eckpunkte (die Wörtern mit einem Platzhalterzeichen entsprechen) einfügt und homöomorph zu dem fraglichen Diagramm ist. Ich habe darüber nachgedacht, diesen Graphen zu verwenden, anstatt ihn auf G zu reduzieren - und ich nehme an, dass ich dies aus Golfsicht hätte tun sollen - auf der Grundlage, dass ein Platzhalterknoten mit mehr als 3 Kanten die Anzahl der Kanten im Graphen reduziert und Die Standardlaufzeit im ungünstigsten Fall für Algorithmen mit kürzestem Pfad beträgt O(V heap-op + E)
.
Als erstes habe ich jedoch einige Analysen der Grafiken G für verschiedene Wortlängen durchgeführt und festgestellt, dass sie für Wörter mit 5 oder mehr Buchstaben äußerst spärlich sind. Das 5-Buchstaben-Diagramm hat 12478 Eckpunkte und 40759 Kanten. Das Hinzufügen von Verbindungsknoten verschlechtert das Diagramm. Wenn Sie bis zu 8 Buchstaben haben, gibt es weniger Kanten als Knoten, und 3/7 der Wörter sind "fern". Daher habe ich diese Optimierungsidee als nicht wirklich hilfreich abgelehnt.
Die Idee, die sich als hilfreich erwies, bestand darin, den Haufen zu untersuchen. Ich kann ehrlich sagen, dass ich in der Vergangenheit einige mäßig exotische Haufen implementiert habe, aber keine so exotische wie diese. Ich verwende A-Stern (da C keinen Vorteil bietet, wenn ich den Haufen verwende) mit der offensichtlichen Heuristik der Anzahl der Buchstaben, die sich vom Ziel unterscheiden, und eine kleine Analyse zeigt, dass es zu keinem Zeitpunkt mehr als 3 verschiedene Prioritäten gibt auf dem Haufen. Wenn ich einen Knoten mit der Priorität (Kosten + Heuristik) platziere und seine Nachbarn betrachte, gibt es drei Fälle, die ich in Betracht ziehe: 1) Die Kosten des Nachbarn sind Kosten + 1; Die Heuristik des Nachbarn ist heuristisch-1 (weil der geänderte Buchstabe "korrekt" wird). 2) Kosten + 1 und Heuristik + 0 (weil der geänderte Buchstabe von "falsch" in "immer noch falsch" wechselt; 3) Kosten + 1 und Heuristik + 1 (da der geänderte Buchstabe von "richtig" in "falsch" wechselt). Wenn ich also den Nachbarn entspanne, füge ich ihn mit derselben Priorität, Priorität + 1 oder Priorität + 2 ein. Als Ergebnis kann ich ein 3-Element-Array von verknüpften Listen für den Heap verwenden.
Ich sollte eine Anmerkung zu meiner Annahme hinzufügen, dass Hash-Lookups konstant sind. Sehr gut, können Sie sagen, aber was ist mit Hash-Berechnungen? Die Antwort ist, dass ich sie abschreibe: java.lang.String
Zwischenspeichern hashCode()
, so dass die Gesamtzeit, die für das Berechnen von Hashes aufgewendet wird O(V n^2)
(für das Generieren des Diagramms).
Es gibt eine weitere Änderung, die sich auf die Komplexität auswirkt. Die Frage, ob es sich um eine Optimierung handelt oder nicht, hängt jedoch von Ihren statistischen Annahmen ab. (IMO "die beste Big O-Lösung" als Kriterium zu setzen, ist ein Fehler, weil es aus einem einfachen Grund keine beste Komplexität gibt: Es gibt keine einzige Variable). Diese Änderung wirkt sich auf den Schritt der Diagrammerstellung aus. Im obigen Code ist es:
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);
}
}
}
Das ist O(V * n * (n + hash) + E * hash)
. Der O(V * n^2)
Teil besteht jedoch darin, für jede Verknüpfung eine neue Zeichenfolge mit n Zeichen zu generieren und dann ihren Hashcode zu berechnen. Dies kann mit einer Helferklasse vermieden werden:
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;
}
}
Dann wird die erste Hälfte der Graphgenerierung
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;
}
}
Mithilfe der Struktur des Hashcodes können wir die Links in generieren O(V * n)
. Dies wirkt sich jedoch nachteilig aus. In meiner Annahme, dass Hash-Lookups eine konstante Zeit sind, liegt die Annahme, dass der Vergleich von Objekten auf Gleichheit billig ist. Der Gleichstellungstest von Link ist jedoch O(n)
im schlimmsten Fall. Der schlimmste Fall ist, wenn eine Hash-Kollision zwischen zwei gleichen Links vorliegt, die aus verschiedenen Wörtern O(E)
generiert wurden. Davon abgesehen sind wir gut, außer im unwahrscheinlichen Fall einer Hash-Kollision zwischen ungleichen Links. Deshalb haben wir gehandelt O(V * n^2)
für O(E * n * hash)
. Siehe meinen vorherigen Punkt zur Statistik.
HOUSE
zuGORGE
kommt, als 2 gemeldet wird. Mir ist klar, dass es 2 Zwischenwörter gibt, daher macht es Sinn, aber die Anzahl der Operationen wäre intuitiver.