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.String
armazena 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.
HOUSE
paraGORGE
seja relatada como 2. Sei que existem 2 palavras intermediárias, então faz sentido, mas o número de operações seria mais intuitivo.