1P5: Word Changer


20

Про це було написано як частина першого періодичного прем'єрного програмування головоломки .

Гра

Наведено початкове і кінцеве слово однакової довжини. Мета гри полягає в тому, щоб змінити одну літеру у вихідному слові, щоб утворити інше дійсне слово, повторюючи цей крок до досягнення кінця, використовуючи найменшу кількість кроків. Наприклад, з урахуванням слів TREE та FLED, вихід буде таким:

TREE
FREE
FLEE
FLED
2

Технічні умови

  • Статті у Вікіпедії для OWL або SOWPODS можуть бути корисною відправною точкою для списків слів.
  • Програма повинна підтримувати два способи вибору початкового та кінцевого слів:
    1. Вказано користувачем за допомогою командного рядка, stdin або будь-якої іншої, що підходить для вашої мови вибору (просто згадайте, що ви робите).
    2. Вибір з файлу навмання 2 слів.
  • Початкові та закінчувальні слова, а також усі проміжні слова повинні бути однакової довжини.
  • Кожен крок повинен бути надрукований у рядку.
  • Заключний рядок результату повинен містити кількість проміжних кроків, які потрібно було пройти між початковим і кінцевим словами.
  • Якщо між початковим і кінцевим словами не можна знайти відповідності, вихід повинен складатися з 3 рядків: початкове слово, закінчувальне слово та слово OY.
  • Включіть у відповідь нотацію Big O для свого рішення
  • Будь ласка, включіть 10 унікальних пар початкових і кінцевих слів (звичайно, їх вихід), щоб показати кроки, які виробляє ваша програма. (Щоб заощадити місце, хоча ваша програма повинна виводити їх на окремі рядки, ви можете об'єднати їх в один рядок для публікації, замінивши нові рядки пробілами та комою між кожним запуском.

Цілі / Критерії виграшу

  • Виграє найшвидше / найкраще рішення Big O, що створить найкоротші проміжні кроки через тиждень.
  • Якщо нічия буде результатом критеріїв Big O, виграє найкоротший код.
  • Якщо все-таки є нічия, виграє перше рішення, яке здійснить найшвидший і найкоротший перегляд.

Тести / Вибірка вибірки

DIVE
DIME
DAME
NAME
2

PEACE
PLACE
PLATE
SLATE
2

HOUSE
HORSE
GORSE
GORGE
2

POLE
POSE
POST
PAST
FAST
3

Перевірка

Я працюю над сценарієм, який можна використовувати для перевірки результатів.

Це буде:

  1. Переконайтесь, що кожне слово є дійсним.
  2. Переконайтесь, що кожне слово рівно на 1 букву відрізняється від попереднього слова.

Я не буду:

  1. Перевірте, чи була використана найкоротша кількість кроків.

Після того, як я напишу це, я, звичайно, оновлю цю посаду. (:


4
Мені здається дивним, що про виконання 3 операцій, щоб дістатися HOUSEдо GORGE, повідомляється як 2. Я розумію, що є 2 проміжні слова, тому це має сенс, але # операцій було б більш інтуїтивно зрозумілим.
Матвій читайте

4
@Peter, на сторінці вікіпедії совідів є ~ 15
тис.

4
Я не хочу все це знати, але головоломка насправді має ім'я, її винайшов Льюїс Керролл en.wikipedia.org/wiki/Word_ladder
st0le

1
У вас є невирішена мета у запитанні: The fastest/best Big O solution producing the shortest interim steps after one week will win.оскільки ви не можете гарантувати, що найшвидшим рішенням є тим часом, який виконує найменші кроки, вам слід надати перевагу, якщо одне рішення використовує менше кроків, але досягає мети пізніше.
користувач невідомий

2
Я просто хочу підтвердити, BATі у CATмене будуть нульові кроки, правда?
st0le

Відповіді:


9

Оскільки довжина вказана як критерій, ось версія для гольфу на 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). Дивіться мій попередній пункт про статистику.


Я вважаю, що 8192 - це розмір буфера за замовчуванням для BufferedReader (на SunVM)
st0le

@ st0le, я опустив цей параметр у версії для гольфу, і він не завдає шкоди в невольтовому.
Пітер Тейлор

5

Java

Складність : ?? (У мене немає ступеня CompSci, тому я вдячний за допомогу в цьому питанні.)

Введення : Введіть пару слів (якщо ви хочете більше 1 пари) у командному рядку. Якщо не вказаний командний рядок, вибираються два чіткі випадкові слова.

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

Я використовував Мемоїзацію для швидших результатів. Шлях до словника є жорстким кодом.
st0le

@Joey, це було раніше, але не більше. Тепер у нього є статичне поле, яке воно збільшується щоразу і додає System.nanoTime().
Пітер Тейлор

@Joey, ага, добре, але я покину це поки, не хочу збільшувати свої зміни: P
st0le

о, btw, я на роботі, і ці веб-сайти, які скремблирують, очевидно, заблоковані, тому я не маю доступу до словників ... генерує ці 10 унікальних слів найкраще до завтрашнього ранку. Ура!
st0le

Ви можете зменшити (обчислювальну) складність, виконавши двонаправлений bfs, тобто шукайте з обох сторін і зупиняєтесь, коли стикаєтесь із вузлом, відвідуваним з іншого боку.
Набб

3

c на unix

Використання алгоритму дікстра.

Велика частка коду - це реалізація коштовного дерева, яка служить для утримання

  • Список слів (таким чином, мінімізуючи кількість прочитаних вхідних файлів (двічі без аргументів, один раз для інших випадків) за умови, що IO файлу повільний
  • Часткові дерева, коли ми їх будуємо.
  • Кінцевий шлях.

Будь-який зацікавлений в тому , як це працює , ймовірно , слід прочитати findPath, processі processOne(і пов'язані з ними коментарі). А може, buildPathіbuildPartialPath . Решта - бухгалтерія та риштування. Кілька процедур, що використовуються під час тестування та розробки, але не у "виробничій" версії, залишилися на місці.

Я використовую /usr/share/dict/wordsна моєму Mac OS 10.5 ящик, який має так багато довгих, езотеричні записи, дозволяючи йому працювати повністю випадковим чином генерує багато з OYс.

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

Деякі результати:

$ ./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

Аналіз складності нетривіальний. Пошук - це двостороннє, ітеративне поглиблення.

  • Для кожного досліджуваного вузла я проходжу весь список слів (хоча обмежений словами правильної довжини). Назвіть довжину списку W.
  • Мінімальна кількість кроків полягає в S_min = (<number of different letter>-1)тому, що якщо у нас є лише одна літера, ми помічаємо зміни на 0 проміжних кроків. Максимум важко оцінити, див. ТРИКІ - СУОШ, що біжать вище. Кожна половина дерева буде S/2-1доS/2
  • Я не робив аналіз поведінки гілок дерева, але називаю це B.

Тож мінімальна кількість операцій навколо 2 * (S/2)^B * W, не дуже добре.


Можливо, це наївно на мене, але я не бачу нічого у вашому дизайні чи виконанні, що вимагало б обважнення. Хоча Дійкстра справді працює для невагомих графіків (вага краю незмінно "1"), чи не застосовуватиметься тут простий пошук в широті для поліпшення своїх меж O(|V|+|E|)замість O(|E|+|V| log |V|)?
MrGomez
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.