Karena panjang terdaftar sebagai kriteria, inilah versi golf di 1681 karakter (mungkin masih dapat ditingkatkan 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>{}
Versi ungolfed, yang menggunakan nama paket dan metode dan tidak memberikan peringatan atau memperluas kelas hanya untuk alias mereka, adalah:
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;
}
}
}
Seperti yang Anda lihat, analisis biaya berjalan adalah O(filelength + num_words * hash + V * n * (n + hash) + E * hash)
. Jika Anda akan menerima asumsi saya bahwa penyisipan / pencarian tabel hash adalah waktu yang konstan, itu O(filelength + V n^2 + E)
. Statistik grafik tertentu dalam SOWPODS berarti yang O(V n^2)
benar - benar mendominasi O(E)
sebagian besar n
.
Output sampel:
IDOLA, IDOLS, IDYLS, ODYLS, ODALS, OVAL, OVEL, OVENS, EVENS, ETENS, STENS, SKENS, KULIT, SPINS, SPINE, 13
WICCA, PROSY, OY
BRINY, BRIN, TRIN, TAIN, TARNS, BENANG, YAWNS, YAWPS, YAPPS, 7
GALES, GAS, GAS, GEST, GESTE, GESSE, DESSE, 5
SURES, SELAMA, DUNE, DINES, DING, DINGY, 4
LICHT, LIGHT, BIGHT, BIGOT, BIGOS, BIROS, GIROS, GIRNS, GURNS, GUANS, GUANA, RUANA, 10
SARGE, SERGE, SERRE, SERRS, SEER, DEER, DYERS, OYERS, OVERS, OVEL, OVAL, ODAL, ODYLS, ODYLS, IDYLS, 12
KEIRS, SEIRS, SEER, BEERS, BRERS, BRERE, BREME, CREME, CREPE, 7
Ini adalah salah satu dari 6 pasangan dengan jalur terpendek terpanjang:
GAINEST, FAINEST, FAIREST, SAIREST, SAIDEST, SADDEST, MADDEST, MIDDEST, MILDEST, WILDEST, WILIEST, WALIEST, WANIEST, CANIEST, CANTEST, CONTEST, KONFES, KONFES, KONFER, KONTAK, COPERS, COVER. POPPITS, Poppies, POPSIES, MOPSIES, MOUSIES, mousse, POUSSES, plusses, PLISSES, Prisses, menekan, PREASES, UREASES, UNEASES, UNCASES, UNCASED, UNBASED, UNBATED, UNMATED, UNMETED, UNMEWED, ENMEWED, ENDEWED, INDEWED, diindeks, INDEKS, INDEN, INDENTS, INSENTS, INCENTS, INFESTS, INFECTS, INJECTS, 56
Dan salah satu pasangan 8-huruf yang larut dalam kasus terburuk:
ENROBING, UNROBING, UNROPING, UNCOPING, UNCAPING, UNCAGING, ENCAGING, ENRAGING, ENRACING, ENLACING, UNLACING, UNLAYING, UPLAYING, SPLAYING, SPAYING, SABAR, STROYING, STROYING, STROKE, STOCK, STUCING, MENGAMBING, MENGUMPUTAN, MENGUMPULKAN, MENGUMPULKAN, MENGUMPULKAN, MENGUMPULKAN, MENGUMPULKAN, MENGUAT, MENGUMPULKAN, MENGUMPULKAN, MENGUMPULKAN, MENGUMPULKAN. CRIMPING, CRISPING, CRISPINS, CRISPENS, CRISPERS, CRIMPERS, CRAMPERS, CLAMPERS, CLASPERS, CLASHER, SLASHER, SLATHERS, SLITHERS, SMITHERS, SOOTHERS, SOUTHERS, MOUTHERS, MOUTCHERS, PUTERS, PUTERS, PUTERS LUNCHERS, LYNCHERS, LYNCHETS, LINCHETS, 52
Sekarang saya pikir saya sudah mendapatkan semua persyaratan dari pertanyaan, diskusi saya.
Untuk CompSci, pertanyaannya jelas berkurang menjadi jalur terpendek dalam grafik G yang simpulnya adalah kata-kata dan ujung-ujungnya menghubungkan kata-kata yang berbeda dalam satu huruf. Menghasilkan grafik secara efisien bukanlah hal sepele - Saya benar-benar memiliki ide yang perlu saya tinjau kembali untuk mengurangi kompleksitas menjadi O (V n hash + E). Cara saya melakukannya melibatkan pembuatan grafik yang menyisipkan simpul tambahan (sesuai dengan kata-kata dengan satu karakter wildcard) dan bersifat homeomorfik dengan grafik yang dimaksud. Saya memang mempertimbangkan menggunakan grafik itu daripada mengurangi ke G - dan saya kira itu dari sudut pandang golf yang seharusnya saya lakukan - atas dasar bahwa simpul wildcard dengan lebih dari 3 tepi mengurangi jumlah tepi dalam grafik, dan kasus berjalan terburuk standar algoritma jalur terpendek adalahO(V heap-op + E)
.
Namun, hal pertama yang saya lakukan adalah menjalankan beberapa analisis grafik G untuk panjang kata yang berbeda, dan saya menemukan bahwa mereka sangat jarang untuk kata-kata dari 5 huruf atau lebih. Grafik 5 huruf memiliki 12478 simpul dan 40759 tepi; menambahkan node tautan membuat grafik semakin buruk. Pada saat Anda hingga 8 huruf ada lebih sedikit tepi daripada node, dan 3/7 dari kata-kata itu "menyendiri". Jadi saya menolak gagasan optimasi itu karena tidak terlalu membantu.
Gagasan yang terbukti bermanfaat adalah memeriksa tumpukan itu. Saya bisa jujur mengatakan bahwa saya telah menerapkan beberapa tumpukan yang cukup eksotis di masa lalu, tetapi tidak ada yang eksotis seperti ini. Saya menggunakan bintang-A (karena C tidak memberikan manfaat mengingat tumpukan yang saya gunakan) dengan heuristik yang jelas dari jumlah huruf yang berbeda dari target, dan sedikit analisis menunjukkan bahwa setiap saat tidak ada lebih dari 3 prioritas yang berbeda di tumpukan. Ketika saya memunculkan simpul yang prioritasnya adalah (biaya + heuristik) dan melihat tetangganya, ada tiga kasus yang saya pertimbangkan: 1) biaya tetangga adalah biaya + 1; heuristik tetangga adalah heuristik-1 (karena huruf yang diubahnya menjadi "benar"); 2) biaya + 1 dan heuristik + 0 (karena huruf yang diubahnya berubah dari "salah" ke "masih salah"; 3) biaya +1 dan heuristik +1 (karena huruf yang diubahnya berubah dari "benar" menjadi "salah"). Jadi, jika saya menenangkan tetangga, saya akan memasukkannya pada prioritas yang sama, prioritas + 1, atau prioritas + 2. Sebagai hasilnya, saya dapat menggunakan array 3-elemen dari daftar tertaut untuk heap.
Saya harus menambahkan catatan tentang asumsi saya bahwa pencarian hash konstan. Baiklah, Anda mungkin berkata, tetapi bagaimana dengan perhitungan hash? Jawabannya adalah bahwa saya mengamortisasi mereka: java.lang.String
menyimpannya hashCode()
, jadi total waktu yang dihabiskan untuk menghitung hash adalahO(V n^2)
(dalam menghasilkan grafik).
Ada perubahan lain yang memengaruhi kompleksitas, tetapi pertanyaan apakah ini merupakan pengoptimalan atau tidak bergantung pada asumsi Anda tentang statistik. (IMO menempatkan "solusi Big O terbaik" sebagai kriteria adalah kesalahan karena tidak ada kompleksitas terbaik, karena alasan sederhana: tidak ada variabel tunggal). Perubahan ini memengaruhi langkah pembuatan grafik. Dalam kode di atas, itu:
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);
}
}
}
Itu O(V * n * (n + hash) + E * hash)
. Tetapi O(V * n^2)
bagian tersebut berasal dari menghasilkan string n-karakter baru untuk setiap tautan dan kemudian menghitung kode hash-nya. Ini dapat dihindari dengan kelas pembantu:
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;
}
}
Kemudian paruh pertama pembuatan grafik menjadi
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;
}
}
Dengan menggunakan struktur kode hash kita dapat menghasilkan tautan O(V * n)
. Namun, ini memiliki efek knock-on. Melekat dalam asumsi saya bahwa pencarian hash adalah waktu yang konstan adalah asumsi bahwa membandingkan objek untuk kesetaraan itu murah. Namun, uji persamaan Link O(n)
dalam kasus terburuk. Kasus terburuk adalah ketika kita memiliki tabrakan hash antara dua tautan yang sama yang dihasilkan dari kata-kata yang berbeda - yaitu terjadi O(E)
kali pada paruh kedua generasi grafik. Selain itu, kecuali jika terjadi tabrakan hash antara tautan yang tidak sama, kami baik-baik saja. Jadi kami telah diperdagangkan di O(V * n^2)
untuk O(E * n * hash)
. Lihat poin saya sebelumnya tentang statistik.
HOUSE
keGORGE
dilaporkan sebagai 2. Saya menyadari ada 2 kata menengah, sehingga tidak masuk akal, tapi # operasi akan lebih intuitif.