1P5: Wortwechsler

20

Dies wurde als Teil des First Periodic Premier Programming Puzzle Push geschrieben .

Das Spiel

Es werden ein Start- und ein Endwort gleicher Länge bereitgestellt. Das Ziel des Spiels ist es, einen Buchstaben im Startwort zu ändern, um ein anderes gültiges Wort zu bilden. Wiederholen Sie diesen Schritt, bis das Endwort erreicht ist, und verwenden Sie dabei die geringste Anzahl von Schritten. Bei den Wörtern TREE und FLED wäre die Ausgabe beispielsweise:

TREE
FREE
FLEE
FLED
2

Spezifikationen

  • Die Wikipedia-Artikel für die OWL oder SOWPODS könnten ein nützlicher Ausgangspunkt sein, was Wortlisten betrifft .
  • Das Programm sollte zwei Arten der Auswahl der Start- und Endwörter unterstützen:
    1. Wird vom Benutzer über die Befehlszeile, stdin oder eine andere für die Sprache Ihrer Wahl geeignete Sprache angegeben (geben Sie nur an, was Sie tun).
    2. Auswahl von 2 zufälligen Wörtern aus der Datei.
  • Die Start- und Endwörter sowie alle Zwischenwörter sollten gleich lang sein.
  • Jeder Schritt sollte in seiner Zeile ausgedruckt werden.
  • Die letzte Zeile Ihrer Ausgabe sollte die Anzahl der Zwischenschritte sein, die zwischen dem Anfangs- und dem Endwort erforderlich waren.
  • Wenn zwischen dem Anfangs- und dem Endwort keine Übereinstimmung gefunden werden kann, sollte die Ausgabe aus drei Zeilen bestehen: dem Anfangswort, dem Endwort und dem Wort OY.
  • Nehmen Sie die Big O-Notation für Ihre Lösung in Ihre Antwort auf
  • Bitte geben Sie 10 eindeutige Start- und Endwortpaare an (natürlich mit ihrer Ausgabe), um die Schritte anzuzeigen, die Ihr Programm erzeugt. (Um Platz zu sparen, kann Ihr Programm diese in einzelnen Zeilen ausgeben, Sie können diese jedoch zu einer einzelnen Zeile zusammenfassen, um sie zu veröffentlichen. Dabei werden neue Zeilen durch Leerzeichen und ein Komma zwischen den einzelnen Zeilen ersetzt.

Ziele / Gewinnkriterien

  • Die schnellste / beste Big O-Lösung mit den kürzesten Zwischenschritten nach einer Woche gewinnt.
  • Wenn sich aus den Big O-Kriterien ein Gleichstand ergibt, gewinnt der kürzeste Code.
  • Wenn es immer noch ein Unentschieden gibt, gewinnt die erste Lösung, die ihre schnellste und kürzeste Revision erreicht.

Tests / Beispielausgabe

DIVE
DIME
DAME
NAME
2

PEACE
PLACE
PLATE
SLATE
2

HOUSE
HORSE
GORSE
GORGE
2

POLE
POSE
POST
PAST
FAST
3

Validierung

Ich arbeite an einem Skript, mit dem die Ausgabe überprüft werden kann.

Es wird:

  1. Stellen Sie sicher, dass jedes Wort gültig ist.
  2. Stellen Sie sicher, dass sich jedes Wort genau um einen Buchstaben vom vorherigen Wort unterscheidet.

Es wird nicht:

  1. Überprüfen Sie, ob die kürzeste Anzahl von Schritten verwendet wurde.

Sobald ich das geschrieben habe, werde ich diesen Beitrag natürlich aktualisieren. (:

Rebecca Chernoff
quelle
4
Mir kommt es seltsam vor, dass das Ausführen von 3 Operationen, von denen aus man HOUSEzu GORGEkommt, 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.
Matthew Read
4
@ Peter, laut sowpods wikipedia Seite gibt es ~ 15k Wörter länger als 13 Buchstaben
gnibbler
4
Ich meine nicht als wissen es alle, aber das Puzzle tatsächlich einen Namen hat, wird es durch Lewis Carroll erfunden wurde en.wikipedia.org/wiki/Word_ladder
st0le
1
Sie haben ein unentscheidbares Ziel in der Frage: The fastest/best Big O solution producing the shortest interim steps after one week will win.Da Sie nicht garantieren können, dass die schnellste Lösung inzwischen diejenige ist, die die wenigsten Schritte verwendet, sollten Sie eine Präferenz angeben, wenn eine Lösung weniger Schritte verwendet, das Ziel jedoch später erreicht.
Benutzer unbekannt
2
Ich möchte nur bestätigen BATund CAThabe keine Schritte, richtig?
st0le

Antworten:

9

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.StringZwischenspeichern 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.

Peter Taylor
quelle
Ich glaube, 8192 ist die Standardpuffergröße für BufferedReader (auf SunVM)
st0le
@ st0le, ich habe diesen Parameter in der Golf-Version weggelassen und er schadet dem nicht-Golf-Parameter nicht.
Peter Taylor
5

Java

Komplexität : ?? (Ich habe keinen CompSci-Abschluss, daher würde ich mich über Hilfe in dieser Angelegenheit freuen.)

Eingabe : Geben Sie ein Wortpaar (mehr als 1 Paar, wenn Sie dies wünschen) in die Befehlszeile ein. Wenn keine Befehlszeile angegeben ist, werden 2 verschiedene zufällige Wörter ausgewählt.

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

public class M {

    // for memoization
    private static Map<String, List<String>> memoEdits = new HashMap<String, List<String>>(); 
    private static Set<String> dict;

    private static List<String> edits(String word, Set<String> dict) {
        if(memoEdits.containsKey(word))
            return memoEdits.get(word);

        List<String> editsList = new LinkedList<String>();
        char[] letters = word.toCharArray();
        for(int i = 0; i < letters.length; i++) {
            char hold = letters[i];
            for(char ch = 'A'; ch <= 'Z'; ch++) {
                if(ch != hold) {
                    letters[i] = ch;
                    String nWord = new String(letters);
                    if(dict.contains(nWord)) {
                        editsList.add(nWord);
                    }
                }
            }
            letters[i] = hold;
        }
        memoEdits.put(word, editsList);
        return editsList;
    }

    private static Map<String, String> bfs(String wordFrom, String wordTo,
                                           Set<String> dict) {
        Set<String> visited = new HashSet<String>();
        List<String> queue = new LinkedList<String>();
        Map<String, String> pred = new HashMap<String, String>();
        queue.add(wordFrom);
        while(!queue.isEmpty()) {
            String word = queue.remove(0);
            if(word.equals(wordTo))
                break;

            for(String nWord: edits(word, dict)) {
                if(!visited.contains(nWord)) {
                    queue.add(nWord);
                    visited.add(nWord);
                    pred.put(nWord, word);
                }
            }
        }
        return pred;
    }

    public static void printPath(String wordTo, String wordFrom) {
        int c = 0;
        Map<String, String> pred = bfs(wordFrom, wordTo, dict);
        do {
            System.out.println(wordTo);
            c++;
            wordTo = pred.get(wordTo);
        }
        while(wordTo != null && !wordFrom.equals(wordTo));
        System.out.println(wordFrom);
        if(wordTo != null)
            System.out.println(c - 1);
        else
            System.out.println("OY");
        System.out.println();
    }

    public static void main(String[] args) throws Exception {
        BufferedReader scan = new BufferedReader(new FileReader(new File("c:\\332609\\dict.txt")),
                                                 40 * 1024);
        String line;
        dict = new HashSet<String>(); //the dictionary (1 word per line)
        while((line = scan.readLine()) != null) {
            dict.add(line);
        }
        scan.close();
        if(args.length == 0) { // No Command line Arguments? Pick 2 random
                               // words.
            Random r = new Random(System.currentTimeMillis());
            String[] words = dict.toArray(new String[dict.size()]);
            int x = r.nextInt(words.length), y = r.nextInt(words.length);
            while(x == y) //same word? that's not fun...
                y = r.nextInt(words.length);
            printPath(words[x], words[y]);
        }
        else { // Arguments provided, search for path pairwise
            for(int i = 0; i < args.length; i += 2) {
                if(i + 1 < args.length)
                    printPath(args[i], args[i + 1]);
            }
        }
    }
}
st0le
quelle
Ich habe Memoization verwendet, um schnellere Ergebnisse zu erzielen. Der Dictionary-Pfad ist fest codiert.
st0le
@Joey, früher war es aber nicht mehr. Es hat jetzt ein statisches Feld, das jedes Mal erhöht und ergänzt wird System.nanoTime().
Peter Taylor
@Joey, aah, ok, aber ich lasse es jetzt, ich möchte meine Revisionen nicht
erhöhen
Oh, übrigens, ich bin auf der Arbeit und diese Scrabble-Websites sind anscheinend blockiert, so dass ich keinen Zugriff auf die Wörterbücher habe ... diese 10 einzigartigen Wörter werden am besten bis morgen früh generiert. Prost!
st0le
Sie können die (rechnerische) Komplexität reduzieren, indem Sie ein bidirektionales bfs ausführen, dh von beiden Seiten suchen und anhalten, wenn Sie auf einen von der anderen Seite besuchten Knoten stoßen.
Nabb
3

c unter Unix

Verwendung des Dijkstra-Algorithmus.

Ein großer Teil des Codes ist eine Kostüm-N-Ary-Tree-Implementierung, die zum Halten dient

  • Die Wortliste (minimiert somit die Häufigkeit, mit der die Eingabedatei gelesen wird (zweimal für keine Argumente, einmal für andere Fälle), unter der Annahme, dass die Datei-E / A langsam ist
  • Die Teilbäume, wie wir sie bauen.
  • Der letzte Weg.

Wer daran interessiert, wie es funktioniert , sollten Sie vielleicht gelesen findPath, processund processOne(und ihre zugehörigen Kommentare). Und vielleicht buildPathund buildPartialPath. Der Rest ist Buchhaltung und Gerüste. Einige Routinen, die beim Testen und Entwickeln verwendet wurden, jedoch nicht in der "Produktions" -Version, wurden beibehalten.

Ich verwende /usr/share/dict/wordsauf meinem Mac OS 10.5 - Box, die so viele lange, esoterische Einträge hat , dass lässt sie völlig zufällig laufen eine erzeugt viel von OYs.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getline.h>
#include <time.h>
#include <unistd.h>
#include <ctype.h>

const char*wordfile="/usr/share/dict/words";
/* const char*wordfile="./testwords.txt"; */
const long double RANDOM_MAX = (2LL<<31)-1;

typedef struct node_t {
  char*word;
  struct node_t*kids;
  struct node_t*next;
} node;


/* Return a pointer to a newly allocated node. If word is non-NULL, 
 * call setWordNode;
 */
node*newNode(char*word){
  node*n=malloc(sizeof(node));
  n->word=NULL;
  n->kids=NULL;
  n->next=NULL;
  if (word) n->word = strdup(word);
  return n;
}
/* We can use the "next" links to treat these as a simple linked list,
 * and further can make it a stack or queue by
 *
 * * pop()/deQueu() from the head
 * * push() onto the head
 * * enQueue at the back
 */
void push(node*n, node**list){
  if (list==NULL){
    fprintf(stderr,"Active operation on a NULL list! Exiting\n");
    exit(5);
  }
  n->next = (*list);
  (*list) = n;
}
void enQueue(node*n, node**list){
  if (list==NULL){
    fprintf(stderr,"Active operation on a NULL list! Exiting\n");
    exit(5);
  }
  if ( *list==NULL ) {
    *list=n;
  } else {
    enQueue(n,&((*list)->next));
  }
}
node*pop(node**list){
  node*temp=NULL;
  if (list==NULL){
    fprintf(stderr,"Active operation on a NULL list! Exiting\n");
    exit(5);
  }
  temp = *list;
  if (temp != NULL) {
    (*list) = temp->next;
    temp->next=NULL;
  }
  return temp;
}
node*deQueue(node**list){ /* Alias for pop */
  return pop(list);
}

/* return a pointer to a node in tree matching word or NULL if none */
node* isInTree(char*word, node*tree){
  node*isInNext=NULL;
  node*isInKids=NULL;
  if (tree==NULL || word==NULL) return NULL;
  if (tree->word && (0 == strcasecmp(word,tree->word))) return tree;
  /* prefer to find the target at shallow levels so check the siblings
     before the kids */
  if (tree->next && (isInNext=isInTree(word,tree->next))) return isInNext;
  if (tree->kids && (isInKids=isInTree(word,tree->kids))) return isInKids;
  return NULL;
}

node* freeTree(node*t){
  if (t==NULL) return NULL;
  if (t->word) {free(t->word); t->word=NULL;}
  if (t->next) t->next=freeTree(t->next);
  if (t->kids) t->kids=freeTree(t->kids);
  free(t);
  return NULL;
}

void printTree(node*t, int indent){
  int i;
  if (t==NULL) return;
  for (i=0; i<indent; i++) printf("\t"); printf("%s\n",t->word);
  printTree(t->kids,indent+1);
  printTree(t->next,indent);
}

/* count the letters of difference between two strings */
int countDiff(const char*w1, const char*w2){
  int count=0;
  if (w1==NULL || w2==NULL) return -1;
  while ( (*w1)!='\0' && (*w2)!='\0' ) {
    if ( (*w1)!=(*w2) ) count++;
    w1++;
    w2++;
  }
  return count;
}

node*buildPartialPath(char*stop, node*tree){
  node*list=NULL;
  while ( (tree != NULL) && 
      (tree->word != NULL) && 
      (0 != strcasecmp(tree->word,stop)) ) {
    node*kid=tree->kids;
    node*newN = newNode(tree->word);
    push(newN,&list);
    newN=NULL;
    /* walk over all all kids not leading to stop */
    while ( kid && 
        (strcasecmp(kid->word,stop)!=0) &&
        !isInTree(stop,kid->kids) ) {
      kid=kid->next;
    }
    if (kid==NULL) {
      /* Assuming a preconditions where isInTree(stop,tree), we should
       * not be able to get here...
       */
      fprintf(stderr,"Unpossible!\n");
      exit(7);
    } 
    /* Here we've found a node that either *is* the target or leads to it */
    if (strcasecmp(stop,kid->word) == 0) {
      break;
    }
    tree = kid;
  }
  return list; 
}
/* build a node list path 
 *
 * We can walk down each tree, identfying nodes as we go
 */
node*buildPath(char*pivot,node*frontTree,node*backTree){
  node*front=buildPartialPath(pivot,frontTree);
  node*back=buildPartialPath(pivot,backTree);
  /* weld them together with pivot in between 
  *
  * The front list is in reverse order, the back list in order
  */
  node*thePath=NULL;
  while (front != NULL) {
    node*n=pop(&front);
    push(n,&thePath);
  }
  if (pivot != NULL) {
    node*n=newNode(pivot);
    enQueue(n,&thePath);
  }
  while (back != NULL) {
    node*n=pop(&back);
    enQueue(n,&thePath);
  }
  return thePath;
}

/* Add new child nodes to the single node in ts named by word. Also
 * queue these new word in q
 * 
 * Find node N matching word in ts
 * For tword in wordList
 *    if (tword is one change from word) AND (tword not in ts)
 *        add tword to N.kids
 *        add tword to q
 *        if tword in to
 *           return tword
 * return NULL
 */
char* processOne(char *word, node**q, node**ts, node**to, node*wordList){
  if ( word==NULL || q==NULL || ts==NULL || to==NULL || wordList==NULL ) {
    fprintf(stderr,"ProcessOne called with NULL argument! Exiting.\n");
    exit(9);
  }
  char*result=NULL;
  /* There should be a node in ts matching the leading node of q, find it */
  node*here = isInTree(word,*ts);
  /* Now test each word in the list as a possible child of HERE */
  while (wordList != NULL) {
    char *tword=wordList->word;
    if ((1==countDiff(word,tword)) && !isInTree(tword,*ts)) {
      /* Queue this up as a child AND for further processing */
      node*newN=newNode(tword);
      enQueue(newN,&(here->kids));
      newN=newNode(tword);
      enQueue(newN,q);
      /* This might be our pivot */
      if ( isInTree(tword,*to) ) {
    /* we have found a node that is in both trees */
    result=strdup(tword);
    return result;
      }
    }
    wordList=wordList->next;
  }
  return result;
}

/* Add new child nodes to ts for all the words in q */
char* process(node**q, node**ts, node**to, node*wordList){
  node*tq=NULL;
  char*pivot=NULL;
  if ( q==NULL || ts==NULL || to==NULL || wordList==NULL ) {
    fprintf(stderr,"Process called with NULL argument! Exiting.\n");
    exit(9);
  }
  while (*q && (pivot=processOne((*q)->word,&tq,ts,to,wordList))==NULL) {
    freeTree(deQueue(q));
  }
  freeTree(*q); 
  *q=tq;
  return pivot;
}

/* Find a path between w1 and w2 using wordList by dijkstra's
 * algorithm
 *
 * Use a breadth-first extensions of the trees alternating between
 * trees.
 */
node* findPath(char*w1, char*w2, node*wordList){
  node*thePath=NULL; /* our resulting path */
  char*pivot=NULL; /* The node we find that matches */
  /* trees of existing nodes */
  node*t1=newNode(w1); 
  node*t2=newNode(w2);
  /* queues of nodes to work on */
  node*q1=newNode(w1);
  node*q2=newNode(w2);

  /* work each queue all the way through alternating until a word is
     found in both lists */
  while( (q1!=NULL) && ((pivot = process(&q1,&t1,&t2,wordList)) == NULL) &&
     (q2!=NULL) && ((pivot = process(&q2,&t2,&t1,wordList)) == NULL) )
    /* no loop body */ ;


  /* one way or another we are done with the queues here */
  q1=freeTree(q1);
  q2=freeTree(q2);
  /* now construct the path */
  if (pivot!=NULL) thePath=buildPath(pivot,t1,t2);
  /* clean up after ourselves */
  t1=freeTree(t1);
  t2=freeTree(t2);

  return thePath;
}

/* Convert a non-const string to UPPERCASE in place */
void upcase(char *s){
  while (s && *s) {
    *s = toupper(*s);
    s++;
  }
}

/* Walks the input file stuffing lines of the given length into a list */
node*getListWithLength(const char*fname, int len){
  int l=-1;
  size_t n=0;
  node*list=NULL;
  char *line=NULL;
  /* open the word file */
  FILE*f = fopen(fname,"r");
  if (NULL==f){
    fprintf(stderr,"Could not open word file '%s'. Exiting.\n",fname);
    exit(3);
  }
  /* walk the file, trying each word in turn */
  while ( !feof(f) && ((l = getline(&line,&n,f)) != -1) ) {
    /* strip trailing whitespace */
    char*temp=line;
    strsep(&temp," \t\n");
    if (strlen(line) == len) {
      node*newN = newNode(line);
      upcase(newN->word);
      push(newN,&list);
    }
  }
  fclose(f);
  return list;
}

/* Assumes that filename points to a file containing exactly one
 * word per line with no other whitespace.
 * It will return a randomly selected word from filename.
 *
 * If veto is non-NULL, only non-matching words of the same length
 * wll be considered.
 */
char*getRandomWordFile(const char*fname, const char*veto){
  int l=-1, count=1;
  size_t n=0;
  char *word=NULL;
  char *line=NULL;
  /* open the word file */
  FILE*f = fopen(fname,"r");
  if (NULL==f){
    fprintf(stderr,"Could not open word file '%s'. Exiting.\n",fname);
    exit(3);
  }
  /* walk the file, trying each word in turn */
  while ( !feof(f) && ((l = getline(&line,&n,f)) != -1) ) {
    /* strip trailing whitespace */
    char*temp=line;
    strsep(&temp," \t\n");
    if (strlen(line) < 2) continue; /* Single letters are too easy! */
    if ( (veto==NULL) || /* no veto means chose from all */ 
     ( 
      ( strlen(line) == strlen(veto) )  && /* veto means match length */
      ( 0 != strcasecmp(veto,line) )       /* but don't match word */ 
       ) ) { 
      /* This word is worthy of consideration. Select it with random
         chance (1/count) then increment count */
      if ( (word==NULL) || (random() < RANDOM_MAX/count) ) {
    if (word) free(word);
    word=strdup(line);
      }
      count++;
    }
  }
  fclose(f);
  upcase(word);
  return word;
}

void usage(int argc, char**argv){
  fprintf(stderr,"%s [ <startWord> [ <endWord> ]]:\n\n",argv[0]);
  fprintf(stderr,
      "\tFind the shortest transformation from one word to another\n");
  fprintf(stderr,
      "\tchanging only one letter at a time and always maintaining a\n");
  fprintf(stderr,
      "\tword that exists in the word file.\n\n");
  fprintf(stderr,
      "\tIf startWord is not passed, chose at random from '%s'\n",
      wordfile);
  fprintf(stderr,
      "\tIf endWord is not passed, chose at random from '%s'\n",
      wordfile);
  fprintf(stderr,
      "\tconsistent with the length of startWord\n");
  exit(2);
}

int main(int argc, char**argv){
  char *startWord=NULL;
  char *endWord=NULL;

  /* intialize OS services */
  srandom(time(0)+getpid());
  /* process command line */
  switch (argc) {
  case 3:
    endWord = strdup(argv[2]);
    upcase(endWord);
  case 2:
    startWord = strdup(argv[1]);
    upcase(startWord);
  case 1:
    if (NULL==startWord) startWord = getRandomWordFile(wordfile,NULL);
    if (NULL==endWord)   endWord   = getRandomWordFile(wordfile,startWord);
    break;
  default:
    usage(argc,argv);
    break;
  }
  /* need to check this in case the user screwed up */
  if ( !startWord || ! endWord || strlen(startWord) != strlen(endWord) ) {
    fprintf(stderr,"Words '%s' and '%s' are not the same length! Exiting\n",
        startWord,endWord);
    exit(1);
  }
  /* Get a list of all the words having the right length */
  node*wordList=getListWithLength(wordfile,strlen(startWord));
  /* Launch into the path finder*/
  node *theList=findPath(startWord,endWord,wordList);
  /* Print the resulting path */
  if (theList) {
    int count=-2;
    while (theList) {
      printf("%s\n",theList->word);
      theList=theList->next;
      count++;
    }
    printf("%d\n",count);
  } else {
    /* No path found case */
    printf("%s %s OY\n",startWord,endWord);
  }
  return 0;
}

Einige Ausgaben:

$ ./changeword dive name
DIVE
DIME
DAME
NAME
2
$ ./changeword house gorge
HOUSE
HORSE
GORSE
GORGE
2
$ ./changeword stop read
STOP
STEP
SEEP
SEED
REED
READ
4
$ ./changeword peace slate
PEACE
PLACE
PLATE
SLATE
2
$ ./changeword pole fast  
POLE
POSE
POST
PAST
FAST
3
$ ./changeword          
QUINTIPED LINEARITY OY
$ ./changeword sneaky   
SNEAKY WAXILY OY
$ ./changeword TRICKY
TRICKY
PRICKY
PRINKY
PRANKY
TRANKY
TWANKY
SWANKY
SWANNY
SHANNY
SHANTY
SCANTY
SCATTY
SCOTTY
SPOTTY
SPOUTY
STOUTY
STOUTH
STOUSH
SLOUSH
SLOOSH
SWOOSH
19
$ ./changeword router outlet
ROUTER
ROTTER
RUTTER
RUTHER
OUTHER
OUTLER
OUTLET
5
$ ./changeword 
IDIOM
IDISM
IDIST
ODIST
OVIST
OVEST
OVERT
AVERT
APERT
APART
SPART
SPARY
SEARY
DEARY
DECRY
DECAY
DECAN
DEDAN
SEDAN
17

Die Komplexitätsanalyse ist nicht trivial. Die Suche ist eine zweiseitige, iterative Vertiefung.

  • Für jeden untersuchten Knoten gehe ich die gesamte Wortliste durch (wenn auch auf Wörter mit der richtigen Länge begrenzt). Nennen Sie die Länge der Liste W.
  • Die minimale Anzahl von Schritten ist, S_min = (<number of different letter>-1)weil wir, wenn wir nur einen Buchstaben voneinander entfernt sind, die Änderung bei 0 Zwischenschritten bewerten. Das Maximum ist schwer zu quantifizieren, siehe den obigen TRICKY-SWOOSH-Lauf. Jede Hälfte des Baumes wird S/2-1zuS/2
  • Ich habe keine Analyse des Verzweigungsverhaltens des Baums durchgeführt, sondern nenne es B.

Die Mindestanzahl an Operationen ist also 2 * (S/2)^B * Wnicht wirklich gut.

dmckee
quelle
Vielleicht ist das für mich naiv, aber ich sehe in Ihrem Design oder Ihrer Implementierung nichts, was Kantengewichte erfordert. Während Dijkstra's in der Tat für ungewichtete Graphen funktioniert (Kantengewicht ist immer "1"), würde hier nicht eine einfache Breitensuche angewendet, um Ihre Grenzen zu verbessern, O(|V|+|E|)anstatt O(|E|+|V| log |V|)?
MrGomez,