Оскільки довжина вказана як критерій, ось версія для гольфу на 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>{}
Невикористана версія, яка використовує назви пакетів та методи і не дає попереджень і не розширює класи лише для їх псевдоніму:
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
БРІНІ, БРІНС, БРОНІ, КАЙКИ, ТАРНС, ПІВНЯ, ЯТНІ, ЯВП, ЯППС, 7
ГАЛЕС, ГАЗ, ГОСТ, ГЕСТ, ГЕСТЕ, ГЕСС, ДЕСС, 5
РЕЗУЛЬТАТИ, ДЕРЖАВИ, ДУНІ, СТРАНИ, ДІНГИ, МОНТАЖ, 4
LICHT, LIGHT, BIGHT, BIGOT, BIGOS, BIROS, GIROS, GIRNS, GURNS, GUANS, GUANA, RUANA, 10
БОЛЬШИЙ, СЕРЖ, СЕРРЕ, СЕРСЬ, СЕРС, ДЕРЖ, ДІЙЕР, ОЙЕР, ОВЕР, ОВЕЛЬ, ОВАЛ, ОДАЛ, ОДИЛЬ, ІДИЛЬ, 12
KEIRS, SEIRS, SEERS, BEERS, BRERS, BRERE, BREME, CREME, CREPE, 7
Це одна з 6 пар з найдовшим найкоротшим шляхом:
GAINEST, FAINEST, FAIREST, SAIREST, SAIDEST, SADDEST, MADDEST, MIDDEST, MILDEST, WILDEST, WILIEST, WALIEST, WANIEST, CANIEST, CANTEST, CONTEST, CONFEST, CONFESS, CONFERS, CONKERS, COOKERS, POPPOPPOP POPPITS, маки, POPSIES, MOPSIES, MOUSIES, муси, POUSSES, плюси, PLISSES, PRISSES, преси, PREASES, уреази, UNEASES, UNCASES, безкорпусний, UNBASED, UNBATED, роз'єднаних, UNMETED, UNMEWED, ENMEWED, ENDEWED, INDEWED, індексований, ІНДЕКСИ, ІНДЕНТИ, ВИДАЛЕННЯ, НАПРЯМИ, ІНЦЕКТИ, ІНФЕСТИ, ІНФЕКТИ, ІНЖЕКТИ, 56
І одна з найгірших розчинних пар з 8 літер:
РОЗПОЛУЧЕННЯ, ПОДРОБКА, РОЗКРИТТЯ, РОЗКЛАДУВАННЯ, РОЗДІЛУВАННЯ, РОЗДІЛУВАННЯ, РОЗДІЛУВАННЯ, СВЯЗАННЯ, ПОВЕРНЕННЯ, ПІДГОТУВАННЯ, СПРАЙГУВАННЯ, СТРАЙТУВАННЯ, СТРУЖУВАННЯ, СТУПУВАННЯ, СТУПУВАННЯ, ЗАСТОСУВАННЯ КРИМПІВАННЯ, КРИПІЗАЦІЯ, КРИСПІН, КРИПЕНС, КРИПЕР, КРИМПЕР, КАМПЕР, КЛАМПЕР, КЛАПЕР, КЛАШЕР, СЛАШЕР, СЛАЙТЕР, СЛІТЕР, МИТЕР, МАТЕР, СУТЕР, ПІВЧЕНЬ, МОТЕР, МУШ, МАЙБУЧЕР, КУПЕР, КУПЕР ЛУНЧЕРИ, ЛІНЧЕРИ, ЛИНЧЕТИ, ЛИНЧЕТИ, 52
Тепер, коли я думаю, що я вийшов з ладу всі вимоги питання, моє обговорення.
Для CompSci питання, очевидно, зводиться до найкоротшого шляху в графі G, вершинами якого є слова, чиї краї з'єднують слова, що відрізняються однією буквою. Ефективне генерування графіка не є тривіальним - у мене фактично є ідея, яку мені потрібно переглянути, щоб зменшити складність до O (V n хеш + Е). Те, як я це роблю, передбачає створення графіка, який вставляє додаткові вершини (відповідні словам з одним символом підстановки) і є гомеоморфним для відповідного графіка. Я розглядав можливість використання цього графіка, а не зведення до G - і я вважаю, що з точки зору гольфу я повинен був це зробити - виходячи з того, що вузол wildcard з більш ніж 3 ребрами зменшує кількість ребер у графіку, а Стандартний найгірший час роботи алгоритмів найкоротшого шляху O(V heap-op + E)
.
Однак першим, що я зробив, було провести кілька аналізів графіків G для різної довжини слів, і я виявив, що вони надзвичайно рідкісні для слів з 5 і більше літер. 5-літерний графік має 12478 вершин і 40759 ребер; додавання вузлів посилань робить графік гіршим. На той момент, коли вам до 8 букв, є менше ребер, ніж вузли, і 3/7 слів є "осторонь". Тому я відкинув ідею оптимізації як не дуже корисну.
Ідея, яка виявилася корисною, була вивчити купу. Я можу чесно сказати, що я впроваджував кілька помірно-екзотичних груп у минулому, але жодної настільки екзотичної, як ця. Я використовую A-зірку (оскільки 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)
. Однак це має ефект стукання. Притаманне моєму припущенню, що пошук хешу - це постійний час, - це припущення, що порівняння об'єктів за рівністю є дешевим. Однак тест на рівність Лінка є O(n)
в гіршому випадку. Найгірший випадок, коли у нас відбувається хеш-зіткнення між двома рівними ланками, породженими різними словами - тобто це трапляється O(E)
періоди у другій половині генерації графіків. Крім цього, за винятком випадків хеш-зіткнення між нерівними посиланнями, ми добре. Тому ми торгували O(V * n^2)
на O(E * n * hash)
. Дивіться мій попередній пункт про статистику.
HOUSE
доGORGE
, повідомляється як 2. Я розумію, що є 2 проміжні слова, тому це має сенс, але # операцій було б більш інтуїтивно зрозумілим.