1P5: Trocador de Palavras

20

Isso foi escrito como parte do primeiro envio periódico de quebra-cabeças de programação Premier .

O jogo

Uma palavra inicial e final do mesmo tamanho é fornecida. O objetivo do jogo é alterar uma letra na palavra inicial para formar uma palavra válida diferente, repetindo esta etapa até que a palavra final seja alcançada, usando o menor número possível de etapas. Por exemplo, dadas as palavras TREE e FLED, a saída seria:

TREE
FREE
FLEE
FLED
2

Especificações

  • Os artigos da Wikipedia para OWL ou SOWPODS podem ser um ponto de partida útil na lista de palavras.
  • O programa deve oferecer suporte a duas maneiras de selecionar as palavras inicial e final:
    1. Especificado pelo usuário via linha de comando, stdin ou qualquer outro que seja adequado ao seu idioma de escolha (apenas mencione o que você está fazendo).
    2. Selecionando 2 palavras aleatoriamente no arquivo.
  • As palavras inicial e final, assim como todas as palavras intermediárias, devem ter o mesmo comprimento.
  • Cada etapa deve ser impressa em sua linha.
  • A linha final da sua saída deve ser o número de etapas intermediárias necessárias para obter entre as palavras inicial e final.
  • Se não for possível encontrar uma correspondência entre as palavras inicial e final, a saída deve consistir em 3 linhas: a palavra inicial, a palavra final e a palavra OY.
  • Inclua a notação Big O para sua solução em sua resposta
  • Inclua 10 pares de palavras iniciais e finais exclusivas (com suas saídas, é claro) para mostrar as etapas que seu programa produz. (Para economizar espaço, enquanto o programa deve produzi-los em linhas individuais, você pode consolidá-los em uma única linha para postagem, substituindo novas linhas por espaços e vírgula entre cada execução.

Objetivos / Critérios de vitória

  • A solução Big O mais rápida / melhor, que produzir as etapas intermediárias mais curtas após uma semana, vencerá.
  • Se um empate resultar do critério Big O, o código mais curto vencerá.
  • Se ainda houver um empate, a primeira solução a atingir sua revisão mais rápida e mais rápida vencerá.

Testes / Saída de Amostra

DIVE
DIME
DAME
NAME
2

PEACE
PLACE
PLATE
SLATE
2

HOUSE
HORSE
GORSE
GORGE
2

POLE
POSE
POST
PAST
FAST
3

Validação

Estou trabalhando em um script que pode ser usado para validar a saída.

Será:

  1. Verifique se cada palavra é válida.
  2. Certifique-se de que cada palavra seja exatamente 1 letra diferente da palavra anterior.

Não vai:

  1. Verifique se o menor número de etapas foi usado.

Depois que eu tiver escrito isso, é claro que atualizarei este post. (:

Rebecca Chernoff
fonte
4
Parece-me estranho que a execução de 3 operações para passar de HOUSEpara GORGEseja relatada como 2. Sei que existem 2 palavras intermediárias, então faz sentido, mas o número de operações seria mais intuitivo.
Matthew Leia
4
@ Pedro, de acordo com SOWPODS wikipedia página há ~ 15k palavras com mais de 13 cartas
gnibbler
4
Eu não quero ser um sabe-tudo, mas o quebra-cabeça, na verdade, tem um nome, ele foi inventado por Lewis Carroll en.wikipedia.org/wiki/Word_ladder
st0le
1
Você tem um objetivo indecidível na pergunta: The fastest/best Big O solution producing the shortest interim steps after one week will win.como você não pode garantir que a solução mais rápida seja a que utiliza menos etapas, você deve fornecer uma preferência, se uma solução usar menos etapas, mas atingir a meta mais tarde.
usuário desconhecido
2
Eu só quero confirmar BATe CATterei zero etapas, certo?
st0le

Respostas:

9

Como o comprimento está listado como critério, eis a versão em golf com 1681 caracteres (provavelmente ainda pode ser melhorada em 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>{}

A versão ungolfed, que usa nomes e métodos de pacotes e não dá avisos ou estende classes apenas para aliasá-los, é:

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 você pode ver, a análise dos custos de execução é O(filelength + num_words * hash + V * n * (n + hash) + E * hash). Se você aceitar minha suposição de que uma inserção / pesquisa de tabela de hash é tempo constante, é isso O(filelength + V n^2 + E). As estatísticas particulares dos gráficos em SOWPODS significam que O(V n^2)realmente domina O(E)a maiorian .

Saídas de amostra:

IDOLA, IDOLS, IDYLS, ODYLS, ODALS, OVALS, OVELS, FORNOS, EVENS, ETENS, STENS, SKENS, PELE, SPINS, SPINE, SPINE, 13

WICCA, PROSY, OY

BRINY, BRINCES, TRINS, MOEDAS, CHARNES, Fios, Bocejos, YAWPS, YAPPS, 7

GALES, GASES, GASTOS, GESTS, GESTE, GESSE, DESSE, 5

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

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

SARGE, SERGE, SERRE, SERRS, SEERS, veados, tintureiros, OYERS, OVERS, OVELS, OVALS, ODALS, ODYLS, IDYLS, 12

CHAVES, PRIMEIROS SOCORROS, CERVEJAS, CERVEJAS, BRERE, BREME, CREME, CREPE, 7

Este é um dos 6 pares com o caminho mais curto e mais longo:

GAINEST, FAINEST, FAIREST, SAIREST, SAIDEST, SADDEST, MADDEST, MIDDEST, MILDEST, WILDEST, WILIEST, WALIEST, WANIEST, CANIEST, CANTEST, CONCURSO, CONFEST, CONFESS, CONFERS, CONKERS, COOKERS, COOPERS, COPPERS, POPPERS, POPPERS PAPÉIS, PAPOILAS, POPSIES, MOPSIES, MOUSIES, MOUSSES, POUSSES, PLUSSES, PLISSES, PRISSES, PRESSES, PREASES, UREASES, UREASES, UNEASES, UNCASES, UNCASED, UNBASED, UNBATED, UNMATED, UNMETED, UNMEWED, ENDEWED, ENDEWED ÍNDICES, INDENES, RECENTES, INCENTES, INFESTS, INFESTS, INFECTES, INJETOS, 56

E um dos pares de 8 letras solúveis no pior dos casos:

ENROBANDO, DESENVOLVENDO, DESENVOLVENDO, DESENVOLVENDO, DESENVOLVENDO, DESENVOLVENDO, ENCAGINDO, ENRAGANDO, ENRACANDO, ENLACANDO, DESENVOLVENDO, INCLINANDO, ATRAVÉS, SPLAYING, SPRAYING, STRAYING, STROYING, STROKING, STOUM, STOUM CRIANÇAS, CRISPIS, CRISPINOS, CRISPENS, CRISPERS, CRIMPERS, CRAMPERS, CLAMPERS, CLASPERS, CLASHERS, SLASHERS, SLATHERS, SLITHERS, SMITHERS, SMOTHERS, SOOTHERS, SOUTHERS, MOUTHERS, MOUCHERS, SOUTCHERS, PACHECERS, PACHECERS, PACHECERS, PENCHERS, PACHECERS ALMOÇOS, LÍQUIDOS, LÍQUIDOS, LINCHETS, 52

Agora que acho que tenho todos os requisitos da questão fora do caminho, minha discussão.

Para um CompSci, a pergunta obviamente se reduz ao menor caminho em um gráfico G, cujos vértices são palavras e cujas bordas conectam palavras diferentes em uma letra. Gerar o gráfico com eficiência não é trivial - na verdade, tenho uma ideia que preciso revisitar para reduzir a complexidade a O (V n hash + E). A maneira como faço isso envolve a criação de um gráfico que insere vértices extras (correspondentes a palavras com um caractere curinga) e é homeomórfico para o gráfico em questão. Eu considerei usar esse gráfico em vez de reduzir para G - e suponho que, do ponto de vista do golfe, eu deveria ter feito - com base em que um nó curinga com mais de 3 arestas reduz o número de arestas no gráfico e o o pior caso padrão de tempo de execução dos algoritmos de caminho mais curto é O(V heap-op + E).

No entanto, a primeira coisa que fiz foi executar algumas análises dos gráficos G para comprimentos de palavras diferentes, e descobri que eles são extremamente escassos para palavras de 5 ou mais letras. O gráfico de 5 letras possui 12478 vértices e 40759 arestas; adicionar nós de link torna o gráfico pior. No momento em que você tem até 8 letras, há menos bordas que nós e 3/7 das palavras estão "distantes". Rejeitei essa ideia de otimização por não ser realmente útil.

A idéia que se mostrou útil foi examinar a pilha. Posso dizer honestamente que implementei alguns montes moderadamente exóticos no passado, mas nenhum tão exótico quanto esse. Uso a estrela A (já que C não oferece nenhum benefício, dada a pilha que estou usando) com a heurística óbvia do número de letras diferentes do alvo, e um pouco de análise mostra que a qualquer momento não há mais do que três prioridades diferentes na pilha. Quando ponho um nó cuja prioridade é (custo + heurística) e olho para seus vizinhos, há três casos que estou considerando: 1) o custo do vizinho é custo + 1; a heurística do vizinho é heurística-1 (porque a letra que muda se torna "correta"); 2) custo + 1 e heurística + 0 (porque a letra que muda muda de "errado" para "ainda errado"; 3) custo + 1 e heurística + 1 (porque a letra que muda passa de "correta" para "errada"). Então, se eu relaxar o vizinho, vou inseri-lo na mesma prioridade, prioridade + 1 ou prioridade + 2. Como resultado, posso usar uma matriz de 3 elementos de listas vinculadas para o heap.

Devo adicionar uma observação sobre minha suposição de que as pesquisas de hash são constantes. Muito bem, você pode dizer, mas e os cálculos de hash? A resposta é que eu os estou amortizando: java.lang.Stringarmazena em cache o seu hashCode(), de modo que o tempo total gasto na computação de hashes é O(V n^2)(na geração do gráfico).

Há outra mudança que afeta a complexidade, mas a questão de saber se é uma otimização ou não depende de suas suposições sobre estatísticas. (A IMO colocar "a melhor solução Big O" como critério é um erro, porque não há uma melhor complexidade, por um motivo simples: não há uma única variável). Essa alteração afeta a etapa de geração do gráfico. No código acima, é:

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

É isso O(V * n * (n + hash) + E * hash). Mas a O(V * n^2)parte vem da geração de uma nova seqüência de caracteres de n caracteres para cada link e da computação do seu código de hash. Isso pode ser evitado com uma classe 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;
        }
    }

Então a primeira metade da geração do gráfico se torna

        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 a estrutura do código hash, podemos gerar os links O(V * n). No entanto, isso tem um efeito indireto. Inerente à minha suposição de que as pesquisas de hash são tempo constante, é uma suposição que comparar objetos para igualdade seja barato. No entanto, o teste de igualdade do Link está O(n)no pior caso. O pior caso é quando temos uma colisão de hash entre dois links iguais gerados a partir de palavras diferentes - isto é, ocorre O(E)vezes na segunda metade da geração do gráfico. Fora isso, exceto no improvável evento de colisão de hash entre links não iguais, estamos bem. Então, nós trocamos O(V * n^2)por O(E * n * hash). Veja meu ponto anterior sobre estatísticas.

Peter Taylor
fonte
Eu acredito que 8192 é o tamanho do buffer padrão para BufferedReader (em SunVM)
st0le
@ st0le, omiti esse parâmetro na versão golfed, e não prejudica na ungolfed.
Peter Taylor
5

Java

Complexidade : ?? (Eu não tenho um diploma CompSci, por isso gostaria de receber ajuda nesse assunto.)

Entrada : Forneça Par de palavras (mais de 1 par, se desejar) na linha de comando. Se nenhuma linha de comando for especificada, serão escolhidas 2 palavras aleatórias 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
fonte
Eu usei Memoization, para resultados mais rápidos. O caminho do dicionário é codificado.
st0le
@ Joey, costumava ser, mas não mais. Agora ele tem um campo estático que é incrementado a cada vez e adicionado System.nanoTime().
Peter Taylor
@Joey, aah, ok, mas vou deixar isso por agora, não querem incrementar minhas revisões: P
st0le
oh, aliás, estou no trabalho e esses sites de scrabble estão aparentemente bloqueados, então não tenho acesso aos dicionários ... gerará essas 10 palavras únicas melhor amanhã de manhã. Felicidades!
st0le
Você pode reduzir a complexidade (computacional) fazendo um bfs bidirecional, ou seja, procure pelos dois lados e pare quando encontrar um nó visitado do outro lado.
Nabb 6/05
3

c no unix

Usando o algoritmo dijkstra.

Uma grande fração do código é uma implementação de árvore n-ária, que serve para manter

  • A lista de palavras (minimizando, assim, o número de vezes que o arquivo de entrada é lido (duas vezes sem argumentos, uma vez para outros casos)) pressupondo que a E / S do arquivo seja lenta
  • As árvores parciais à medida que as construímos.
  • O caminho final.

Quem estiver interessado em ver como ele funciona provavelmente deve ler findPath, processe processOne(e seus comentários associados). E talvez buildPathebuildPartialPath . O resto é contabilidade e andaimes. Várias rotinas usadas durante o teste e desenvolvimento, mas não na versão "produção", foram mantidas.

Eu estou usando /usr/share/dict/wordsem minha caixa de Mac OS 10.5, que tem tantas entradas longas, esotéricos que deixá-lo correr completamente aleatoriamente gera um monte 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;
}

Alguma saída:

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

A análise de complexidade não é trivial. A pesquisa é um aprofundamento iterativo de dois lados.

  • Para cada nó examinado, ando em toda a lista de palavras (embora limitada a palavras do tamanho certo). Ligue para o comprimento da listaW .
  • O número mínimo de etapas é S_min = (<number of different letter>-1)porque, se tivermos apenas uma letra, pontuamos a alteração em 0 etapas intermediárias. É difícil quantificar o máximo, veja a execução TRICKY-SWOOSH acima. Cada metade da árvore será S/2-1aS/2
  • Eu não fiz uma análise do comportamento de ramificação da árvore, mas chame-o B.

Portanto, o número mínimo de operações está próximo 2 * (S/2)^B * W, não muito bom.

dmckee
fonte
Talvez isso seja ingênuo da minha parte, mas não vejo nada em seu design ou implementação que exija pesos de borda. Embora o Dijkstra realmente funcione para gráficos não ponderados (o peso da borda é invariavelmente "1"), uma simples pesquisa de largura não se aplica aqui para melhorar seus limites em O(|V|+|E|)vez de O(|E|+|V| log |V|)?
MrGomez