1P5: Thay đổi từ


20

Điều này đã được viết như là một phần của Câu đố lập trình Premier định kỳ đầu tiên .

Tro choi

Một từ bắt đầu và kết thúc có cùng độ dài được cung cấp. Mục tiêu của trò chơi là thay đổi một chữ cái trong từ bắt đầu để tạo thành một từ hợp lệ khác, lặp lại bước này cho đến khi đạt được từ kết thúc, sử dụng số lượng bước ít nhất. Ví dụ: với các từ TREE và FLED, đầu ra sẽ là:

TREE
FREE
FLEE
FLED
2

Thông số kỹ thuật

  • Các bài viết Wikipedia cho OWL hoặc SOWPODS có thể là một điểm khởi đầu hữu ích cho đến khi danh sách từ đi.
  • Chương trình nên hỗ trợ hai cách chọn từ bắt đầu và kết thúc:
    1. Được chỉ định bởi người dùng thông qua dòng lệnh, stdin hoặc bất cứ điều gì phù hợp với ngôn ngữ bạn chọn (chỉ cần đề cập đến những gì bạn đang làm).
    2. Chọn ngẫu nhiên 2 từ trong tệp.
  • Các từ bắt đầu và kết thúc, cũng như tất cả các từ tạm thời phải có cùng độ dài.
  • Mỗi bước nên được in ra trên dòng của nó.
  • Dòng cuối cùng của đầu ra của bạn phải là số bước tạm thời được yêu cầu để có được giữa các từ bắt đầu và kết thúc.
  • Nếu không thể tìm thấy kết quả khớp giữa các từ bắt đầu và kết thúc, đầu ra phải bao gồm 3 dòng: từ bắt đầu, từ kết thúc và từ OY.
  • Bao gồm ký hiệu Big O cho giải pháp của bạn trong câu trả lời của bạn
  • Vui lòng bao gồm 10 cặp từ bắt đầu và kết thúc duy nhất (tất nhiên là có đầu ra) để hiển thị các bước mà chương trình của bạn tạo ra. (Để tiết kiệm dung lượng, trong khi chương trình của bạn nên xuất những dòng này trên từng dòng riêng lẻ, bạn có thể hợp nhất chúng thành một dòng duy nhất để đăng, thay thế các dòng mới bằng khoảng trắng và dấu phẩy giữa mỗi lần chạy.

Mục tiêu / Tiêu chí chiến thắng

  • Giải pháp Big O nhanh nhất / tốt nhất tạo ra các bước tạm thời ngắn nhất sau một tuần sẽ giành chiến thắng.
  • Nếu kết quả hòa từ các tiêu chí Big O, mã ngắn nhất sẽ giành chiến thắng.
  • Nếu vẫn còn một sự ràng buộc, giải pháp đầu tiên để đạt được sửa đổi nhanh nhất và ngắn nhất của nó sẽ giành chiến thắng.

Kiểm tra / đầu ra mẫu

DIVE
DIME
DAME
NAME
2

PEACE
PLACE
PLATE
SLATE
2

HOUSE
HORSE
GORSE
GORGE
2

POLE
POSE
POST
PAST
FAST
3

Xác nhận

Tôi đang làm việc trên một kịch bản có thể được sử dụng để xác nhận đầu ra.

Nó sẽ:

  1. Đảm bảo rằng mỗi từ là hợp lệ.
  2. Đảm bảo rằng mỗi từ chính xác là 1 chữ cái khác với từ trước đó.

Nó sẽ không:

  1. Kiểm tra xem số bước ngắn nhất đã được sử dụng.

Khi tôi đã nhận được văn bản đó, tất nhiên tôi sẽ cập nhật bài viết này. (


4
Nó có vẻ lạ với tôi rằng thực hiện 3 thao tác để có được từ HOUSEđể GORGEđược báo cáo là 2. Tôi nhận thấy có 2 chữ trung gian, vì vậy nó không có ý nghĩa, nhưng # hoạt động sẽ trực quan hơn.
Matthew đọc

4
@Peter, theo trang wikipedia của sowpods có ~ 15k từ dài hơn 13 chữ cái
gnibbler

4
Tôi không có nghĩa là biết tất cả, nhưng câu đố thực sự có Tên, nó được phát minh bởi Lewis Carroll en.wikipedia.org/wiki/Word_ladder
st0le

1
Bạn có một mục tiêu không thể giải quyết được trong câu hỏi: The fastest/best Big O solution producing the shortest interim steps after one week will win.Vì bạn không thể đảm bảo, giải pháp nhanh nhất là giải pháp sử dụng ít bước nhất, bạn nên cung cấp tùy chọn, nếu một giải pháp sử dụng ít bước hơn, nhưng sẽ đạt được mục tiêu sau.
người dùng không biết

2
Tôi chỉ muốn xác nhận BATCATsẽ không có bước nào, phải không?
st0le

Câu trả lời:


9

Vì độ dài được liệt kê là một tiêu chí, đây là phiên bản chơi gôn với 1681 ký tự (có thể vẫn có thể được cải thiện 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>{}

Phiên bản không được sử dụng, sử dụng tên và phương thức gói và không đưa ra cảnh báo hoặc mở rộng các lớp chỉ để bí danh chúng là:

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;
        }
    }
}

Như bạn có thể thấy, phân tích chi phí hoạt động là O(filelength + num_words * hash + V * n * (n + hash) + E * hash). Nếu bạn chấp nhận giả định của tôi rằng việc chèn / tra cứu bảng băm là thời gian không đổi, thì đó là O(filelength + V n^2 + E). Số liệu thống kê cụ thể của các biểu đồ trong SOWPODS có nghĩa là O(V n^2)thực sự chiếm ưu thế O(E)đối với hầu hết n.

Đầu ra mẫu:

IDola, IDOLS, IDYLS, ODYLS, ODALS, OVALS, OVELS, OVENS, EVENS, ETENS, STENS, SKENS, SKIN, SPIN, SPINE, 13

WICCA, PROSY, OY

BRINY, BRIN, TRIN, TAIN, TARNS, YARNS, YAWNS, YAWPS, YAPPS, 7

GALES, GASES, GASTS, GEST, GESTE, GESSE, DESSE, 5

SOUND, DOUND, DUNES, DINES, DINGY, DINGY, 4

GIẤY PHÉP, ÁNH SÁNG, ĐÊM, BIGOT, BIGOS, BIROS, GIROS, GIRNS, GACKS, GUans, GUANA, RUANA, 10

SARGE, SERGE, SERRE, SERRS, SEERS, DEERS, DYERS, OYERS, OVERS, OVELS, OVALS, ODALS, ODYLS, IDYLS, 12

KEIRS, SEIRS, SEERS, BEERS, BRERS, BRERE, BREME, CREME, CREPE, 7

Đây là một trong 6 cặp có con đường ngắn nhất dài nhất:

GAINEST, FAINEST, FAIREST, SAIREST, SAIDEST, SADDEST, MADDEST, MIDDEST, SADDEST, MADDEST, MIDDEST, MILDEST, WILEST, WILIEST, CONFEST, CANIEST, WILIEST, CONFEST, CONFEST, CONFEST, CONFEST, CONFEST, CONFEST DÂN TỘC, DÂN TỘC INDEXES, INDENES, INDENT, INCENT, INCESTS, INFESTS, INFECTS, IN DỰ ÁN, 56

Và một trong những cặp 8 chữ cái hòa tan trong trường hợp xấu nhất:

TĂNG TRƯỞNG KHAI THÁC, KHAI THÁC, KHAI THÁC LUNCHERS, LYNCHERS, LYNCHET, LINCHETS, 52

Bây giờ tôi nghĩ rằng tôi đã có tất cả các yêu cầu của câu hỏi ngoài lề, cuộc thảo luận của tôi.

Đối với một CompSci, câu hỏi rõ ràng giảm xuống đường đi ngắn nhất trong đồ thị G có các đỉnh là các từ và các cạnh của nó kết nối các từ khác nhau trong một chữ cái. Tạo đồ thị hiệu quả không phải là chuyện nhỏ - Tôi thực sự có một ý tưởng tôi cần xem lại để giảm độ phức tạp thành O (V n hash + E). Cách tôi thực hiện liên quan đến việc tạo một biểu đồ chèn thêm các đỉnh (tương ứng với các từ có một ký tự đại diện) và tương đồng với biểu đồ được đề cập. Tôi đã cân nhắc sử dụng biểu đồ đó thay vì giảm xuống G - và tôi cho rằng từ quan điểm chơi gôn tôi nên làm - trên cơ sở một nút ký tự đại diện có nhiều hơn 3 cạnh làm giảm số cạnh trong biểu đồ và trường hợp xấu nhất tiêu chuẩn thời gian chạy của thuật toán đường dẫn ngắn nhất là O(V heap-op + E).

Tuy nhiên, điều đầu tiên tôi làm là chạy một số phân tích biểu đồ G cho các độ dài từ khác nhau và tôi phát hiện ra rằng chúng cực kỳ thưa thớt đối với các từ có 5 chữ cái trở lên. Biểu đồ 5 chữ cái có 12478 đỉnh và 40759 cạnh; thêm các nút liên kết làm cho biểu đồ tồi tệ hơn. Vào thời điểm bạn có tới 8 chữ cái, có ít cạnh hơn các nút và 3/7 từ là "cách ly". Vì vậy, tôi đã từ chối ý tưởng tối ưu hóa đó là không thực sự hữu ích.

Ý tưởng đã chứng minh hữu ích là kiểm tra đống. Tôi có thể thành thật nói rằng tôi đã thực hiện một số đống kỳ lạ vừa phải trong quá khứ, nhưng không có gì kỳ lạ như thế này. Tôi sử dụng sao A (vì C không mang lại lợi ích gì cho heap tôi đang sử dụng) với số lượng chữ cái rõ ràng khác với mục tiêu và một chút phân tích cho thấy rằng bất cứ lúc nào cũng có không quá 3 ưu tiên khác nhau trong đống. Khi tôi bật một nút có mức độ ưu tiên là (chi phí + heuristic) và xem xét các hàng xóm của nó, có ba trường hợp tôi đang xem xét: 1) chi phí của neighbour là chi phí + 1; heuristic của neighbour là heuristic-1 (vì chữ cái nó thay đổi trở thành "chính xác"); 2) chi phí + 1 và heuristic + 0 (vì chữ cái nó thay đổi chuyển từ "sai" sang "vẫn sai"; 3) chi phí + 1 và heuristic + 1 (vì chữ cái nó thay đổi đi từ "chính xác" thành "sai"). Vì vậy, nếu tôi thư giãn hàng xóm, tôi sẽ chèn nó ở cùng mức độ ưu tiên, mức độ ưu tiên + 1 hoặc mức độ ưu tiên + 2. Kết quả là tôi có thể sử dụng một mảng gồm 3 phần tử của các danh sách được liên kết cho heap.

Tôi nên thêm một lưu ý về giả định của tôi rằng tra cứu băm là không đổi. Rất tốt, bạn có thể nói, nhưng còn tính toán băm thì sao? Câu trả lời là tôi sẽ loại bỏ chúng: java.lang.Stringlưu trữ nó hashCode(), vì vậy tổng thời gian sử dụng băm tính toán là O(V n^2)(trong việc tạo biểu đồ).

Có một sự thay đổi khác ảnh hưởng đến sự phức tạp, nhưng câu hỏi liệu nó có phải là tối ưu hóa hay không phụ thuộc vào giả định của bạn về thống kê. (IMO đặt "giải pháp Big O tốt nhất" làm tiêu chí là một sai lầm vì không có độ phức tạp tốt nhất, vì một lý do đơn giản: không có một biến duy nhất). Sự thay đổi này ảnh hưởng đến bước tạo đồ thị. Trong đoạn mã trên, đó là:

        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);
                }
            }
        }

Đó là O(V * n * (n + hash) + E * hash). Nhưng O(V * n^2)phần này đến từ việc tạo ra một chuỗi ký tự n mới cho mỗi liên kết và sau đó tính toán mã băm của nó. Điều này có thể tránh được với một lớp người trợ giúp:

    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;
        }
    }

Sau đó, nửa đầu của thế hệ đồ thị trở thành

        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;
            }
        }

Bằng cách sử dụng cấu trúc của mã băm, chúng tôi có thể tạo các liên kết trong O(V * n). Tuy nhiên, điều này có tác dụng gõ cửa. Kế thừa trong giả định của tôi rằng tra cứu băm là thời gian không đổi là một giả định rằng so sánh các đối tượng cho sự bình đẳng là rẻ. Tuy nhiên, bài kiểm tra bình đẳng của Link là O(n)trong trường hợp xấu nhất. Trường hợp xấu nhất là khi chúng ta có xung đột băm giữa hai liên kết bằng nhau được tạo từ các từ khác nhau - tức là nó xảy ra O(E)lần trong nửa sau của thế hệ đồ thị. Ngoài ra, ngoại trừ trường hợp không thể xảy ra xung đột băm giữa các liên kết không bằng nhau, chúng tôi đều tốt. Vì vậy, chúng tôi đã được giao dịch trong O(V * n^2)cho O(E * n * hash). Xem điểm trước của tôi về số liệu thống kê.


Tôi tin rằng 8192 là kích thước bộ đệm mặc định cho BufferedReader (trên SunVM)
st0le

@ st0le, tôi đã bỏ qua tham số đó trong phiên bản chơi gôn và nó không gây hại cho người không có kinh nghiệm.
Peter Taylor

5

Java

Phức tạp : ?? (Tôi không có Bằng CompSci, vì vậy tôi sẽ đánh giá cao sự giúp đỡ trong vấn đề này.)

Đầu vào : Cung cấp Cặp từ (nhiều hơn 1 cặp nếu bạn muốn) trong dòng lệnh. Nếu không có dòng lệnh được chỉ định, 2 từ ngẫu nhiên riêng biệt được chọn.

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

public class M {

    // for memoization
    private static Map<String, List<String>> memoEdits = new HashMap<String, List<String>>(); 
    private static Set<String> dict;

    private static List<String> edits(String word, Set<String> dict) {
        if(memoEdits.containsKey(word))
            return memoEdits.get(word);

        List<String> editsList = new LinkedList<String>();
        char[] letters = word.toCharArray();
        for(int i = 0; i < letters.length; i++) {
            char hold = letters[i];
            for(char ch = 'A'; ch <= 'Z'; ch++) {
                if(ch != hold) {
                    letters[i] = ch;
                    String nWord = new String(letters);
                    if(dict.contains(nWord)) {
                        editsList.add(nWord);
                    }
                }
            }
            letters[i] = hold;
        }
        memoEdits.put(word, editsList);
        return editsList;
    }

    private static Map<String, String> bfs(String wordFrom, String wordTo,
                                           Set<String> dict) {
        Set<String> visited = new HashSet<String>();
        List<String> queue = new LinkedList<String>();
        Map<String, String> pred = new HashMap<String, String>();
        queue.add(wordFrom);
        while(!queue.isEmpty()) {
            String word = queue.remove(0);
            if(word.equals(wordTo))
                break;

            for(String nWord: edits(word, dict)) {
                if(!visited.contains(nWord)) {
                    queue.add(nWord);
                    visited.add(nWord);
                    pred.put(nWord, word);
                }
            }
        }
        return pred;
    }

    public static void printPath(String wordTo, String wordFrom) {
        int c = 0;
        Map<String, String> pred = bfs(wordFrom, wordTo, dict);
        do {
            System.out.println(wordTo);
            c++;
            wordTo = pred.get(wordTo);
        }
        while(wordTo != null && !wordFrom.equals(wordTo));
        System.out.println(wordFrom);
        if(wordTo != null)
            System.out.println(c - 1);
        else
            System.out.println("OY");
        System.out.println();
    }

    public static void main(String[] args) throws Exception {
        BufferedReader scan = new BufferedReader(new FileReader(new File("c:\\332609\\dict.txt")),
                                                 40 * 1024);
        String line;
        dict = new HashSet<String>(); //the dictionary (1 word per line)
        while((line = scan.readLine()) != null) {
            dict.add(line);
        }
        scan.close();
        if(args.length == 0) { // No Command line Arguments? Pick 2 random
                               // words.
            Random r = new Random(System.currentTimeMillis());
            String[] words = dict.toArray(new String[dict.size()]);
            int x = r.nextInt(words.length), y = r.nextInt(words.length);
            while(x == y) //same word? that's not fun...
                y = r.nextInt(words.length);
            printPath(words[x], words[y]);
        }
        else { // Arguments provided, search for path pairwise
            for(int i = 0; i < args.length; i += 2) {
                if(i + 1 < args.length)
                    printPath(args[i], args[i + 1]);
            }
        }
    }
}

Tôi đã sử dụng Ghi nhớ, để có kết quả nhanh hơn. Đường dẫn từ điển được mã hóa cứng.
st0le

@Joey, nó đã từng nhưng không còn nữa. Bây giờ nó có một trường tĩnh mà nó tăng lên mỗi lần và thêm vào System.nanoTime().
Peter Taylor

@Joey, aah, ổn, nhưng tôi sẽ để nó cho bây giờ, không muốn để tăng sửa đổi của tôi: P
st0le

oh, btw, tôi đang làm việc và những trang web lặt vặt đó dường như bị chặn nên tôi không có quyền truy cập vào từ điển ... sẽ tạo ra 10 từ duy nhất tốt nhất vào sáng mai. Chúc mừng!
st0le

Bạn có thể giảm độ phức tạp (tính toán) bằng cách thực hiện bfs hai chiều, tức là tìm kiếm từ cả hai phía và dừng lại khi bạn gặp một nút được truy cập từ phía bên kia.
Nabb

3

c trên unix

Sử dụng thuật toán dijkstra.

Một phần lớn của mã là một triển khai cây n-ary trang phục, phục vụ để giữ

  • Danh sách từ (do đó giảm thiểu số lần tệp đầu vào được đọc (hai lần không có đối số, một lần cho các trường hợp khác) với giả định rằng tệp IO chậm
  • Những cây một phần khi chúng ta xây dựng chúng.
  • Con đường cuối cùng.

Bất cứ ai quan tâm đến việc nhìn thấy như thế nào nó hoạt động có lẽ nên đọc findPath, processprocessOne(và bình luận liên quan của họ). Và có thểbuildPathbuildPartialPath. Phần còn lại là sổ sách kế toán và giàn giáo. Một số thói quen được sử dụng trong quá trình thử nghiệm và phát triển nhưng không có trong phiên bản "sản xuất" đã bị bỏ lại.

Tôi đang sử dụng /usr/share/dict/wordstrên Mac OS 10,5 hộp của tôi, trong đó có rất nhiều lâu, mục bí truyền mà để cho nó chạy hoàn toàn một cách ngẫu nhiên tạo ra một nhiều của OYs.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getline.h>
#include <time.h>
#include <unistd.h>
#include <ctype.h>

const char*wordfile="/usr/share/dict/words";
/* const char*wordfile="./testwords.txt"; */
const long double RANDOM_MAX = (2LL<<31)-1;

typedef struct node_t {
  char*word;
  struct node_t*kids;
  struct node_t*next;
} node;


/* Return a pointer to a newly allocated node. If word is non-NULL, 
 * call setWordNode;
 */
node*newNode(char*word){
  node*n=malloc(sizeof(node));
  n->word=NULL;
  n->kids=NULL;
  n->next=NULL;
  if (word) n->word = strdup(word);
  return n;
}
/* We can use the "next" links to treat these as a simple linked list,
 * and further can make it a stack or queue by
 *
 * * pop()/deQueu() from the head
 * * push() onto the head
 * * enQueue at the back
 */
void push(node*n, node**list){
  if (list==NULL){
    fprintf(stderr,"Active operation on a NULL list! Exiting\n");
    exit(5);
  }
  n->next = (*list);
  (*list) = n;
}
void enQueue(node*n, node**list){
  if (list==NULL){
    fprintf(stderr,"Active operation on a NULL list! Exiting\n");
    exit(5);
  }
  if ( *list==NULL ) {
    *list=n;
  } else {
    enQueue(n,&((*list)->next));
  }
}
node*pop(node**list){
  node*temp=NULL;
  if (list==NULL){
    fprintf(stderr,"Active operation on a NULL list! Exiting\n");
    exit(5);
  }
  temp = *list;
  if (temp != NULL) {
    (*list) = temp->next;
    temp->next=NULL;
  }
  return temp;
}
node*deQueue(node**list){ /* Alias for pop */
  return pop(list);
}

/* return a pointer to a node in tree matching word or NULL if none */
node* isInTree(char*word, node*tree){
  node*isInNext=NULL;
  node*isInKids=NULL;
  if (tree==NULL || word==NULL) return NULL;
  if (tree->word && (0 == strcasecmp(word,tree->word))) return tree;
  /* prefer to find the target at shallow levels so check the siblings
     before the kids */
  if (tree->next && (isInNext=isInTree(word,tree->next))) return isInNext;
  if (tree->kids && (isInKids=isInTree(word,tree->kids))) return isInKids;
  return NULL;
}

node* freeTree(node*t){
  if (t==NULL) return NULL;
  if (t->word) {free(t->word); t->word=NULL;}
  if (t->next) t->next=freeTree(t->next);
  if (t->kids) t->kids=freeTree(t->kids);
  free(t);
  return NULL;
}

void printTree(node*t, int indent){
  int i;
  if (t==NULL) return;
  for (i=0; i<indent; i++) printf("\t"); printf("%s\n",t->word);
  printTree(t->kids,indent+1);
  printTree(t->next,indent);
}

/* count the letters of difference between two strings */
int countDiff(const char*w1, const char*w2){
  int count=0;
  if (w1==NULL || w2==NULL) return -1;
  while ( (*w1)!='\0' && (*w2)!='\0' ) {
    if ( (*w1)!=(*w2) ) count++;
    w1++;
    w2++;
  }
  return count;
}

node*buildPartialPath(char*stop, node*tree){
  node*list=NULL;
  while ( (tree != NULL) && 
      (tree->word != NULL) && 
      (0 != strcasecmp(tree->word,stop)) ) {
    node*kid=tree->kids;
    node*newN = newNode(tree->word);
    push(newN,&list);
    newN=NULL;
    /* walk over all all kids not leading to stop */
    while ( kid && 
        (strcasecmp(kid->word,stop)!=0) &&
        !isInTree(stop,kid->kids) ) {
      kid=kid->next;
    }
    if (kid==NULL) {
      /* Assuming a preconditions where isInTree(stop,tree), we should
       * not be able to get here...
       */
      fprintf(stderr,"Unpossible!\n");
      exit(7);
    } 
    /* Here we've found a node that either *is* the target or leads to it */
    if (strcasecmp(stop,kid->word) == 0) {
      break;
    }
    tree = kid;
  }
  return list; 
}
/* build a node list path 
 *
 * We can walk down each tree, identfying nodes as we go
 */
node*buildPath(char*pivot,node*frontTree,node*backTree){
  node*front=buildPartialPath(pivot,frontTree);
  node*back=buildPartialPath(pivot,backTree);
  /* weld them together with pivot in between 
  *
  * The front list is in reverse order, the back list in order
  */
  node*thePath=NULL;
  while (front != NULL) {
    node*n=pop(&front);
    push(n,&thePath);
  }
  if (pivot != NULL) {
    node*n=newNode(pivot);
    enQueue(n,&thePath);
  }
  while (back != NULL) {
    node*n=pop(&back);
    enQueue(n,&thePath);
  }
  return thePath;
}

/* Add new child nodes to the single node in ts named by word. Also
 * queue these new word in q
 * 
 * Find node N matching word in ts
 * For tword in wordList
 *    if (tword is one change from word) AND (tword not in ts)
 *        add tword to N.kids
 *        add tword to q
 *        if tword in to
 *           return tword
 * return NULL
 */
char* processOne(char *word, node**q, node**ts, node**to, node*wordList){
  if ( word==NULL || q==NULL || ts==NULL || to==NULL || wordList==NULL ) {
    fprintf(stderr,"ProcessOne called with NULL argument! Exiting.\n");
    exit(9);
  }
  char*result=NULL;
  /* There should be a node in ts matching the leading node of q, find it */
  node*here = isInTree(word,*ts);
  /* Now test each word in the list as a possible child of HERE */
  while (wordList != NULL) {
    char *tword=wordList->word;
    if ((1==countDiff(word,tword)) && !isInTree(tword,*ts)) {
      /* Queue this up as a child AND for further processing */
      node*newN=newNode(tword);
      enQueue(newN,&(here->kids));
      newN=newNode(tword);
      enQueue(newN,q);
      /* This might be our pivot */
      if ( isInTree(tword,*to) ) {
    /* we have found a node that is in both trees */
    result=strdup(tword);
    return result;
      }
    }
    wordList=wordList->next;
  }
  return result;
}

/* Add new child nodes to ts for all the words in q */
char* process(node**q, node**ts, node**to, node*wordList){
  node*tq=NULL;
  char*pivot=NULL;
  if ( q==NULL || ts==NULL || to==NULL || wordList==NULL ) {
    fprintf(stderr,"Process called with NULL argument! Exiting.\n");
    exit(9);
  }
  while (*q && (pivot=processOne((*q)->word,&tq,ts,to,wordList))==NULL) {
    freeTree(deQueue(q));
  }
  freeTree(*q); 
  *q=tq;
  return pivot;
}

/* Find a path between w1 and w2 using wordList by dijkstra's
 * algorithm
 *
 * Use a breadth-first extensions of the trees alternating between
 * trees.
 */
node* findPath(char*w1, char*w2, node*wordList){
  node*thePath=NULL; /* our resulting path */
  char*pivot=NULL; /* The node we find that matches */
  /* trees of existing nodes */
  node*t1=newNode(w1); 
  node*t2=newNode(w2);
  /* queues of nodes to work on */
  node*q1=newNode(w1);
  node*q2=newNode(w2);

  /* work each queue all the way through alternating until a word is
     found in both lists */
  while( (q1!=NULL) && ((pivot = process(&q1,&t1,&t2,wordList)) == NULL) &&
     (q2!=NULL) && ((pivot = process(&q2,&t2,&t1,wordList)) == NULL) )
    /* no loop body */ ;


  /* one way or another we are done with the queues here */
  q1=freeTree(q1);
  q2=freeTree(q2);
  /* now construct the path */
  if (pivot!=NULL) thePath=buildPath(pivot,t1,t2);
  /* clean up after ourselves */
  t1=freeTree(t1);
  t2=freeTree(t2);

  return thePath;
}

/* Convert a non-const string to UPPERCASE in place */
void upcase(char *s){
  while (s && *s) {
    *s = toupper(*s);
    s++;
  }
}

/* Walks the input file stuffing lines of the given length into a list */
node*getListWithLength(const char*fname, int len){
  int l=-1;
  size_t n=0;
  node*list=NULL;
  char *line=NULL;
  /* open the word file */
  FILE*f = fopen(fname,"r");
  if (NULL==f){
    fprintf(stderr,"Could not open word file '%s'. Exiting.\n",fname);
    exit(3);
  }
  /* walk the file, trying each word in turn */
  while ( !feof(f) && ((l = getline(&line,&n,f)) != -1) ) {
    /* strip trailing whitespace */
    char*temp=line;
    strsep(&temp," \t\n");
    if (strlen(line) == len) {
      node*newN = newNode(line);
      upcase(newN->word);
      push(newN,&list);
    }
  }
  fclose(f);
  return list;
}

/* Assumes that filename points to a file containing exactly one
 * word per line with no other whitespace.
 * It will return a randomly selected word from filename.
 *
 * If veto is non-NULL, only non-matching words of the same length
 * wll be considered.
 */
char*getRandomWordFile(const char*fname, const char*veto){
  int l=-1, count=1;
  size_t n=0;
  char *word=NULL;
  char *line=NULL;
  /* open the word file */
  FILE*f = fopen(fname,"r");
  if (NULL==f){
    fprintf(stderr,"Could not open word file '%s'. Exiting.\n",fname);
    exit(3);
  }
  /* walk the file, trying each word in turn */
  while ( !feof(f) && ((l = getline(&line,&n,f)) != -1) ) {
    /* strip trailing whitespace */
    char*temp=line;
    strsep(&temp," \t\n");
    if (strlen(line) < 2) continue; /* Single letters are too easy! */
    if ( (veto==NULL) || /* no veto means chose from all */ 
     ( 
      ( strlen(line) == strlen(veto) )  && /* veto means match length */
      ( 0 != strcasecmp(veto,line) )       /* but don't match word */ 
       ) ) { 
      /* This word is worthy of consideration. Select it with random
         chance (1/count) then increment count */
      if ( (word==NULL) || (random() < RANDOM_MAX/count) ) {
    if (word) free(word);
    word=strdup(line);
      }
      count++;
    }
  }
  fclose(f);
  upcase(word);
  return word;
}

void usage(int argc, char**argv){
  fprintf(stderr,"%s [ <startWord> [ <endWord> ]]:\n\n",argv[0]);
  fprintf(stderr,
      "\tFind the shortest transformation from one word to another\n");
  fprintf(stderr,
      "\tchanging only one letter at a time and always maintaining a\n");
  fprintf(stderr,
      "\tword that exists in the word file.\n\n");
  fprintf(stderr,
      "\tIf startWord is not passed, chose at random from '%s'\n",
      wordfile);
  fprintf(stderr,
      "\tIf endWord is not passed, chose at random from '%s'\n",
      wordfile);
  fprintf(stderr,
      "\tconsistent with the length of startWord\n");
  exit(2);
}

int main(int argc, char**argv){
  char *startWord=NULL;
  char *endWord=NULL;

  /* intialize OS services */
  srandom(time(0)+getpid());
  /* process command line */
  switch (argc) {
  case 3:
    endWord = strdup(argv[2]);
    upcase(endWord);
  case 2:
    startWord = strdup(argv[1]);
    upcase(startWord);
  case 1:
    if (NULL==startWord) startWord = getRandomWordFile(wordfile,NULL);
    if (NULL==endWord)   endWord   = getRandomWordFile(wordfile,startWord);
    break;
  default:
    usage(argc,argv);
    break;
  }
  /* need to check this in case the user screwed up */
  if ( !startWord || ! endWord || strlen(startWord) != strlen(endWord) ) {
    fprintf(stderr,"Words '%s' and '%s' are not the same length! Exiting\n",
        startWord,endWord);
    exit(1);
  }
  /* Get a list of all the words having the right length */
  node*wordList=getListWithLength(wordfile,strlen(startWord));
  /* Launch into the path finder*/
  node *theList=findPath(startWord,endWord,wordList);
  /* Print the resulting path */
  if (theList) {
    int count=-2;
    while (theList) {
      printf("%s\n",theList->word);
      theList=theList->next;
      count++;
    }
    printf("%d\n",count);
  } else {
    /* No path found case */
    printf("%s %s OY\n",startWord,endWord);
  }
  return 0;
}

Một số đầu ra:

$ ./changeword dive name
DIVE
DIME
DAME
NAME
2
$ ./changeword house gorge
HOUSE
HORSE
GORSE
GORGE
2
$ ./changeword stop read
STOP
STEP
SEEP
SEED
REED
READ
4
$ ./changeword peace slate
PEACE
PLACE
PLATE
SLATE
2
$ ./changeword pole fast  
POLE
POSE
POST
PAST
FAST
3
$ ./changeword          
QUINTIPED LINEARITY OY
$ ./changeword sneaky   
SNEAKY WAXILY OY
$ ./changeword TRICKY
TRICKY
PRICKY
PRINKY
PRANKY
TRANKY
TWANKY
SWANKY
SWANNY
SHANNY
SHANTY
SCANTY
SCATTY
SCOTTY
SPOTTY
SPOUTY
STOUTY
STOUTH
STOUSH
SLOUSH
SLOOSH
SWOOSH
19
$ ./changeword router outlet
ROUTER
ROTTER
RUTTER
RUTHER
OUTHER
OUTLER
OUTLET
5
$ ./changeword 
IDIOM
IDISM
IDIST
ODIST
OVIST
OVEST
OVERT
AVERT
APERT
APART
SPART
SPARY
SEARY
DEARY
DECRY
DECAY
DECAN
DEDAN
SEDAN
17

Phân tích phức tạp là không tầm thường. Tìm kiếm là một hai chiều, lặp đi lặp lại sâu sắc.

  • Đối với mỗi nút được kiểm tra, tôi đi bộ toàn bộ danh sách từ (mặc dù giới hạn ở các từ có độ dài đúng). Gọi độ dài của danh sáchW .
  • Số bước tối thiểu là S_min = (<number of different letter>-1)bởi vì nếu chúng ta chỉ cách nhau một chữ cái, chúng ta sẽ ghi điểm thay đổi ở 0 bước trung gian. Tối đa là khó định lượng, hãy xem TRICKY - SWOOSH chạy ở trên. Mỗi nửa của cây sẽ làS/2-1 đếnS/2
  • Tôi đã không thực hiện một phân tích về hành vi phân nhánh của cây, nhưng gọi nó B.

Vì vậy, số lượng hoạt động tối thiểu là xung quanh 2 * (S/2)^B * W, không thực sự tốt.


Có thể điều này là ngây thơ đối với tôi, nhưng tôi không thấy bất cứ điều gì trong thiết kế hoặc triển khai của bạn đòi hỏi trọng lượng cạnh. Mặc dù Dijkstra thực sự hoạt động đối với các biểu đồ không có trọng số (trọng số cạnh luôn là "1"), nhưng một tìm kiếm đầu tiên đơn giản sẽ áp dụng ở đây để cải thiện giới hạn của bạn O(|V|+|E|)thay vì O(|E|+|V| log |V|)?
MrGomez
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.