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.String
almacena 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.
HOUSE
aGORGE
2. 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.