Поскольку длина указана в качестве критерия, вот версия для гольфа с 1681 символом (вероятно, все еще может быть улучшена на 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>{}
Версия ungolfed, которая использует имена пакетов и методы и не дает предупреждений или расширяет классы только для их псевдонимов, такова:
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;
}
}
}
Как видите, анализ текущих затрат O(filelength + num_words * hash + V * n * (n + hash) + E * hash)
. Если вы примете мое предположение, что вставка / поиск в хэш-таблице является постоянным временем, это так O(filelength + V n^2 + E)
. Конкретная статистика графиков в SOWPODS означает, что это O(V n^2)
действительно доминирует O(E)
для большинства n
.
Пример выходов:
ИДОЛА, ИДОЛЫ, ИДИЛЫ, ОДИЛЫ, ОДАЛЫ, ОВАЛЫ, СУМКИ, ДУХОВКИ, ДАЖЕ, ЭТЕНС, СТЕНЫ, КОЖИ, КОЖИ, СПИНЫ, СПИНЬ, 13
WICCA, PROSY, OY
BRINY, BRINS, TRINS, TAINS, TARNS, YARNS, YAWNS, YAWPS, YAPPS, 7
ГАЛЕЙ, ГАЗЫ, ГАСТЫ, ГАСТЫ, GESTE, GESSE, DESSE, 5
SURES, DURES, DUNES, DINES, DINGS, DINGY, 4
СВЕТ, СВЕТ, СВЕТ, БОЛЬШОЙ, БИГОС, БИРОС, ГИРОС, ДЕВУШКИ, ГУРНЫ, ГУАНЫ, ГУАНА, РУАНА, 10
SARGE, SERGE, SERRE, SERRS, SEERS, DEERS, DYERS, OYERS, OVERS, OVELS, овалы, ODALS, ODYLS, IDYLS, 12
КИРСЫ, SEIRS, SEERS, ПИВО, BRERS, BRERE, BREME, КРЕМ, КРЕП, 7
Это одна из 6 пар с самым длинным кратчайшим путем:
GAINEST, FAINEST, FAIREST, SAIREST, SAIDEST, SADDEST, MADDEST, MIDDEST, MILDEST, WILDEST, WILIEST, WILIEST, WIIEST, CANIEST, CANTEST, КОНКУРС, КОНФЕСС, КОНФЕРСЫ, КОНКЕРЫ, КУХНИ, КУПЕРЫ, КОТЛЫ, КОПЫ, POPPITS, маки, POPSIES, MOPSIES, MOUSIES, муссы, POUSSES, плюсы, PLISSES, PRISSES, прессы, PREASES, уреазы, UNEASES, UNCASES, бескорпусный, UNBASED, UNBATED, Разъединенный, UNMETED, UNMEWED, ENMEWED, ENDEWED, INDEWED, индексированный, УКАЗАТЕЛИ, УКАЗАНИЯ, УКАЗАНИЯ, ОТНОШЕНИЯ, ИНВЕСТЫ, ИНФЕСТЫ, ИНФЕКЦИИ, ВСТАВКИ, 56
И одна из разрешимых 8-буквенных пар в худшем случае:
ENROBING, UNROBING, UNROBING, UNCOPING, UNCINGING, UNCAGING, ENCAGING, ENRACING, ENLACING, UNLACING, UNLAY, UPLING, SPLAY, SPRAY, STRAYING, STROYING, STROBING, STINGINGING, STINGPING, STINGPING, STINGPING, STINGINGING Обжимные, хрустящие, хрустящие, хрустящие, хрустящие, скребки, скребки, скреперы, скреперы, скребки, скребки, молния, молния, мятеж, мятеж, мятежник, мятежник, мятежник, мятежник ЛАНЧЕРЫ, ЛИНЧЕРЫ, ЛИНЧЕТЫ, ЛИНЧЕТЫ, 52
Теперь, когда я думаю, что у меня есть все требования этого вопроса, моя дискуссия.
Для CompSci вопрос, очевидно, сводится к кратчайшему пути в графе G, вершинами которого являются слова, а ребра которого соединяют слова, отличающиеся одной буквой. Эффективное создание графа не тривиально - у меня действительно есть идея, которую я должен пересмотреть, чтобы уменьшить сложность до O (V n hash + E). То, как я это делаю, включает в себя создание графа, который вставляет дополнительные вершины (соответствующие словам с одним символом подстановки) и гомеоморфен рассматриваемому графу. Я подумал об использовании этого графа, а не о сокращении до G - и я полагаю, что с точки зрения игры в гольф я должен был это сделать - исходя из того, что подстановочный узел с более чем 3 ребрами уменьшает число ребер в графе, и Стандартное время выполнения алгоритмов кратчайшего пути составляет O(V heap-op + E)
.
Тем не менее, первое, что я сделал, - запустил анализ графиков G для разных длин слов, и я обнаружил, что они чрезвычайно редки для слов из 5 или более букв. 5-буквенный граф имеет 12478 вершин и 40759 ребер; добавление узлов ссылок ухудшает график. К тому времени, когда у вас до 8 букв, ребер меньше, чем узлов, и 3/7 слов «в стороне». Поэтому я отклонил эту идею оптимизации как не очень полезную.
Идея, которая оказалась полезной, состояла в том, чтобы изучить кучу. Я могу честно сказать, что я реализовал несколько умеренно экзотических куч в прошлом, но ни одна из них не была такой экзотической, как эта. Я использую A-star (поскольку C не дает никакой пользы, учитывая кучу, которую я использую) с очевидной эвристикой количества букв, отличных от цели, и небольшой анализ показывает, что в любой момент времени не более 3 различных приоритетов в кучу. Когда я открываю узел с приоритетом (стоимость + эвристика) и смотрю на его соседей, я рассматриваю три случая: 1) стоимость соседа равна стоимости + 1; эвристика соседа - эвристика-1 (потому что буква, которую она меняет, становится «правильной»); 2) стоимость + 1 и эвристика + 0 (потому что буква, которую она меняет, переходит от «неправильно» к «все еще неправильно»; 3) стоимость + 1 и эвристика + 1 (потому что буква, которую она меняет, переходит от «правильной» к «неправильной»). Поэтому, если я расслаблю соседа, я собираюсь вставить его с тем же приоритетом, приоритетом + 1 или приоритетом + 2. В результате я могу использовать массив из 3-х элементов связанных списков для кучи.
Я должен добавить примечание о моем предположении, что поиск хеша является постоянным. Хорошо, вы можете сказать, но как насчет хэш-вычислений? Ответ в том, что я их амортизирую: java.lang.String
кэширует их hashCode()
, поэтому общее время, затрачиваемое на вычисление хэшей, составляет O(V n^2)
(при создании графика).
Есть еще одно изменение, которое влияет на сложность, но вопрос о том, является ли это оптимизацией или нет, зависит от ваших предположений о статистике. (IMO, ставя «лучшее решение Big O» в качестве критерия, является ошибкой, потому что нет лучшей сложности по простой причине: не существует единственной переменной). Это изменение влияет на шаг генерации графика. В приведенном выше коде это:
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);
}
}
}
Это O(V * n * (n + hash) + E * hash)
. Но эта O(V * n^2)
часть получается из генерации новой строки из n символов для каждой ссылки и последующего вычисления ее хеш-кода. Этого можно избежать с помощью вспомогательного класса:
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;
}
}
Тогда первая половина генерации графа становится
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;
}
}
Используя структуру хеш-кода, мы можем генерировать ссылки в O(V * n)
. Тем не менее, это имеет эффект удара. Мое предположение о том, что поиск по хешу является постоянным временем, предполагает, что сравнение объектов на равенство обходится дешево. Тем не менее, тест на равенство Link O(n)
в худшем случае. Худший случай - когда у нас есть столкновение хешей между двумя равными ссылками, сгенерированными из разных слов - т.е. это происходит O(E)
раз во второй половине генерации графа. Кроме этого, за исключением маловероятного случая столкновения хэша между неравными ссылками, мы хороши. Итак, мы обменялись O(V * n^2)
на O(E * n * hash)
. Смотрите мой предыдущий пункт о статистике.
HOUSE
доGORGE
, указывается как 2. Я понимаю, что есть 2 промежуточных слова, так что это имеет смысл, но число операций было бы более интуитивно понятным.