1P5: Word Changer


20

Questo è stato scritto come parte del primo periodico Premier Programming Puzzle Push .

Il gioco

Sono fornite una parola iniziale e finale della stessa lunghezza. L'obiettivo del gioco è cambiare una lettera nella parola iniziale per formare una diversa parola valida, ripetendo questo passaggio fino a raggiungere la parola finale, usando il minor numero di passaggi. Ad esempio, date le parole TREE e FLED, l'output sarebbe:

TREE
FREE
FLEE
FLED
2

specificazioni

  • Gli articoli di Wikipedia per OWL o SOWPODS potrebbero essere un utile punto di partenza per quanto riguarda gli elenchi di parole.
  • Il programma dovrebbe supportare due modi per selezionare le parole iniziale e finale:
    1. Specificato dall'utente tramite riga di comando, stdin o qualunque cosa sia adatta alla tua lingua preferita (basta menzionare ciò che stai facendo).
    2. Selezione di 2 parole a caso dal file.
  • Le parole iniziale e finale, così come tutte le parole intermedie, dovrebbero avere la stessa lunghezza.
  • Ogni passaggio deve essere stampato sulla sua riga.
  • La riga finale dell'output dovrebbe essere il numero di passaggi intermedi richiesti tra le parole iniziale e finale.
  • Se non è possibile trovare una corrispondenza tra le parole iniziale e finale, l'output dovrebbe essere composto da 3 righe: la parola iniziale, la parola finale e la parola OY.
  • Includi la Big O Notation per la tua soluzione nella tua risposta
  • Includi 10 coppie di parole univoche iniziali e finali (con il loro output, ovviamente) per mostrare i passaggi prodotti dal tuo programma. (Per risparmiare spazio, mentre il tuo programma dovrebbe emetterli su singole righe, puoi consolidarli in un'unica riga per la pubblicazione, sostituendo nuove righe con spazi e una virgola tra ogni esecuzione.

Obiettivi / criteri di vincita

  • Vincerà la soluzione Big O più veloce / migliore che produce i passaggi intermedi più brevi dopo una settimana.
  • Se un pareggio risulta dai criteri Big O, vincerà il codice più corto.
  • Se c'è ancora un pareggio, vincerà la prima soluzione per raggiungere la revisione più rapida e più breve.

Test / Output del campione

DIVE
DIME
DAME
NAME
2

PEACE
PLACE
PLATE
SLATE
2

HOUSE
HORSE
GORSE
GORGE
2

POLE
POSE
POST
PAST
FAST
3

Validazione

Sto lavorando a uno script che può essere utilizzato per convalidare l'output.

Lo farà:

  1. Assicurarsi che ogni parola sia valida.
  2. Assicurarsi che ogni parola sia esattamente 1 lettera diversa dalla parola precedente.

Non sarà:

  1. Verificare che sia stato utilizzato il numero più breve di passaggi.

Una volta scritto, aggiornerò ovviamente questo post. (:


4
Sembra strano a me che l'esecuzione di 3 operazioni per andare da HOUSEa GORGEè segnalato come 2. Mi rendo conto che ci sono 2 parole intermedi, quindi ha senso, ma # delle operazioni sarebbe più intuitivo.
Matteo Leggi

4
@Peter, secondo la pagina wowipedia di sowpods ci sono ~ 15k parole più lunghe di 13 lettere
gnibbler

4
Non voglio essere un sapere tutto, ma il puzzle in realtà ha un nome, è stato inventato da Lewis Carroll en.wikipedia.org/wiki/Word_ladder
st0le

1
Hai un obiettivo indecidibile nella domanda: The fastest/best Big O solution producing the shortest interim steps after one week will win.poiché non puoi garantire che la soluzione più veloce sia nel frattempo quella, che utilizza il minor numero di passaggi, devi fornire una preferenza, se una soluzione utilizza meno passaggi, ma raggiunge l'obiettivo in un secondo momento.
utente sconosciuto

2
Voglio solo confermare BATe CATavrò zero passaggi, giusto?
st0le

Risposte:


9

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


Credo che 8192 sia la dimensione del buffer predefinita per BufferedReader (su SunVM)
st0le

@ st0le, ho omesso quel parametro nella versione golfata, e non danneggia quello non golfato.
Peter Taylor,

5

Giava

Complessità : ?? (Non ho una laurea CompSci, quindi apprezzerei l'aiuto in merito.)

Input : fornire una coppia di parole (più di 1 coppia se lo si desidera) nella riga di comando. Se non viene specificata alcuna riga di comando, vengono scelte 2 parole casuali distinte.

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]);
            }
        }
    }
}

Ho usato Memoization, per risultati più rapidi. Il percorso del dizionario è hardcoded.
st0le

@Joey, lo era una volta ma non più. Ora ha un campo statico che aumenta ogni volta e aggiunge System.nanoTime().
Peter Taylor,

@Joey, aah, ok, ma per ora lo lascio, non voglio aumentare le mie revisioni: P
st0le

oh, a proposito, sono al lavoro e quei siti web scrabble sono apparentemente bloccati, quindi non ho accesso ai dizionari ... genereranno quelle 10 parole uniche meglio entro domani mattina. Saluti!
st0le

È possibile ridurre la complessità (computazionale) eseguendo un bfs bidirezionale, ovvero cercare da entrambi i lati e arrestarsi quando si incontra un nodo visitato dall'altro lato.
Nabb,

3

c su unix

Utilizzando l'algoritmo dijkstra.

Una grande parte del codice è un'implementazione di alberi in costume, che serve a contenere

  • La lista di parole (minimizzando così il numero di volte in cui il file di input viene letto (due volte per nessun argomento, una volta per altri casi) presupponendo che il file IO sia lento
  • Gli alberi parziali mentre li costruiamo.
  • Il percorso finale.

Chiunque sia interessato a vedere come funziona dovrebbe probabilmente letto findPath, processe processOne(ed i loro commenti associati). E forse buildPathebuildPartialPath . Il resto è contabilità e impalcature. Diverse routine utilizzate durante i test e lo sviluppo ma non nella versione "produzione" sono state lasciate al loro posto.

Sto usando /usr/share/dict/wordssul mio Mac OS 10.5 box, che ha così tante voci esoteriche lunghe che lasciarlo funzionare completamente a caso genera un sacco di 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;
}

Alcuni output:

$ ./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

L'analisi della complessità non è banale. La ricerca è un approfondimento iterativo bilaterale.

  • Per ogni nodo esaminato percorro l'intero elenco di parole (sebbene limitato a parole della giusta lunghezza). Chiama la lunghezza dell'elenco W.
  • Il numero minimo di passaggi è S_min = (<number of different letter>-1)perché se siamo separati da una sola lettera calcoliamo la modifica a 0 passaggi intermedi. Il massimo è difficile da quantificare, vedi la TRICKY - SWOOSH sopra. Ogni metà della struttura sarà S/2-1aS/2
  • Non ho fatto un'analisi del comportamento di ramificazione dell'albero, ma lo chiamo B.

Quindi il numero minimo di operazioni è intorno 2 * (S/2)^B * W, non proprio buono.


Forse questo è ingenuo da parte mia, ma non vedo nulla nella tua progettazione o implementazione che richieda pesi ai bordi. Mentre Dijkstra funziona davvero per i grafici non ponderati (il peso del bordo è invariabilmente "1"), una semplice ricerca per ampiezza non si applicherebbe qui per migliorare i tuoi limiti O(|V|+|E|)anziché O(|E|+|V| log |V|)?
Mr Gomez,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.