1P5:ワードチェンジャー


20

これは、最初の定期的なプレミアプログラミングパズルプッシュの一部として書かれました。

ゲーム

同じ長さの開始ワードと終了ワードが提供されます。ゲームの目的は、開始単語の1文字を変更して別の有効な単語を作成し、最小限のステップを使用して、終了単語に達するまでこのステップを繰り返すことです。たとえば、TREEとFLEDという単語を指定すると、出力は次のようになります。

TREE
FREE
FLEE
FLED
2

仕様書

  • OWLまたはSOWPODSのウィキペディアの記事は、単語リストに関する限り、有用な出発点になる可能性があります。
  • プログラムは、開始ワードと終了ワードを選択する2つの方法をサポートする必要があります。
    1. コマンドライン、stdin、または選択した言語に適したものを使用してユーザーが指定します(実行していることを記載してください)。
    2. ファイルからランダムに2つの単語を選択します。
  • 開始語と終了語、およびすべての中間語は同じ長さでなければなりません。
  • 各ステップはその行に印刷する必要があります。
  • 出力の最終行は、開始語と終了語の間を取得するために必要な中間ステップの数である必要があります。
  • 開始語と終了語の間で一致が見つからない場合、出力は、開始語、終了語、および単語OYの3行で構成される必要があります。
  • ソリューションのビッグO表記を回答に含めてください
  • プログラムが生成するステップを示すために、10の一意の開始と終了の単語のペアを(もちろん、その出力とともに)含めてください。(スペースを節約するために、プログラムはこれらを個々の行に出力する必要がありますが、これらを投稿用に1行に統合し、各実行の間に新しい行をスペースとコンマで置き換えることができます。

目標/勝利基準

  • 1週間後に最短の暫定ステップを生成する最速/最高の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、SOWPODSに応じてページウィキペディア〜15K 13文字より長い言葉がある
gnibblerは

4
私はそれをすべて知っているつもりはありませんが、パズルには実際に名前があります。これはルイスキャロルによって発明されました 。en.wikipedia.org
wiki/

1
質問には未定の目標があります:The fastest/best Big O solution producing the shortest interim steps after one week will win.最速のソリューションは最少のステップを使用するものであることを保証することはできないので、1つのソリューションがより少ないステップを使用し、後で目標に到達する場合は、設定を提供する必要があります。
ユーザー不明

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

サンプル出力:

IDOLA、IDOLS、IDYLS、ODYLS、ODALS、OVALS、OVELS、OVENS、EVENS、ETENS、STENS、SKENS、SKINS、SPINS、SPINE、13

ウィッカ、プロシー、オイ

BRINY、BRINS、TRINS、TAINS、TARNS、YARNS、YAWNS、YAWPS、YAPPS、7

GALES、GASES、GASTS、GESTS、GESTE、GESSE、DESSE、5

SURES、DURES、DUNES、DINES、DINGS、DINGY、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つのペアの1つです。

GAINEST、FAINEST、FAIREST、SAIREST、SAIDEST、SADDEST、MADDEST、MIDDEST、MILDEST、WILDEST、WILIEST、WALIEST、WANIEST、CANIEST、CANTEST、CONTEST、CONFEST、CONFESS、CONFERS、CONKERS、COOKERS、COOPERS、COPPERS、POPPERS、POPPERS、POPPERS、POPPERSポピーズ、ポピー、ポピー、モッピー、マウジー、ムース、ポース、プラス、プリシス、プリシス、プレス、プリーゼ、ウレアーゼ、ウーゼ、アンキャス、アンキャス、アンベース、アンベート、アンメート、アンメッド、アンメッド、エド、エド、エド、エド、エド、エド、エド、エド、エド、エド、エド、エド、エドインデックス、インデン、インデント、インセンツ、インセスト、インフェスト、インフェクト、インジェクト、56

そして、最悪の場合の可溶性8文字ペアの1つ:

エンロービング、UNROBING、UNROPING、UNCOPING、UNCAPING、アンケージング、ENCAGING、ENRAGING、ENRACING、ENLACING、UNLACING、UNLAYING、UPLAYING、広がり、噴霧、逸脱、STROYING、なでる、STOOKING、前かがみ、踏み、遊説、スランピング、凝集、CRUMPING、圧着、圧着、圧着、圧着、圧着、圧着、圧着、クランプ、クラッパー、クラッシャー、スラッシャー、スラッサー、スリザー、スミザー、スマザー、サザーズ、サワーズ、マザーズ、ムーサーズ、カウチャーズ、ポーチャーズ、ポーチャーズ、ポーチャーズ、ポーチャーズ昼食、リンチェ、リンチェット、リンチ、52

これで、質問のすべての要件が不要になったと思うので、議論します。

CompSciの場合、頂点が単語であり、エッジが1文字が異なる単語をつなぐグラフGで、質問は明らかに最短経路になります。グラフを効率的に生成することは簡単ではありません。実際、複雑さをO(V n hash + E)に減らすために再検討する必要があるアイデアがあります。私がそれを行う方法は、余分な頂点(1つのワイルドカード文字を含む単語に対応する)を挿入し、問題のグラフと同相であるグラフを作成することを含みます。Gに減らすのではなく、そのグラフを使用することを検討しました。ゴルフの観点からは、3つ以上のエッジを持つワイルドカードノードがグラフ内のエッジの数を減らすことに基づいて行うべきでした。最短経路アルゴリズムの標準的な最悪の場合の実行時間はO(V heap-op + E)です。

しかし、私が最初にしたことは、さまざまな語長についてグラフGの分析を実行することでした。5文字以上の語については非常にまばらであることを発見しました。5文字のグラフには、12478の頂点と40759のエッジがあります。リンクノードを追加すると、グラフが悪化します。最大8文字になるまでに、ノードよりもエッジが少なくなり、3/7の単語が「アローフ」になります。そのため、最適化のアイデアはあまり役に立たないと断りました。

役立つと判明したアイデアは、ヒープを調べることでした。正直に言って、過去に中程度のエキゾチックなヒープを実装したことがありますが、これほどエキゾチックなものはありません。ターゲットとは異なる文字数のヒューリスティックでA-starを使用します(Cは使用しているヒープを考慮するとメリットがありません)。ヒープ内。優先度が(コスト+ヒューリスティック)であるノードをポップし、その隣接ノードを見ると、3つのケースが考えられます。1)隣接ノードのコストはコスト+1です。neighbourのヒューリスティックはheuristic-1です(変更される文字が「正しい」ため)。2)cost + 1およびheuristic + 0(変更する文字が「間違った」から「まだ間違っている」に変わるため。3)cost + 1とheuristic + 1(変更する文字が「正しい」から「間違った」に変わるため)。そのため、隣人をリラックスさせる場合、同じ優先度、priority + 1、またはpriority + 2で挿入します。その結果、ヒープにリンクリストの3要素配列を使用できます。

ハッシュルックアップは一定であるという仮定についてのメモを追加する必要があります。あなたは言うかもしれませんが、ハッシュ計算はどうですか?その答えは、私はそれらを償却しているということです:java.lang.StringキャッシュするhashCode()ので、ハッシュの計算に費やされる合計時間はO(V n^2)(グラフの生成に)あります。

複雑さに影響する別の変更がありますが、それが最適化であるかどうかの問題は、統計に関する仮定に依存します。(「最高のBig Oソリューション」を基準として置くIMOは、単純な理由で、最高の複雑さがないため、間違いです。単一の変数がないためです)。この変更は、グラフ生成手順に影響します。上記のコードでは、次のとおりです。

        Map<String, Set<String>> wordsToLinks = new HashMap<String, Set<String>>();
        Map<String, Set<String>> linksToWords = new HashMap<String, Set<String>>();

        // Cost: O(Vn * (n + hash))
        for (String word : words)
        {
            // Cost: O(n*(n + hash))
            for (int i = 0; i < word.length(); i++)
            {
                // Cost: O(n + hash)
                char[] ch = word.toCharArray();
                ch[i] = '.';
                String link = new String(ch).intern();
                add(wordsToLinks, word, link);
                add(linksToWords, link, word);
            }
        }

        // Cost: O(V * n * hash + E * hash)
        for (Map.Entry<String, Set<String>> from : wordsToLinks.entrySet()) {
            String src = from.getKey();
            wordsToWords.put(src, new HashSet<String>());
            for (String link : from.getValue()) {
                Set<String> to = linksToWords.get(link);
                for (String snk : to) {
                    // Note: equality test is safe here. Cost is O(hash)
                    if (snk != src) add(wordsToWords, src, snk);
                }
            }
        }

それがありますO(V * n * (n + hash) + E * hash)。しかし、そのO(V * n^2)部分は、各リンクに対して新しいn文字の文字列を生成し、そのハッシュコードを計算することから来ています。これは、ヘルパークラスを使用して回避できます。

    private static class Link
    {
        private String str;
        private int hash;
        private int missingIdx;

        public Link(String str, int hash, int missingIdx) {
            this.str = str;
            this.hash = hash;
            this.missingIdx = missingIdx;
        }

        @Override
        public int hashCode() { return hash; }

        @Override
        public boolean equals(Object obj) {
            Link l = (Link)obj; // Unsafe, but I know the contexts where I'm using this class...
            if (this == l) return true; // Essential
            if (hash != l.hash || missingIdx != l.missingIdx) return false;
            for (int i = 0; i < str.length(); i++) {
                if (i != missingIdx && str.charAt(i) != l.str.charAt(i)) return false;
            }
            return true;
        }
    }

次に、グラフ生成の前半は

        Map<String, Set<Link>> wordsToLinks = new HashMap<String, Set<Link>>();
        Map<Link, Set<String>> linksToWords = new HashMap<Link, Set<String>>();

        // Cost: O(V * n * hash)
        for (String word : words)
        {
            // apidoc: The hash code for a String object is computed as
            // s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
            // Cost: O(n * hash)
            int hashCode = word.hashCode();
            int pow = 1;
            for (int j = word.length() - 1; j >= 0; j--) {
                Link link = new Link(word, hashCode - word.charAt(j) * pow, j);
                add(wordsToLinks, word, link);
                add(linksToWords, link, word);
                pow *= 31;
            }
        }

ハッシュコードの構造を使用して、でリンクを生成できますO(V * n)。ただし、これにはノックオン効果があります。ハッシュルックアップが一定時間であるという私の仮定に内在するのは、オブジェクトが等しいかどうかを比較するのが安価であるという仮定です。ただし、Linkの等価性テストはO(n)最悪の場合です。最悪のケースは、異なる単語から生成された2つの等しいリンク間でハッシュ衝突が発生した場合です。つまりO(E)、グラフ生成の後半に発生します。それ以外は、等しくないリンク間でハッシュ衝突が起こる可能性が低い場合を除き、私たちは大丈夫です。だから私たちは下取りしO(V * n^2)ましたO(E * n * hash)。統計に関する以前のポイントを参照してください。


私は8192(SunVM上)BufferedReaderのためのデフォルトのバッファサイズであると考えている
st0le

@ st0le、私はゴルフバージョンではそのパラメーターを省略しましたが、ゴルフバージョンでは害はありません。
ピーターテイラー

5

Java

複雑さ:?? (私はCompSci学位を持っていないので、この問題について助けていただければ幸いです。)

入力:コマンドラインで単語のペア(必要に応じて複数のペア)を提供します。コマンドラインが指定されていない場合、2つの異なるランダムな単語が選択されます。

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()ます。
ピーターテイラー

@ジョーイ、ああ、わかりました、しかし、私は今のところそれを残します、私のリビジョンをインクリメントしたくない:P
st0le

ああ、ところで、私は仕事をしていて、それらのスクラブルWebサイトは明らかにブロックされているので、辞書にアクセスできません...明日の朝までにこれらの10のユニークな単語を生成します。乾杯!
-st0le

双方向のbfsを行うことで(計算)の複雑さを軽減できます。つまり、両側から検索し、反対側からアクセスしたノードに遭遇したときに停止できます。
ナブ

3

UNIXのc

dijkstraアルゴリズムを使用します。

コードの大部分は、コスチュームn項ツリー実装であり、

  • ファイルIOが遅いという前提でのワードリスト(したがって、入力ファイルの読み取り回数を最小化(引数なしの場合は2回、他の場合は1回))
  • 部分的なツリーを作成します。
  • 最終パス。

誰もが見に興味がある、それは動作しますおそらく読まなければならないfindPathprocessprocessOne(およびそれに関連するコメント)。そして多分buildPathそしてbuildPartialPath。残りは簿記と足場です。「プロダクション」バージョンではなく、テストおよび開発中に使用されるいくつかのルーチンはそのまま残されています。

私は/usr/share/dict/wordsMac OS 10.5ボックスで使用しています。これには非常に多くの難解なエントリがあり、完全にランダムに実行すると多く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;
}

いくつかの出力:

$ ./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)1文字しか離れていない場合、中間ステップ0で変化を記録するためです。最大値を定量化するのは困難です。上記のTRICKY--SWOOSHを参照してください。ツリーの各半分はなりS/2-1S/2
  • ツリーの分岐動作の分析は行っていませんが、それを呼び出していBます。

そのため、操作の最小数は約であり 2 * (S/2)^B * W、あまり良くありません。


たぶんこれは私には素朴ですが、エッジの重みを必要とする設計や実装には何も見当たりません。ダイクストラは実際に重みのないグラフ(エッジの重みは常に「1」)で機能しますが、境界を改善するために単純な幅優先検索をここに適用しO(|V|+|E|)ないのO(|E|+|V| log |V|)ですか?
-MrGomez
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.