1P5: Changeur de mots

20

Ceci a été écrit dans le cadre du premier puzzle de programmation Premier périodique .

Le jeu

Un mot de début et de fin de même longueur est fourni. L'objectif du jeu est de changer une lettre dans le mot de départ pour former un mot valide différent, en répétant cette étape jusqu'à ce que le mot de fin soit atteint, en utilisant le moins d'étapes. Par exemple, étant donné les mots TREE et FLED, la sortie serait:

TREE
FREE
FLEE
FLED
2

Caractéristiques

  • Les articles Wikipedia pour le OWL ou les SOWPODS pourraient être un point de départ utile en ce qui concerne les listes de mots.
  • Le programme doit prendre en charge deux manières de sélectionner les mots de début et de fin:
    1. Spécifié par l'utilisateur via la ligne de commande, stdin ou tout ce qui convient à la langue de votre choix (mentionnez simplement ce que vous faites).
    2. Sélection de 2 mots au hasard dans le fichier.
  • Les mots de début et de fin, ainsi que tous les mots intermédiaires doivent avoir la même longueur.
  • Chaque étape doit être imprimée sur sa ligne.
  • La dernière ligne de votre sortie doit être le nombre d'étapes intermédiaires qui ont été nécessaires pour passer entre les mots de début et de fin.
  • Si aucune correspondance ne peut être trouvée entre les mots de début et de fin, la sortie doit être composée de 3 lignes: le mot de début, le mot de fin et le mot OY.
  • Incluez la notation Big O pour votre solution dans votre réponse
  • Veuillez inclure 10 paires de mots de début et de fin uniques (avec leur sortie, bien sûr) pour montrer les étapes produites par votre programme. (Pour économiser de l'espace, alors que votre programme devrait les afficher sur des lignes individuelles, vous pouvez les consolider en une seule ligne pour publication, en remplaçant les nouvelles lignes par des espaces et une virgule entre chaque exécution.

Objectifs / Critères gagnants

  • La solution Big O la plus rapide / la meilleure produisant les étapes intermédiaires les plus courtes après une semaine sera gagnante.
  • Si une égalité résulte des critères Big O, le code le plus court l'emportera.
  • S'il y a toujours égalité, la première solution à atteindre sa révision la plus rapide et la plus courte l'emportera.

Tests / sortie d'échantillon

DIVE
DIME
DAME
NAME
2

PEACE
PLACE
PLATE
SLATE
2

HOUSE
HORSE
GORSE
GORGE
2

POLE
POSE
POST
PAST
FAST
3

Validation

Je travaille sur un script qui peut être utilisé pour valider la sortie.

Ce sera:

  1. Assurez-vous que chaque mot est valide.
  2. Assurez-vous que chaque mot est exactement 1 lettre différente du mot précédent.

Ça ne sera pas:

  1. Vérifiez que le nombre d'étapes le plus court a été utilisé.

Une fois que j'aurai écrit cela, je mettrai bien sûr à jour ce post. (:

Rebecca Chernoff
la source
4
Il me semble étrange que l'exécution de 3 opérations pour aller de HOUSEà GORGEsoit signalée comme 2. Je me rends compte qu'il y a 2 mots intermédiaires, donc cela a du sens, mais le nombre d'opérations serait plus intuitif.
Matthew Read
4
@Peter, selon la page wikipedia de sowpods, il y a ~ 15k mots de plus de 13 lettres
gnibbler
4
Je ne veux pas tout savoir, mais le puzzle a en fait un nom, il a été inventé par Lewis Carroll en.wikipedia.org/wiki/Word_ladder
st0le
1
Vous avez un objectif indécidable dans la question: The fastest/best Big O solution producing the shortest interim steps after one week will win.comme vous ne pouvez pas garantir que la solution la plus rapide est quant à elle celle qui utilise le moins d'étapes, vous devez fournir une préférence, si une solution utilise moins d'étapes, mais atteint l'objectif plus tard.
utilisateur inconnu
2
Je veux juste confirmer BATet CATje n'aurai aucun pas, non?
st0le

Réponses:

9

La longueur étant répertoriée comme critère, voici la version golfée à 1681 caractères (pourrait encore être améliorée de 10%):

import java.io.*;import java.util.*;public class W{public static void main(String[]
a)throws Exception{int n=a.length<1?5:a[0].length(),p,q;String f,t,l;S w=new S();Scanner
s=new Scanner(new
File("sowpods"));while(s.hasNext()){f=s.next();if(f.length()==n)w.add(f);}if(a.length<1){String[]x=w.toArray(new
String[0]);Random
r=new Random();q=x.length;p=r.nextInt(q);q=r.nextInt(q-1);f=x[p];t=x[p>q?q:q+1];}else{f=a[0];t=a[1];}H<S>
A=new H(),B=new H(),C=new H();for(String W:w){A.put(W,new
S());for(p=0;p<n;p++){char[]c=W.toCharArray();c[p]='.';l=new
String(c);A.get(W).add(l);S z=B.get(l);if(z==null)B.put(l,z=new
S());z.add(W);}}for(String W:A.keySet()){C.put(W,w=new S());for(String
L:A.get(W))for(String b:B.get(L))if(b!=W)w.add(b);}N m,o,ñ;H<N> N=new H();N.put(f,m=new
N(f,t));N.put(t,o=new N(t,t));m.k=0;N[]H=new
N[3];H[0]=m;p=H[0].h;while(0<1){if(H[0]==null){if(H[1]==H[2])break;H[0]=H[1];H[1]=H[2];H[2]=null;p++;continue;}if(p>=o.k-1)break;m=H[0];H[0]=m.x();if(H[0]==m)H[0]=null;for(String
v:C.get(m.s)){ñ=N.get(v);if(ñ==null)N.put(v,ñ=new N(v,t));if(m.k+1<ñ.k){if(ñ.k<ñ.I){q=ñ.k+ñ.h-p;N
Ñ=ñ.x();if(H[q]==ñ)H[q]=Ñ==ñ?null:Ñ;}ñ.b=m;ñ.k=m.k+1;q=ñ.k+ñ.h-p;if(H[q]==null)H[q]=ñ;else{ñ.n=H[q];ñ.p=ñ.n.p;ñ.n.p=ñ.p.n=ñ;}}}}if(o.b==null)System.out.println(f+"\n"+t+"\nOY");else{String[]P=new
String[o.k+2];P[o.k+1]=o.k-1+"";m=o;for(q=m.k;q>=0;q--){P[q]=m.s;m=m.b;}for(String
W:P)System.out.println(W);}}}class N{String s;int k,h,I=(1<<30)-1;N b,p,n;N(String S,String
d){s=S;for(k=0;k<d.length();k++)if(d.charAt(k)!=S.charAt(k))h++;k=I;p=n=this;}N
x(){N r=n;n.p=p;p.n=n;n=p=this;return r;}}class S extends HashSet<String>{}class H<V>extends
HashMap<String,V>{}

La version non golfée, qui utilise des noms et des méthodes de package et ne donne pas d'avertissement ou n'étend les classes que pour les alias, est:

package com.akshor.pjt33;

import java.io.*;
import java.util.*;

// WordLadder partially golfed and with reduced dependencies
//
// Variables used in complexity analysis:
// n is the word length
// V is the number of words (vertex count of the graph)
// E is the number of edges
// hash is the cost of a hash insert / lookup - I will assume it's constant, but without completely brushing it under the carpet
public class WordLadder2
{
    private Map<String, Set<String>> wordsToWords = new HashMap<String, Set<String>>();

    // Initialisation cost: O(V * n * (n + hash) + E * hash)
    private WordLadder2(Set<String> words)
    {
        Map<String, Set<String>> wordsToLinks = new HashMap<String, Set<String>>();
        Map<String, Set<String>> linksToWords = new HashMap<String, Set<String>>();

        // Cost: O(Vn * (n + hash))
        for (String word : words)
        {
            // Cost: O(n*(n + hash))
            for (int i = 0; i < word.length(); i++)
            {
                // Cost: O(n + hash)
                char[] ch = word.toCharArray();
                ch[i] = '.';
                String link = new String(ch).intern();
                add(wordsToLinks, word, link);
                add(linksToWords, link, word);
            }
        }

        // Cost: O(V * n * hash + E * hash)
        for (Map.Entry<String, Set<String>> from : wordsToLinks.entrySet()) {
            String src = from.getKey();
            wordsToWords.put(src, new HashSet<String>());
            for (String link : from.getValue()) {
                Set<String> to = linksToWords.get(link);
                for (String snk : to) {
                    // Note: equality test is safe here. Cost is O(hash)
                    if (snk != src) add(wordsToWords, src, snk);
                }
            }
        }
    }

    public static void main(String[] args) throws IOException
    {
        // Cost: O(filelength + num_words * hash)
        Map<Integer, Set<String>> wordsByLength = new HashMap<Integer, Set<String>>();
        BufferedReader br = new BufferedReader(new FileReader("sowpods"), 8192);
        String line;
        while ((line = br.readLine()) != null) add(wordsByLength, line.length(), line);

        if (args.length == 2) {
            String from = args[0].toUpperCase();
            String to = args[1].toUpperCase();
            new WordLadder2(wordsByLength.get(from.length())).findPath(from, to);
        }
        else {
            // 5-letter words are the most interesting.
            String[] _5 = wordsByLength.get(5).toArray(new String[0]);
            Random rnd = new Random();
            int f = rnd.nextInt(_5.length), g = rnd.nextInt(_5.length - 1);
            if (g >= f) g++;
            new WordLadder2(wordsByLength.get(5)).findPath(_5[f], _5[g]);
        }
    }

    // O(E * hash)
    private void findPath(String start, String dest) {
        Node startNode = new Node(start, dest);
        startNode.cost = 0; startNode.backpointer = startNode;

        Node endNode = new Node(dest, dest);

        // Node lookup
        Map<String, Node> nodes = new HashMap<String, Node>();
        nodes.put(start, startNode);
        nodes.put(dest, endNode);

        // Heap
        Node[] heap = new Node[3];
        heap[0] = startNode;
        int base = heap[0].heuristic;

        // O(E * hash)
        while (true) {
            if (heap[0] == null) {
                if (heap[1] == heap[2]) break;
                heap[0] = heap[1]; heap[1] = heap[2]; heap[2] = null; base++;
                continue;
            }

            // If the lowest cost isn't at least 1 less than the current cost for the destination,
            // it can't improve the best path to the destination.
            if (base >= endNode.cost - 1) break;

            // Get the cheapest node from the heap.
            Node v0 = heap[0];
            heap[0] = v0.remove();
            if (heap[0] == v0) heap[0] = null;

            // Relax the edges from v0.
            int g_v0 = v0.cost;
            // O(hash * #neighbours)
            for (String v1Str : wordsToWords.get(v0.key))
            {
                Node v1 = nodes.get(v1Str);
                if (v1 == null) {
                    v1 = new Node(v1Str, dest);
                    nodes.put(v1Str, v1);
                }

                // If it's an improvement, use it.
                if (g_v0 + 1 < v1.cost)
                {
                    // Update the heap.
                    if (v1.cost < Node.INFINITY)
                    {
                        int bucket = v1.cost + v1.heuristic - base;
                        Node t = v1.remove();
                        if (heap[bucket] == v1) heap[bucket] = t == v1 ? null : t;
                    }

                    // Next update the backpointer and the costs map.
                    v1.backpointer = v0;
                    v1.cost = g_v0 + 1;

                    int bucket = v1.cost + v1.heuristic - base;
                    if (heap[bucket] == null) {
                        heap[bucket] = v1;
                    }
                    else {
                        v1.next = heap[bucket];
                        v1.prev = v1.next.prev;
                        v1.next.prev = v1.prev.next = v1;
                    }
                }
            }
        }

        if (endNode.backpointer == null) {
            System.out.println(start);
            System.out.println(dest);
            System.out.println("OY");
        }
        else {
            String[] path = new String[endNode.cost + 1];
            Node t = endNode;
            for (int i = t.cost; i >= 0; i--) {
                path[i] = t.key;
                t = t.backpointer;
            }
            for (String str : path) System.out.println(str);
            System.out.println(path.length - 2);
        }
    }

    private static <K, V> void add(Map<K, Set<V>> map, K key, V value) {
        Set<V> vals = map.get(key);
        if (vals == null) map.put(key, vals = new HashSet<V>());
        vals.add(value);
    }

    private static class Node
    {
        public static int INFINITY = Integer.MAX_VALUE >> 1;

        public String key;
        public int cost;
        public int heuristic;
        public Node backpointer;

        public Node prev = this;
        public Node next = this;

        public Node(String key, String dest) {
            this.key = key;
            cost = INFINITY;
            for (int i = 0; i < dest.length(); i++) if (dest.charAt(i) != key.charAt(i)) heuristic++;
        }

        public Node remove() {
            Node rv = next;
            next.prev = prev;
            prev.next = next;
            next = prev = this;
            return rv;
        }
    }
}

Comme vous pouvez le voir, l'analyse des coûts de fonctionnement est O(filelength + num_words * hash + V * n * (n + hash) + E * hash). Si vous acceptez mon hypothèse qu'une insertion / recherche de table de hachage est à temps constant, c'est O(filelength + V n^2 + E). Les statistiques particulières des graphiques dans SOWPODS signifient que O(V n^2)cela domine vraiment O(E)pour la plupart n.

Exemples de sorties:

IDOLA, IDOLS, IDYLS, ODYLS, ODALS, OVALS, OVELS, FOURS, EVENS, ETENS, STENS, SKENS, SKINS, SPINS, SPINE, 13

WICCA, PROSY, OY

BRINY, BRINS, TRINS, TAINS, TARNS, YARNS, YAWNS, YAWPS, YAPPS, 7

GALES, GAZ, GASTS, GESTS, GESTE, GESSE, DESSE, 5

SURES, DURES, DUNES, DINES, DINGS, DINGY, 4

LICHT, LIGHT, BIGHT, BIGOT, BIGOS, BIROS, GIROS, GIRNS, GURNS, GUANS, GUANA, RUANA, 10

SARGE, SERGE, SERRE, SERRS, SEERS, DEERS, DYERS, OYERS, OVERS, OVELS, OVALS, ODALS, ODYLS, IDYLS, 12

KEIRS, SEIRS, SEERS, BEERS, BRERS, BRERE, BREME, CREME, CREPE, 7

C'est l'une des 6 paires avec le chemin le plus long et le plus court:

GAINEST, FAINEST, FAIREST, SAIREST, SAIDEST, SADDEST, MADDEST, MIDDEST, MILDEST, WILDEST, WILIEST, WALIEST, WANIEST, CANIEST, CANTEST, CONTEST, CONFEST, CONFESS, CONFERS, CONKERS, COOKERS, COOPERS, COPPERS, POPPERS, POPPERS, POPPERS, POPPERS POPPITS, POPPIES, POPSIES, MOPSIES, MOUSIES, MOUSSES, POUSSES, PLUSSES, PLISSES, PRISSES, PRESSES, PREASES, UREASES, UNASES, UNCASES, UNASAS, UNBASED, UNBATED, UNMATED, UNMETED, UNMEWED, ENDEWED, ENDEWED INDEX, INDENES, INDENTS, INCENTS, INCESTS, INFESTS, INFECTS, INJECTS, 56

Et l'une des paires de 8 lettres solubles les plus défavorables:

ENROBING, UNROBING, UNROPING, UNCOPING, UNCAPING, UNCAGING, ENCAGING, ENRAGING, ENRACING, ENLACING, UNLACING, UNLAYING, UPLAYING, SPLAYING, SPRAYING, STRAYING, STOUMING, STOUMING, STOUMING CRIMPING, CRISPING, CRISPINS, CRISPENS, CRISPERS, CRIMPERS, CRAMPERS, CLAMPERS, CLASPERS, CLASHERS, SLASHERS, SLATHERS, SLITHERS, SMITHERS, SMOTHERS, SWOTHERS, SUDERS, MOUTHERS, MOUCHERS, COUCHERS, PACHERS, POACHERS, POACHERS, POACHERS LUNCHERS, LYNCHERS, LYNCHETS, LINCHETS, 52

Maintenant que je pense avoir éliminé toutes les exigences de la question, ma discussion.

Pour un CompSci, la question se réduit évidemment au chemin le plus court dans un graphe G dont les sommets sont des mots et dont les bords relient des mots différents dans une lettre. Générer le graphique efficacement n'est pas anodin - j'ai en fait une idée que je dois revoir pour réduire la complexité à O (V n hash + E). La façon dont je le fais consiste à créer un graphique qui insère des sommets supplémentaires (correspondant aux mots avec un caractère générique) et qui est homéomorphe au graphique en question. J'ai envisagé d'utiliser ce graphique plutôt que de le réduire à G - et je suppose que d'un point de vue golfique j'aurais dû le faire - sur la base qu'un nœud générique avec plus de 3 arêtes réduit le nombre d'arêtes dans le graphique, et le le temps d'exécution standard du pire cas des algorithmes de chemin le plus court est O(V heap-op + E).

Cependant, la première chose que j'ai faite a été d'exécuter des analyses des graphiques G pour différentes longueurs de mots, et j'ai découvert qu'elles sont extrêmement rares pour les mots de 5 lettres ou plus. Le graphique à 5 lettres a 12478 sommets et 40759 arêtes; l'ajout de nœuds de lien aggrave le graphique. Au moment où vous avez jusqu'à 8 lettres, il y a moins de bords que de nœuds, et 3/7 des mots sont "distants". J'ai donc rejeté cette idée d'optimisation car elle n'était pas vraiment utile.

L'idée qui s'est avérée utile était d'examiner le tas. Je peux honnêtement dire que j'ai mis en œuvre des tas modérément exotiques dans le passé, mais aucun aussi exotique que cela. J'utilise une étoile A (puisque C n'apporte aucun avantage compte tenu du tas que j'utilise) avec l'heuristique évidente du nombre de lettres différentes de la cible, et un peu d'analyse montre qu'à tout moment il n'y a pas plus de 3 priorités différentes dans le tas. Lorsque je fais apparaître un nœud dont la priorité est (coût + heuristique) et que je regarde ses voisins, je considère trois cas: 1) le coût du voisin est le coût + 1; l'heuristique du voisin est l'heuristique-1 (car la lettre qu'elle change devient "correcte"); 2) coût + 1 et heuristique + 0 (parce que la lettre qu'il change passe de "faux" à "toujours faux"; 3) coût + 1 et heuristique + 1 (car la lettre qu'il change passe de «correcte» à «fausse»). Donc, si je détends le voisin, je vais l'insérer à la même priorité, priorité + 1 ou priorité + 2. En conséquence, je peux utiliser un tableau à 3 éléments de listes liées pour le tas.

Je devrais ajouter une note sur mon hypothèse selon laquelle les recherches de hachage sont constantes. Très bien, direz-vous, mais qu'en est-il des calculs de hachage? La réponse est que je les amortis: java.lang.Stringmet en cache son hashCode(), donc le temps total passé à calculer les hachages est O(V n^2)(pour générer le graphique).

Il y a un autre changement qui affecte la complexité, mais la question de savoir s'il s'agit d'une optimisation ou non dépend de vos hypothèses sur les statistiques. (IMO mettant "la meilleure solution Big O" comme critère est une erreur car il n'y a pas de meilleure complexité, pour une raison simple: il n'y a pas une seule variable). Cette modification affecte l'étape de génération du graphique. Dans le code ci-dessus, c'est:

        Map<String, Set<String>> wordsToLinks = new HashMap<String, Set<String>>();
        Map<String, Set<String>> linksToWords = new HashMap<String, Set<String>>();

        // Cost: O(Vn * (n + hash))
        for (String word : words)
        {
            // Cost: O(n*(n + hash))
            for (int i = 0; i < word.length(); i++)
            {
                // Cost: O(n + hash)
                char[] ch = word.toCharArray();
                ch[i] = '.';
                String link = new String(ch).intern();
                add(wordsToLinks, word, link);
                add(linksToWords, link, word);
            }
        }

        // Cost: O(V * n * hash + E * hash)
        for (Map.Entry<String, Set<String>> from : wordsToLinks.entrySet()) {
            String src = from.getKey();
            wordsToWords.put(src, new HashSet<String>());
            for (String link : from.getValue()) {
                Set<String> to = linksToWords.get(link);
                for (String snk : to) {
                    // Note: equality test is safe here. Cost is O(hash)
                    if (snk != src) add(wordsToWords, src, snk);
                }
            }
        }

Voilà O(V * n * (n + hash) + E * hash). Mais la O(V * n^2)partie vient de la génération d'une nouvelle chaîne de n caractères pour chaque lien, puis du calcul de son code de hachage. Cela peut être évité avec une classe d'assistance:

    private static class Link
    {
        private String str;
        private int hash;
        private int missingIdx;

        public Link(String str, int hash, int missingIdx) {
            this.str = str;
            this.hash = hash;
            this.missingIdx = missingIdx;
        }

        @Override
        public int hashCode() { return hash; }

        @Override
        public boolean equals(Object obj) {
            Link l = (Link)obj; // Unsafe, but I know the contexts where I'm using this class...
            if (this == l) return true; // Essential
            if (hash != l.hash || missingIdx != l.missingIdx) return false;
            for (int i = 0; i < str.length(); i++) {
                if (i != missingIdx && str.charAt(i) != l.str.charAt(i)) return false;
            }
            return true;
        }
    }

Ensuite, la première moitié de la génération du graphique devient

        Map<String, Set<Link>> wordsToLinks = new HashMap<String, Set<Link>>();
        Map<Link, Set<String>> linksToWords = new HashMap<Link, Set<String>>();

        // Cost: O(V * n * hash)
        for (String word : words)
        {
            // apidoc: The hash code for a String object is computed as
            // s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
            // Cost: O(n * hash)
            int hashCode = word.hashCode();
            int pow = 1;
            for (int j = word.length() - 1; j >= 0; j--) {
                Link link = new Link(word, hashCode - word.charAt(j) * pow, j);
                add(wordsToLinks, word, link);
                add(linksToWords, link, word);
                pow *= 31;
            }
        }

En utilisant la structure du code de hachage, nous pouvons générer les liens dans O(V * n). Cependant, cela a un effet d'entraînement. Inhérent à mon hypothèse que les recherches de hachage sont à temps constant est une hypothèse selon laquelle la comparaison des objets pour l'égalité est bon marché. Cependant, le test d'égalité de Link est O(n)dans le pire des cas. Le pire des cas est lorsque nous avons une collision de hachage entre deux liens égaux générés à partir de mots différents - c'est-à-dire qu'elle se produit O(E)fois dans la seconde moitié de la génération du graphe. En dehors de cela, sauf dans le cas peu probable d'une collision de hachage entre des liens non égaux, nous sommes bons. Nous avons donc échangé O(V * n^2)pour O(E * n * hash). Voir mon point précédent sur les statistiques.

Peter Taylor
la source
Je crois que 8192 est la taille de tampon par défaut pour BufferedReader (sur SunVM)
st0le
@ st0le, j'ai omis ce paramètre dans la version golfée, et cela ne fait pas de mal dans la version non golfée.
Peter Taylor
5

Java

Complexité : ?? (Je n'ai pas de diplôme CompSci, j'apprécierais donc de l'aide à ce sujet.)

Entrée : Fournissez une paire de mots (plus d'une paire si vous le souhaitez) dans la ligne de commande. Si aucune ligne de commande n'est spécifiée, 2 mots aléatoires distincts sont choisis.

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
la source
J'ai utilisé la mémorisation, pour des résultats plus rapides. Le chemin du dictionnaire est codé en dur.
st0le
@Joey, c'était le cas mais plus maintenant. Il a maintenant un champ statique qu'il incrémente à chaque fois et ajoute à System.nanoTime().
Peter Taylor
@ Joey, aah, ok, mais je vais le laisser pour l'instant, je ne veux pas incrémenter mes révisions: P
st0le
oh, btw, je suis au travail et ces sites Web de scrabble sont apparemment bloqués, donc je n'ai pas accès aux dictionnaires ... généreront ces 10 mots uniques mieux demain matin. À votre santé!
st0le
Vous pouvez réduire la complexité (informatique) en effectuant un bfs bidirectionnel, c'est-à-dire rechercher des deux côtés et vous arrêter lorsque vous rencontrez un nœud visité de l'autre côté.
Nabb
3

c sur unix

Utilisation de l'algorithme dijkstra.

Une grande partie du code est une implémentation d'arbre n-aire de costume, qui sert à maintenir

  • La liste de mots (minimisant ainsi le nombre de fois que le fichier d'entrée est lu (deux fois pour aucun argument, une fois pour les autres cas) en supposant que le fichier IO est lent
  • Les arbres partiels pendant que nous les construisons.
  • Le dernier chemin.

Toute personne intéressée à voir comment il fonctionne devrait probablement lire findPath, processet processOne(et leurs commentaires associés). Et peut buildPath- être et buildPartialPath. Le reste est la comptabilité et les échafaudages. Plusieurs routines utilisées lors des tests et du développement mais pas dans la version "production" ont été laissées en place.

j'utilise /usr/share/dict/words sur ma boîte Mac OS 10.5, qui a tellement d'entrées ésotériques longues que le laisser fonctionner complètement au hasard génère beaucoup de 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;
}

Quelques sorties:

$ ./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'analyse de complexité n'est pas anodine. La recherche est un approfondissement itératif bilatéral.

  • Pour chaque nœud examiné, je parcourt la liste de mots entière (bien que limitée aux mots de la bonne longueur). Appelez la longueur de la liste W.
  • Le nombre minimum d'étapes est S_min = (<number of different letter>-1) dû au que si nous ne sommes séparés que par une lettre, nous notons le changement à 0 pas intermédiaire. Le maximum est difficile à quantifier voir la course TRICKY - SWOOSH ci-dessus. Chaque moitié de l'arbre sera S/2-1àS/2
  • Je n'ai pas fait d'analyse du comportement de branchement de l'arbre, mais appelez-le B .

Donc, le nombre minimum d'opérations est d'environ 2 * (S/2)^B * W, pas vraiment bon.

dmckee
la source
C'est peut-être naïf de ma part, mais je ne vois rien dans votre conception ou implémentation qui nécessite des poids de bordure. Alors que Dijkstra fonctionne en effet pour les graphiques non pondérés (le poids des bords est invariablement "1"), une simple recherche en largeur ne s'appliquerait-elle pas ici pour améliorer vos limites au O(|V|+|E|)lieu de O(|E|+|V| log |V|)?
MrGomez