1P5: โปรแกรมเปลี่ยนคำ


20

นี้ถูกเขียนเป็นส่วนหนึ่งของครั้งแรกงวดพรีเมียร์โปรแกรมปริศนากด

เกม

คำที่มีความยาวเท่ากันเริ่มต้นและสิ้นสุด วัตถุประสงค์ของเกมคือการเปลี่ยนตัวอักษรหนึ่งตัวในคำเริ่มต้นเพื่อสร้างคำที่ถูกต้องที่แตกต่างกันโดยทำซ้ำขั้นตอนนี้จนกว่าจะถึงคำลงท้ายโดยใช้ขั้นตอนจำนวนน้อยที่สุด ตัวอย่างเช่นให้คำว่า TREE และ FLED เอาต์พุตจะเป็น:

TREE
FREE
FLEE
FLED
2

ข้อมูลจำเพาะ

  • บทความ Wikipedia สำหรับ 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 คำดังนั้นมันสมเหตุสมผล แต่ # ของการดำเนินการจะง่ายขึ้น
Matthew อ่าน

4
@ ปีเตอร์ตามหน้าวิกิพีเดีย sowpods มี ~ 15k คำนานกว่า 13 ตัวอักษร
gnibbler

4
ฉันไม่ได้ตั้งใจจะรู้ทั้งหมด แต่ปริศนามีชื่อจริงมันถูกคิดค้นโดย Lewis Carroll 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

เนื่องจากความยาวมีการระบุว่าเป็นเกณฑ์ต่อไปนี้เป็นรุ่น golfed ที่ 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, ETENS, STENS, SKENSS, SPINS, SPINE, 13

WICCA, PROSY, OY

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

GALES, GASTS, GASTS, GESTS, GESTE, GESSE, DESSE, 5

SURES, DURE, DUNES, DINES, DINGS, DINGY, 4

LICHT เบา BIGHT BIGOT BIGOS BIROS GIROS GURNS GURNS กวาน GUANA RUANA 10

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

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

นี่คือหนึ่งใน 6 คู่ที่มีเส้นทางที่สั้นที่สุดที่ยาวที่สุด:

GAINEST, FAINEST, FAIREST, SAIREST, SAADEST, SADDEST, MADDEST, MIDDEST, MILDEST, WILDEST, WILIEST, WALIEST, WANIEST, CIANEST, CONFEST, CONFESS, CONFERS, CONFERS, COPERS, POPPERS, POPPERS, POPPERS, COPERS, POPPERS, POPPERS, POPPERS POPPITS, POPPIES, POPSIES, MOUSIES, MOUSIES, MOUSSES, POUSSES, PLUSSES, PLISSES, PLISSES, PRISSES, PRESSES, PRASES, UASASES, UNAS, UNBASED, UNBASED, UNBASED ดัชนี, INDENES, เยื้อง, Incents, INCESTS, INFESTS, INFECTS, INJECTS, 56

และหนึ่งในแปดตัวอักษรที่ละลายน้ำได้กรณีที่เลวร้ายที่สุด:

ENROBING, UNROBING, UNROPING, UNCOPING, UNCAPING, UNCAGING, ENCAGING, ความล้มเหลว, ความล้มเหลว, การทำให้ล้มลง, การล้ม, การทำให้ล้มลง, การล้ม, การทำให้ล้ม CRIMPING, CRISPING, CRISPINS, CRISPENS, CRISPERS, CRIMPERS, CRCHERS, PCHERS, PCHERS, PCHERS, PERSHERS, PERSHERS, PERSHERS, PERSHERS, PERSHERS, PERSHERS, POTHERS, POTHERS, PERSHERS, PERSHERS LUNCHERS, LYNCHERS, LYNCHETS, LINCHETS, 52

ตอนนี้ฉันคิดว่าฉันมีความต้องการครบถ้วนสำหรับคำถามแล้วการอภิปรายของฉัน

สำหรับ CompSci คำถามจะลดลงอย่างเห็นได้ชัดในเส้นทางที่สั้นที่สุดในกราฟ G ซึ่งจุดยอดเป็นคำและขอบซึ่งเชื่อมต่อคำต่างกันในจดหมายฉบับหนึ่ง การสร้างกราฟได้อย่างมีประสิทธิภาพนั้นไม่สำคัญเลย - จริง ๆ แล้วฉันมีความคิดว่าฉันต้องกลับไปเพื่อลดความซับซ้อนเป็น O (V n hash + E) วิธีที่ฉันทำเกี่ยวข้องกับการสร้างกราฟที่แทรกจุดยอดพิเศษ (ตรงกับคำที่มีอักขระไวด์การ์ดหนึ่งตัว) และเป็น homeomorphic ของกราฟที่เป็นปัญหา ฉันได้พิจารณาใช้กราฟนั้นแทนที่จะลดลงเป็น G - และฉันคิดว่าจากมุมมองของการเล่นกอล์ฟที่ฉันควรทำ - บนพื้นฐานที่โหนดสัญลักษณ์ที่มีมากกว่า 3 ขอบลดจำนวนของขอบในกราฟและ O(V heap-op + E)มาตรฐานเวลาที่เลวร้ายกรณีการทำงานของอัลกอริทึมเส้นทางที่สั้นที่สุดคือ

อย่างไรก็ตามสิ่งแรกที่ฉันทำคือเรียกใช้การวิเคราะห์บางอย่างของกราฟ G สำหรับความยาวคำต่าง ๆ และฉันค้นพบว่าพวกมันกระจัดกระจายมากสำหรับคำที่มีตัวอักษร 5 ตัวขึ้นไป กราฟ 5 ตัวอักษรมีจุดยอด 12478 และขอบ 40759; การเพิ่มลิงค์ของโหนดทำให้กราฟแย่ลง ในเวลาที่คุณมีตัวอักษรไม่เกิน 8 ตัวจะมีขอบน้อยกว่าโหนดและ 3/7 ของคำนั้น "ห่างเหิน" ดังนั้นฉันจึงปฏิเสธแนวคิดการเพิ่มประสิทธิภาพที่ไม่เป็นประโยชน์จริงๆ

แนวคิดที่พิสูจน์ว่ามีประโยชน์คือการตรวจสอบกอง ฉันสามารถพูดได้อย่างตรงไปตรงมาว่าฉันได้ติดตั้งฮีปแปลกใหม่ในระดับปานกลางในอดีต แต่ก็ไม่มีอะไรแปลกใหม่เช่นนี้ ฉันใช้ A-star (เนื่องจาก C ไม่ให้ประโยชน์กับกองที่ฉันใช้) ด้วยจำนวนฮิวริสติกที่ชัดเจนของตัวอักษรที่แตกต่างจากเป้าหมายและการวิเคราะห์แสดงให้เห็นว่าในเวลาใดก็ตามมีลำดับความสำคัญไม่เกิน 3 ครั้ง ในกอง เมื่อฉันปรากฏโหนดที่มีลำดับความสำคัญคือ (ราคา + การวิเคราะห์พฤติกรรม) และดูที่เพื่อนบ้านมีสามกรณีที่ฉันกำลังพิจารณา: 1) ค่าใช้จ่ายของเพื่อนบ้านคือราคา + 1; ฮิวริสติกของเพื่อนบ้านคือ heuristic-1 (เพราะตัวอักษรที่เปลี่ยนจะกลายเป็น "ถูกต้อง"); 2) ราคา +1 และแบบแก้ปัญหา + 0 (เนื่องจากตัวอักษรเปลี่ยนไปจาก "ผิด" เป็น "ยังผิด"; 3) ค่าใช้จ่าย +1 และการแก้ปัญหา + 1 (เนื่องจากตัวอักษรเปลี่ยนไปจาก "ถูกต้อง" เป็น "ผิด") ดังนั้นถ้าฉันผ่อนคลายเพื่อนบ้านฉันจะใส่มันด้วยความสำคัญลำดับความสำคัญ + 1 หรือลำดับความสำคัญ + 2 เป็นผลให้ฉันสามารถใช้อาร์เรย์ 3 องค์ประกอบของรายการที่เชื่อมโยงสำหรับกอง

ฉันควรเพิ่มหมายเหตุเกี่ยวกับข้อสันนิษฐานของฉันว่าการค้นหาแฮชคงที่ ดีมากคุณอาจพูดว่า แต่แล้วการคำนวณแฮชล่ะ คำตอบก็คือฉันจะตัดค่าใช้จ่ายออกไป: java.lang.StringแคชhashCode()ดังนั้นเวลาทั้งหมดที่ใช้ในการคำนวณแฮชคือO(V n^2)(ในการสร้างกราฟ)

มีการเปลี่ยนแปลงอีกอย่างที่ส่งผลต่อความซับซ้อน แต่คำถามที่ว่าเป็นการเพิ่มประสิทธิภาพหรือไม่นั้นขึ้นอยู่กับสมมติฐานของคุณเกี่ยวกับสถิติ (IMO วาง "ทางออก Big Big ที่ดีที่สุด" เป็นเกณฑ์เป็นข้อผิดพลาดเพราะไม่มีความซับซ้อนที่ดีที่สุดด้วยเหตุผลง่ายๆ: ไม่มีตัวแปรเดียว) การเปลี่ยนแปลงนี้มีผลต่อขั้นตอนการสร้างกราฟ ในรหัสข้างต้นมันคือ:

        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)กรณีที่เลวร้ายที่สุด กรณีที่แย่ที่สุดคือเมื่อเรามีการชนกันระหว่างสองลิงค์ที่เท่ากันซึ่งสร้างจากคำต่าง ๆ นั่นคือเกิดขึ้นO(E)ครั้งในช่วงครึ่งหลังของการสร้างกราฟ นอกเหนือจากนั้นยกเว้นในกรณีที่มีการชนกันของแฮ็ชระหว่างลิงค์ที่ไม่เท่ากันเราน่าจะดี ดังนั้นเราจึงได้มีการซื้อขายในสำหรับO(V * n^2) O(E * n * hash)ดูจุดก่อนหน้าของฉันเกี่ยวกับสถิติ


ผมเชื่อว่า 8192 เป็นขนาดของบัฟเฟอร์เริ่มต้นสำหรับ BufferedReader (บน SunVM)
st0le

@ st0le ฉันไม่ได้ระบุพารามิเตอร์นั้นในเวอร์ชัน golfed และจะไม่ส่งผลเสียต่อ ungolfed
Peter Taylor

5

ชวา

ความซับซ้อน : (ฉันไม่มีปริญญา CompSci ดังนั้นฉันขอขอบคุณความช่วยเหลือในเรื่องนี้)

อินพุต : ระบุคู่ของคำ (มากกว่า 1 คู่หากคุณต้องการ) ในบรรทัดคำสั่ง หากไม่มีการระบุบรรทัดคำสั่ง 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()ตอนนี้ก็มีสนามแบบคงที่เพิ่มขึ้นในแต่ละครั้งและเพิ่มการ
Peter Taylor

@ Joey, aah, ok แต่ฉันจะปล่อยไว้ตอนนี้ไม่ต้องการเพิ่มการแก้ไขของฉัน: P
st0le

โอ้วววววฉันกำลังทำงานอยู่และเว็บไซต์ Scrabble เหล่านั้นถูกบล็อกดังนั้นฉันไม่สามารถเข้าถึงพจนานุกรม ... จะสร้างคำที่ไม่ซ้ำกัน 10 คำที่ดีที่สุดในพรุ่งนี้เช้า ไชโย!
st0le

คุณสามารถลดความซับซ้อน (การคำนวณ) โดยทำ bfs สองทิศทางคือการค้นหาจากทั้งสองด้านและหยุดเมื่อคุณพบโหนดที่เข้าชมจากอีกด้านหนึ่ง
Nabb

3

c บนยูนิกซ์

การใช้อัลกอริทึม dijkstra

ส่วนใหญ่ของรหัสคือการแต่งกายต้นไม้ n-ary ซึ่งทำหน้าที่ถือ

  • wordlist (ดังนั้นการลดจำนวนครั้งที่อ่านไฟล์อินพุต (สองครั้งสำหรับการไม่มีข้อโต้แย้งหนึ่งครั้งสำหรับกรณีอื่น ๆ ) จากการสันนิษฐานว่าไฟล์ IO ช้า
  • ต้นไม้บางส่วนในขณะที่เราสร้างพวกเขา
  • เส้นทางสุดท้าย

ทุกคนที่สนใจในการมองเห็นว่ามันทำงานอาจจะอ่านfindPath, processและprocessOne(และแสดงความคิดเห็นที่เกี่ยวข้อง) และอาจและbuildPath buildPartialPathส่วนที่เหลือเป็นการทำบัญชีและนั่งร้าน มีการใช้งานประจำหลายครั้งระหว่างการทดสอบและการพัฒนา แต่ไม่มีในเวอร์ชัน "การผลิต"

ฉันใช้/usr/share/dict/wordsบน Mac 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)เพราะถ้าเราแยกห่างกันเพียงตัวเดียวเราจะทำคะแนนการเปลี่ยนแปลงที่ 0 ขั้นตอนกลาง ค่าสูงสุดนั้นยากที่จะหาปริมาณเพื่อดู TRICKY - SWOOSH ด้านบน ครึ่งหนึ่งของต้นไม้แต่ละคนจะS/2-1ไปS/2
  • ผมไม่เคยทำวิเคราะห์พฤติกรรมแตกแขนงของต้นไม้ Bแต่เรียกมันว่า

ดังนั้นจำนวนการดำเนินงานขั้นต่ำจึงอยู่ 2 * (S/2)^B * Wในระดับไม่ดีนัก


บางทีนี่อาจไร้เดียงสาของฉัน แต่ฉันไม่เห็นอะไรเลยในการออกแบบหรือการนำไปใช้งานของคุณที่ต้องการน้ำหนักขอบ ในขณะที่ Dijkstra ใช้งานได้กับกราฟที่ไม่ได้ถ่วง (น้ำหนักของขอบคือ "1") อย่างสม่ำเสมอการค้นหาแบบกว้างครั้งแรกจะไม่ถูกนำไปใช้ที่นี่เพื่อปรับปรุงขอบเขตของคุณO(|V|+|E|)แทนที่จะเป็นO(|E|+|V| log |V|)?
MrGomez
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.