1P5: Cambiador de palabras

20

Esto fue escrito como parte del primer empuje de programación periódica Premier Puzzle .

El juego

Se proporcionan una palabra inicial y final de la misma longitud. El objetivo del juego es cambiar una letra en la palabra inicial para formar una palabra válida diferente, repitiendo este paso hasta llegar a la palabra final, utilizando la menor cantidad de pasos. Por ejemplo, dadas las palabras TREE y FLED, la salida sería:

TREE
FREE
FLEE
FLED
2

Especificaciones

  • Los artículos de Wikipedia para OWL o SOWPODS podrían ser un punto de partida útil en lo que respecta a las listas de palabras.
  • El programa debe admitir dos formas de seleccionar las palabras de inicio y fin:
    1. Especificado por el usuario a través de la línea de comandos, stdin o lo que sea adecuado para su idioma de elección (solo mencione lo que está haciendo).
    2. Seleccionando 2 palabras al azar del archivo.
  • Las palabras iniciales y finales, así como todas las palabras provisionales, deben tener la misma longitud.
  • Cada paso debe imprimirse en su línea.
  • La línea final de su salida debe ser el número de pasos intermedios que se requieren para pasar entre las palabras iniciales y finales.
  • Si no se puede encontrar una coincidencia entre las palabras iniciales y finales, la salida debe constar de 3 líneas: la palabra inicial, la palabra final y la palabra OY.
  • Incluya la notación Big O para su solución en su respuesta
  • Incluya 10 pares únicos de palabras iniciales y finales (con su salida, por supuesto) para mostrar los pasos que produce su programa. (Para ahorrar espacio, aunque su programa debería generarlos en líneas individuales, puede consolidarlos en una sola línea para publicar, reemplazando nuevas líneas con espacios y una coma entre cada ejecución.

Objetivos / Criterios ganadores

  • Ganará la solución Big O más rápida / mejor que produzca los pasos intermedios más cortos después de una semana.
  • Si un empate resulta del criterio Big O, el código más corto ganará.
  • Si todavía hay un empate, ganará la primera solución para alcanzar su revisión más rápida y más corta.

Pruebas / salida de muestra

DIVE
DIME
DAME
NAME
2

PEACE
PLACE
PLATE
SLATE
2

HOUSE
HORSE
GORSE
GORGE
2

POLE
POSE
POST
PAST
FAST
3

Validación

Estoy trabajando en un script que se puede usar para validar la salida.

Va a:

  1. Asegúrese de que cada palabra sea válida.
  2. Asegúrese de que cada palabra sea exactamente 1 letra diferente de la palabra anterior.

No lo hará:

  1. Compruebe que se utilizó el menor número de pasos.

Una vez que haya escrito eso, por supuesto actualizaré esta publicación. (:

Rebecca Chernoff
fuente
44
Me parece extraño que se realicen 3 operaciones para llegar HOUSEa GORGE2. Se da cuenta de que hay 2 palabras intermedias, por lo que tiene sentido, pero el número de operaciones sería más intuitivo.
Matthew leyó
44
@Peter, según la página de Wikipedia de sowpods, hay ~ 15k palabras más largas que 13 letras
gnibbler
44
No quiero ser un sabelotodo, pero el acertijo en realidad tiene un Nombre, fue inventado por Lewis Carroll en.wikipedia.org/wiki/Word_ladder
st0le
1
Tiene un objetivo indecidible en la pregunta: The fastest/best Big O solution producing the shortest interim steps after one week will win.dado que no puede garantizar que la solución más rápida es la que usa la menor cantidad de pasos, debe proporcionar una preferencia, si una solución usa menos pasos, pero alcanza el objetivo más adelante.
usuario desconocido
2
Solo quiero confirmar BATy CATtendré cero pasos, ¿verdad?
st0le

Respuestas:

9

Dado que la longitud figura como criterio, aquí está la versión de golf con 1681 caracteres (probablemente todavía podría mejorarse en un 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 versión no protegida, que usa nombres y métodos de paquetes y no da advertencias ni extiende clases solo para crear un alias, es:

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

Como puede ver, el análisis de costos de funcionamiento es O(filelength + num_words * hash + V * n * (n + hash) + E * hash). Si acepta mi suposición de que una inserción / búsqueda de tabla hash es tiempo constante, eso es O(filelength + V n^2 + E). Las estadísticas particulares de los gráficos en SOWPODS significan que O(V n^2)realmente domina O(E)para la mayoría n.

Resultados de muestra:

IDOLA, IDOLS, IDYLS, ODYLS, ODALS, OVALS, OVELS, HORNOS, 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, 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, CERVEZAS, BRERS, BRERE, BREME, CREME, CREPE, 7

Este es uno de los 6 pares con el camino más corto más largo:

MÁS GANADOR, MÁS FÁCIL, MÁS FÁCIL, MÁS SALUDABLE, MÁS SANO, MÁS SENCILLO, MÁS MADRE, MÁS MEDIO, MÁS SILVESTRE, MÁS SILVESTRE, MÁS SILVESTRE, MÁS SALUDABLE, MÁS SALUDABLE, MÁS CANTIDAD, CANTANTE, CONCURSO, CONFESIÓN, CONFESIÓN, CONFERENCIAS, CONQUISTAS, COCINAS, COOPEROS, COPPERS, COPPERS POPPITS, POPPIES, POPSIES, MOPSIES, MOUSIES, MOUSSES, POUSSES, PLUSSES, PLISSES, PRISSES, PRESSES, PREASES, UREASES, UNEASES, UNCASES, UNCASED, UNBASED, UNBATED, UNMATED, UNMETED, UNMEWED, INMEWED, INDEME ÍNDICES, INDENAS, INDENTES, INCIDENTES, INGRESOS, INFESIONES, INFECTOS, INYECTOS, 56

Y uno de los pares de 8 letras solubles en el peor de los casos:

ENROBING, UNROBING, UNROPING, UNCOPING, UNCAGING, UNCAGING, ENCAGING, ENRAGING, ENRACING, ENLACING, UNLACING, UNLAYING, UPLAYING, SPLAYING, SPRAYING, STRAYING, STROYING, STROKING, STOKING, STUMPING, STUMPING, STUMPING, STUMPING, STUMPING engaste, crujiente, Crispins, Crispens, CAJONES, arrugadores, CRAMPERS, abrazaderas, claspers, clashers, Slashers, slathers, se desliza, Smithers, Smothers, chupetes, Southers, MOUTHERS, MOUCHERS, couchers, monitores de, los cazadores furtivos, POTCHERS, PUTCHERS, pegadores, ALMUERZOS, LYNCHERS, LYNCHETS, LINCHETS, 52

Ahora que creo que tengo todos los requisitos de la pregunta fuera del camino, mi discusión.

Para un CompSci, la pregunta obviamente se reduce al camino más corto en un gráfico G cuyos vértices son palabras y cuyos bordes conectan palabras que difieren en una letra. Generar el gráfico de manera eficiente no es trivial: de hecho, tengo una idea que necesito revisar para reducir la complejidad a O (V n hash + E). La forma en que lo hago implica crear un gráfico que inserta vértices adicionales (correspondientes a palabras con un carácter comodín) y es homeomorfo al gráfico en cuestión. Pensé en usar ese gráfico en lugar de reducirlo a G, y supongo que desde el punto de vista del golf debería haberlo hecho, sobre la base de que un nodo comodín con más de 3 bordes reduce el número de bordes en el gráfico, y el el peor tiempo de ejecución estándar de los algoritmos de ruta más corta esO(V heap-op + E) .

Sin embargo, lo primero que hice fue ejecutar algunos análisis de los gráficos G para diferentes longitudes de palabras, y descubrí que son extremadamente escasos para palabras de 5 o más letras. El gráfico de 5 letras tiene 12478 vértices y 40759 aristas; agregar nodos de enlace empeora el gráfico. Para cuando tenga hasta 8 letras, hay menos aristas que nodos, y 3/7 de las palabras son "distantes". Así que rechacé esa idea de optimización por no ser realmente útil.

La idea que resultó útil fue examinar el montón. Puedo decir honestamente que he implementado algunos montones moderadamente exóticos en el pasado, pero ninguno tan exótico como este. Uso A-star (ya que C no proporciona ningún beneficio dado el montón que estoy usando) con la heurística obvia de la cantidad de letras diferentes del objetivo, y un poco de análisis muestra que en cualquier momento no hay más de 3 prioridades diferentes en el montón Cuando hago estallar un nodo cuya prioridad es (costo + heurístico) y miro a sus vecinos, hay tres casos que estoy considerando: 1) el costo del vecino es el costo + 1; la heurística del vecino es heurística-1 (porque la letra que cambia se vuelve "correcta"); 2) costo + 1 y heurística + 0 (porque la letra que cambia va de "incorrecta" a "aún incorrecta"; 3) costo + 1 y heurística + 1 (porque la letra que cambia va de "correcta" a "incorrecta"). Entonces, si relajo al vecino, lo insertaré con la misma prioridad, prioridad + 1 o prioridad + 2. Como resultado, puedo usar una matriz de 3 elementos de listas vinculadas para el montón.

Debo agregar una nota sobre mi suposición de que las búsquedas de hash son constantes. Muy bien, se puede decir, pero ¿qué pasa con los cálculos hash? La respuesta es que los estoy amortizando: java.lang.Stringalmacena en caché hashCode(), por lo que el tiempo total dedicado a calcular hashes esO(V n^2) (al generar el gráfico).

Hay otro cambio que afecta la complejidad, pero la cuestión de si es una optimización o no depende de sus suposiciones sobre las estadísticas. (OMI, poner "la mejor solución Big O" como criterio es un error porque no hay una mejor complejidad, por una simple razón: no hay una sola variable). Este cambio afecta el paso de generación del gráfico. En el código anterior, 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);
                }
            }
        }

Eso es O(V * n * (n + hash) + E * hash). Pero la O(V * n^2)parte proviene de generar una nueva cadena de n caracteres para cada enlace y luego calcular su código hash. Esto se puede evitar con una clase auxiliar:

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

Entonces la primera mitad de la generación del gráfico se convierte en

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

Al usar la estructura del hashcode podemos generar los enlaces en O(V * n). Sin embargo, esto tiene un efecto knock-on. Inherente a mi suposición de que las búsquedas de hash son de tiempo constante es una suposición de que comparar objetos por igualdad es barato. Sin embargo, la prueba de igualdad de Link está O(n)en el peor de los casos. El peor de los casos es cuando tenemos una colisión hash entre dos enlaces iguales generados a partir de palabras diferentes, es decir, ocurre O(E)veces en la segunda mitad de la generación del gráfico. Aparte de eso, excepto en el improbable caso de una colisión hash entre enlaces no iguales, estamos bien. Así que hemos cambiado O(V * n^2)por O(E * n * hash). Vea mi punto anterior sobre estadísticas.

Peter Taylor
fuente
Creo que 8192 es el tamaño de búfer predeterminado para BufferedReader (en SunVM)
st0le
@ st0le, omití ese parámetro en la versión de golf, y no daña en la versión sin golf.
Peter Taylor
5

Java

Complejidad : ?? (No tengo un título de CompSci, por lo que agradecería ayuda en este asunto).

Entrada : proporcione un par de palabras (más de 1 par si lo desea) en la línea de comando. Si no se especifica una línea de comando, se eligen 2 palabras aleatorias distintas.

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
fuente
He usado Memoization para obtener resultados más rápidos. La ruta del diccionario está codificada.
st0le
@Joey, solía ser pero ya no. Ahora tiene un campo estático que incrementa cada vez y agrega System.nanoTime().
Peter Taylor
@Joey, aah, está bien, pero lo dejaré por ahora, no quiero incrementar mis revisiones: P
stol
oh, por cierto, estoy en el trabajo y esos sitios web de scrabble aparentemente están bloqueados, así que no tengo acceso a los diccionarios ... generaré esas 10 palabras únicas mejor para mañana por la mañana. ¡Salud!
st0le
Podría reducir la complejidad (computacional) haciendo un bfs bidireccional, es decir, buscar desde ambos lados y detenerse cuando encuentre un nodo visitado desde el otro lado.
Nabb
3

c en unix

Usando el algoritmo dijkstra.

Una gran fracción del código es una implementación de árbol de disfraz n-ary, que sirve para mantener

  • La lista de palabras (minimizando así el número de veces que se lee el archivo de entrada (dos veces sin argumentos, una vez para otros casos) bajo el supuesto de que el archivo IO es lento
  • Los árboles parciales a medida que los construimos.
  • El camino final.

Cualquiera que esté interesado en ver cómo funciona probablemente debería leer findPath, processy processOne(y sus comentarios asociados). Y tal vez buildPathybuildPartialPath . El resto es contabilidad y andamios. Varias rutinas utilizadas durante las pruebas y el desarrollo, pero no en la versión de "producción" se han dejado en su lugar.

Estoy usando /usr/share/dict/wordsen mi caja de Mac OS 10.5, que tiene tantas entradas, esotéricos largas que dejar que siga completamente al azar genera una gran cantidad 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;
}

Alguna salida:

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

El análisis de complejidad no es trivial. La búsqueda es una profundización iterativa de dos lados.

  • Para cada nodo examinado, recorro la lista de palabras completa (aunque limitada a palabras de la longitud correcta). Llame a la longitud de la lista W.
  • El número mínimo de pasos se S_min = (<number of different letter>-1)debe a que si solo estamos separados por una letra, calificamos el cambio en 0 pasos intermedios. El máximo es difícil de cuantificar ver el TRICKY - SWOOSH ejecutado arriba. Cada mitad del árbol será S/2-1paraS/2
  • No he hecho un análisis del comportamiento de ramificación del árbol, pero lo llamo B.

Entonces, el número mínimo de operaciones es de alrededor 2 * (S/2)^B * W, no es realmente bueno.

dmckee
fuente
Tal vez esto es ingenuo de mi parte, pero no veo nada en su diseño o implementación que requiera pesos de borde. Si bien Dijkstra realmente funciona para gráficos no ponderados (el peso del borde es invariablemente "1"), ¿no se aplicaría aquí una simple búsqueda de amplitud para mejorar sus límites en O(|V|+|E|)lugar de O(|E|+|V| log |V|)?
MrGomez