ฉันจะหาเส้นทางที่สั้นที่สุดระหว่างเป้าหมายเคลื่อนที่ 100 ชิ้นได้อย่างไร (รวมการสาธิตสด)


89

พื้นหลัง

ภาพนี้แสดงให้เห็นถึงปัญหา: square_grid_with_arrows_giving_directions

ฉันควบคุมวงกลมสีแดงได้ เป้าหมายคือสามเหลี่ยมสีน้ำเงิน ลูกศรสีดำระบุทิศทางที่เป้าหมายจะเคลื่อนที่

ฉันต้องการรวบรวมเป้าหมายทั้งหมดในจำนวนขั้นต่ำ

แต่ละเทิร์นฉันต้องเลื่อน 1 ก้าวไปทางซ้าย / ขวา / ขึ้นหรือลง

แต่ละเทิร์นเป้าหมายจะเคลื่อนที่ 1 ก้าวตามทิศทางที่แสดงบนกระดาน

การสาธิต

ผมเคยใส่ขึ้นสาธิตของปัญหาที่นี่บน Google AppEngine

ฉันจะสนใจมากถ้าใครสามารถเอาชนะคะแนนเป้าหมายได้เพราะนี่จะแสดงให้เห็นว่าอัลกอริทึมปัจจุบันของฉันไม่เหมาะสม (ควรพิมพ์ข้อความแสดงความยินดีหากคุณจัดการสิ่งนี้!)

ปัญหา

อัลกอริทึมปัจจุบันของฉันปรับขนาดได้ไม่ดีกับจำนวนเป้าหมาย เวลาเพิ่มขึ้นอย่างทวีคูณและสำหรับปลา 16 ตัวมันก็หลายวินาทีแล้ว

ฉันต้องการคำนวณคำตอบสำหรับบอร์ดขนาด 32 * 32 และเป้าหมายเคลื่อนที่ 100 ชิ้น

คำถาม

อัลกอริทึมที่มีประสิทธิภาพคืออะไร (ตามหลักการแล้วใน Javascript) สำหรับการคำนวณจำนวนขั้นต่ำในการรวบรวมเป้าหมายทั้งหมด

สิ่งที่ฉันพยายาม

แนวทางปัจจุบันของฉันใช้การช่วยจำ แต่มันช้ามากและฉันไม่รู้ว่ามันจะสร้างทางออกที่ดีที่สุดได้หรือไม่

ฉันแก้ปัญหาย่อยของ "จำนวนขั้นต่ำในการรวบรวมชุดของเป้าหมายและจบลงที่เป้าหมายใดเป้าหมายหนึ่ง"

ปัญหาย่อยได้รับการแก้ไขแบบวนซ้ำโดยการตรวจสอบแต่ละตัวเลือกสำหรับเป้าหมายก่อนหน้านี้ที่เคยไป ฉันคิดว่าเป็นการดีที่สุดเสมอที่จะรวบรวมชุดย่อยของเป้าหมายก่อนหน้านี้ให้เร็วที่สุดเท่าที่จะเป็นไปได้จากนั้นย้ายจากตำแหน่งที่คุณลงเอยไปยังเป้าหมายปัจจุบันโดยเร็วที่สุด (แม้ว่าฉันจะไม่รู้ว่านี่เป็นสมมติฐานที่ถูกต้องหรือไม่ก็ตาม)

สิ่งนี้ส่งผลให้ n * 2 ^ n สถานะที่ต้องคำนวณซึ่งเติบโตอย่างรวดเร็วมาก

รหัสปัจจุบันแสดงอยู่ด้านล่าง:

var DX=[1,0,-1,0];
var DY=[0,1,0,-1]; 

// Return the location of the given fish at time t
function getPt(fish,t) {
  var i;
  var x=pts[fish][0];
  var y=pts[fish][1];
  for(i=0;i<t;i++) {
    var b=board[x][y];
    x+=DX[b];
    y+=DY[b];
  }
  return [x,y];
}

// Return the number of steps to track down the given fish
// Work by iterating and selecting first time when Manhattan distance matches time
function fastest_route(peng,dest) {
  var myx=peng[0];
  var myy=peng[1];
  var x=dest[0];
  var y=dest[1];
  var t=0;
  while ((Math.abs(x-myx)+Math.abs(y-myy))!=t) {
    var b=board[x][y];
    x+=DX[b];
    y+=DY[b];
    t+=1;
  }
  return t;
}

// Try to compute the shortest path to reach each fish and a certain subset of the others
// key is current fish followed by N bits of bitmask
// value is shortest time
function computeTarget(start_x,start_y) {
  cache={};
  // Compute the shortest steps to have visited all fish in bitmask
  // and with the last visit being to the fish with index equal to last
  function go(bitmask,last) {
    var i;
    var best=100000000;
    var key=(last<<num_fish)+bitmask;
    if (key in cache) {
      return cache[key];
    }
    // Consider all previous positions
    bitmask -= 1<<last;
    if (bitmask==0) {
      best = fastest_route([start_x,start_y],pts[last]);
    } else {
      for(i=0;i<pts.length;i++) {
        var bit = 1<<i;
        if (bitmask&bit) {
          var s = go(bitmask,i);   // least cost if our previous fish was i
          s+=fastest_route(getPt(i,s),getPt(last,s));
          if (s<best) best=s;
        }
      }
    }
    cache[key]=best;
    return best;
  }
  var t = 100000000;
  for(var i=0;i<pts.length;i++) {
    t = Math.min(t,go((1<<pts.length)-1,i));
  }
  return t;
}

สิ่งที่ฉันได้พิจารณา

ตัวเลือกบางอย่างที่ฉันสงสัยคือ:

  1. การแคชผลลัพธ์ระดับกลาง การคำนวณระยะทางซ้ำการจำลองจำนวนมากและผลลัพธ์ระดับกลางอาจถูกแคชไว้
    อย่างไรก็ตามฉันไม่คิดว่าสิ่งนี้จะหยุดความซับซ้อนของเลขชี้กำลังได้

  2. อัลกอริทึมการค้นหา A * แม้ว่าจะไม่ชัดเจนสำหรับฉันว่าฮิวริสติกที่ยอมรับได้ที่เหมาะสมจะเป็นอย่างไรและจะมีประสิทธิภาพเพียงใดในทางปฏิบัติ

  3. ตรวจสอบอัลกอริทึมที่ดีสำหรับปัญหาพนักงานขายที่เดินทางและดูว่ามีผลกับปัญหานี้หรือไม่

  4. พยายามที่จะพิสูจน์ว่าปัญหานั้นยากมากและด้วยเหตุนี้จึงไม่มีเหตุผลที่จะแสวงหาคำตอบที่ดีที่สุดสำหรับปัญหานี้


1
ฉันจะไปที่ # 4 และต่อมา # 3: ด้วยบอร์ดที่ใหญ่พอมันจึงเลียนแบบ TSP ได้ดีทีเดียว
John Dvorak

2
เท่าที่ฉันรู้ TSP เป็น NP-hard ที่มีเมตริกแบบยูคลิดเช่นเดียวกับเมตริกแมนฮัตตัน (ตารางสี่เหลี่ยม)
John Dvorak

1
ถ้าคุณทำได้โดยการค้นหาแบบต้นไม้ธรรมดาใช่มันจะเป็นเลขชี้กำลัง อย่างไรก็ตามหากคุณสามารถหาฮิวริสติกที่เหมาะสมได้ในแต่ละขั้นตอนมันอาจไม่เหมาะสมอย่างแท้จริง แต่ก็อาจจะดีมาก การฮิวริสติกที่เป็นไปได้อย่างหนึ่งคือการดูปลาชุดปัจจุบันว่าตัวไหนสามารถเข้าถึงได้เร็วที่สุด ฮิวริสติกรองอาจเป็นปลา 2 ตัวใดที่ฉันสามารถเข้าถึงได้เร็วที่สุด
Mike Dunlavey

2
@MikeDunlavey ซึ่งจะสอดคล้องกับอัลกอริทึม TSP โลภและทำงานได้ดีในทางปฏิบัติ การไปหาปลาที่อยู่ใกล้ที่สุดดูเหมือนจะเป็นความคิดที่ดี
John Dvorak

1
+1 สำหรับหนึ่งในคำถามที่ดีที่สุดที่ฉันเคยพบเมื่อเร็ว ๆ นี้ทั้งเนื้อหาและโครงสร้าง
surfitscrollit

คำตอบ:


24

คุณได้ค้นหาวรรณกรรมหรือไม่? ฉันพบเอกสารเหล่านี้ซึ่งดูเหมือนจะวิเคราะห์ปัญหาของคุณ:

อัปเดต 1:

เอกสารสองชิ้นข้างต้นดูเหมือนจะเน้นไปที่การเคลื่อนที่เชิงเส้นสำหรับเมตริกยูคลิด


ขอบคุณ - ฉันไม่เห็นเอกสารเหล่านั้น แต่ดูมีความเกี่ยวข้องมาก ฉันจะดูว่าฉันสามารถปรับอัลกอริทึมทางพันธุกรรมให้ทำงานในกรณีของฉันได้หรือไม่และเปรียบเทียบกับผลลัพธ์จากวิธีการบังคับแบบเดรัจฉาน
ปีเตอร์เดอรีวาซ

13

วิธีโลภ

แนวทางหนึ่งที่แนะนำในความคิดเห็นคือไปที่เป้าหมายที่ใกล้ที่สุดก่อน

ผมเคยใส่ขึ้นรุ่นสาธิตซึ่งรวมถึงค่าใช้จ่ายในการคำนวณโดยวิธีโลภนี้ที่นี่

รหัสคือ:

function greedyMethod(start_x,start_y) {
  var still_to_visit = (1<<pts.length)-1;
  var pt=[start_x,start_y];
  var s=0;
  while (still_to_visit) {
    var besti=-1;
    var bestc=0;
    for(i=0;i<pts.length;i++) {
      var bit = 1<<i;
      if (still_to_visit&bit) {
        c = fastest_route(pt,getPt(i,s));
        if (besti<0 || c<bestc) {
          besti = i;
          bestc = c;
        }
      }
    }
    s+=c;
    still_to_visit -= 1<<besti;
    pt=getPt(besti,s);
  }
  return s;
}

สำหรับเป้าหมาย 10 เป้าหมายจะอยู่ที่ประมาณสองเท่าของระยะทางที่เหมาะสม แต่บางครั้งก็มากกว่านั้นมาก (เช่น * 4) และในบางครั้งอาจถึงจุดที่เหมาะสม

วิธีนี้มีประสิทธิภาพมากดังนั้นฉันจึงสามารถจ่ายรอบเพื่อปรับปรุงคำตอบได้

ต่อไปฉันกำลังพิจารณาใช้วิธีการของฝูงมดเพื่อดูว่าพวกมันสามารถสำรวจพื้นที่โซลูชันได้อย่างมีประสิทธิภาพ

วิธีการอาณานิคมของมด

วิธีการของAnt colonyดูเหมือนจะใช้ได้ผลดีสำหรับปัญหานี้ ตอนนี้ลิงก์ในคำตอบนี้จะเปรียบเทียบผลลัพธ์เมื่อใช้ทั้งวิธีโลภและมด

แนวคิดก็คือมดจะเลือกเส้นทางของมันโดยอาศัยระดับฟีโรโมนในปัจจุบัน หลังจากการทดลองทุกๆ 10 ครั้งเราจะฝากฟีโรโมนเพิ่มเติมตามเส้นทางที่สั้นที่สุดที่พบ

function antMethod(start_x,start_y) {
  // First establish a baseline based on greedy
  var L = greedyMethod(start_x,start_y);
  var n = pts.length;
  var m = 10; // number of ants
  var numrepeats = 100;
  var alpha = 0.1;
  var q = 0.9;
  var t0 = 1/(n*L);

  pheromone=new Array(n+1); // entry n used for starting position
  for(i=0;i<=n;i++) {
    pheromone[i] = new Array(n);
    for(j=0;j<n;j++)
      pheromone[i][j] = t0; 
  }

  h = new Array(n);
  overallBest=10000000;
  for(repeat=0;repeat<numrepeats;repeat++) {
    for(ant=0;ant<m;ant++) {
      route = new Array(n);
      var still_to_visit = (1<<n)-1;
      var pt=[start_x,start_y];
      var s=0;
      var last=n;
      var step=0;
      while (still_to_visit) {
        var besti=-1;
        var bestc=0;
        var totalh=0;
        for(i=0;i<pts.length;i++) {
          var bit = 1<<i;
          if (still_to_visit&bit) {
            c = pheromone[last][i]/(1+fastest_route(pt,getPt(i,s)));
            h[i] = c;
            totalh += h[i];
            if (besti<0 || c>bestc) {
              besti = i;
              bestc = c;
            }
          }
        }
        if (Math.random()>0.9) {
          thresh = totalh*Math.random();
          for(i=0;i<pts.length;i++) {
            var bit = 1<<i;
            if (still_to_visit&bit) {
              thresh -= h[i];
              if (thresh<0) {
                besti=i;
                break;
              }
            }
          }
        }
        s += fastest_route(pt,getPt(besti,s));
        still_to_visit -= 1<<besti;
        pt=getPt(besti,s);
        route[step]=besti;
        step++;
        pheromone[last][besti] = (1-alpha) * pheromone[last][besti] + alpha*t0;
        last = besti;
      }
      if (ant==0 || s<bestantscore) {
        bestroute=route;
        bestantscore = s;
      }
    }
    last = n;
    var d = 1/(1+bestantscore);
    for(i=0;i<n;i++) {
      var besti = bestroute[i];
      pheromone[last][besti] = (1-alpha) * pheromone[last][besti] + alpha*d;
      last = besti;
    }
    overallBest = Math.min(overallBest,bestantscore);
  }
  return overallBest;
}

ผล

วิธีการล่าอาณานิคมของมดโดยใช้มดซ้ำ 100 ครั้งจากมด 10 ตัวยังคงเร็วมาก (37ms สำหรับ 16 เป้าหมายเทียบกับ 3700ms สำหรับการค้นหาแบบละเอียด) และดูเหมือนว่าแม่นยำมาก

ตารางด้านล่างแสดงผลลัพธ์สำหรับการทดลอง 10 ครั้งโดยใช้ 16 เป้าหมาย:

   Greedy   Ant     Optimal
   46       29      29
   91       38      37
  103       30      30
   86       29      29
   75       26      22
  182       38      36
  120       31      28
  106       38      30
   93       30      30
  129       39      38

วิธีการมดดูเหมือนดีกว่าโลภมากและมักจะใกล้เคียงที่สุด


ดี. คุณอาจยังไม่ได้ผลลัพธ์ที่ดีที่สุดจากการค้นหาที่ละเอียดถี่ถ้วน (หรืออาจไม่เคยเกิดจากความว่องไวของมัน!) แต่มันเป็นเรื่องน่าสนใจที่จะดูว่าฝูงมดมีขนาดเท่ากระดาน (32x32) ที่มีจำนวนเป้าหมายเท่ากันอย่างไร
timxyz

8

ปัญหาอาจแสดงในรูปของปัญหาพนักงานขายสำหรับการเดินทางทั่วไปจากนั้นจึงแปลงเป็นปัญหาพนักงานขายสำหรับการเดินทางทั่วไป นี่เป็นปัญหาที่ได้รับการศึกษามาเป็นอย่างดี เป็นไปได้ว่าโซลูชันที่มีประสิทธิภาพสูงสุดสำหรับปัญหาของ OP นั้นไม่มีประสิทธิภาพมากกว่าวิธีแก้ปัญหา TSP แต่ก็ไม่แน่นอน (ฉันอาจไม่สามารถใช้ประโยชน์จากบางแง่มุมของโครงสร้างปัญหาของ OP ที่จะช่วยให้สามารถแก้ไขปัญหาได้เร็วขึ้น เช่นลักษณะวัฏจักรของมัน) ทั้งสองวิธีนี้ถือเป็นจุดเริ่มต้นที่ดี

จากC. Noon & J. Bean การเปลี่ยนแปลงอย่างมีประสิทธิภาพของปัญหาพนักงานขายสำหรับการเดินทางทั่วไป :

ทั่วไปพนักงานขายปัญหาการเดินทาง (GTSP) เป็นรูปแบบที่มีประโยชน์สำหรับปัญหาที่เกี่ยวข้องกับการตัดสินใจของการเลือกและลำดับ เวอร์ชันอสมมาตรของปัญหาถูกกำหนดไว้บนกราฟกำกับด้วยโหนด N การเชื่อมต่อส่วนโค้ง A และเวกเตอร์ของต้นทุนส่วนโค้งที่สอดคล้องกัน c โหนดถูกจัดกลุ่มไว้ล่วงหน้าเป็นชุดโหนดที่ไม่ซ้ำกันและครบถ้วนสมบูรณ์ การเชื่อมต่อส่วนโค้งถูกกำหนดระหว่างโหนดที่อยู่ในชุดที่แตกต่างกันเท่านั้นนั่นคือไม่มีส่วนโค้งอินทราเน็ต แต่ละส่วนโค้งที่กำหนดมีต้นทุนที่ไม่เป็นลบที่สอดคล้องกัน GTSP อาจกล่าวได้ว่าเป็นปัญหาในการหาค่าใช้จ่ายขั้นต่ำรอบ M-โค้งซึ่งรวมถึงอีกหนึ่งโหนดจากแต่ละ nodeset

สำหรับปัญหาของ OP:

  • สมาชิกแต่ละคนNเป็นตำแหน่งของปลาเฉพาะในช่วงเวลาใดเวลาหนึ่ง แทนค่านี้ว่าพิกัดกริดอยู่(x, y, t)ที่ไหน(x, y)และtเป็นเวลาที่ปลาจะอยู่ที่พิกัดนี้ สำหรับปลาซ้ายสุดในตัวอย่างของ OP สองสามตัวแรก (แบบ 1 ตัว) ได้แก่(3, 9, 1), (4, 9, 2), (5, 9, 3)เมื่อปลาเคลื่อนที่ไปทางขวา
  • สำหรับสมาชิกใด ๆ ของ N ให้fish(n_i)ส่งคืน ID ของปลาที่แสดงโดยโหนด สำหรับสมาชิกสองคนของ N เราสามารถคำนวณmanhattan(n_i, n_j)หาระยะทางแมนฮัตตันระหว่างสองโหนดและtime(n_i, n_j) สำหรับการชดเชยเวลาระหว่างโหนด
  • จำนวนส่วนย่อยที่ไม่ปะติดปะต่อ m เท่ากับจำนวนปลา เซตเคลื่อนจะมีเพียงโหนดที่S_ifish(n) == i
  • ถ้าสองโหนดiและj fish(n_i) != fish(n_j)แล้วมีส่วนโค้งระหว่างiและjและ
  • ค่าใช้จ่ายระหว่างโหนด i และโหนด j คือtime(n_i, n_j)หรือไม่ได้กำหนดไว้ถ้าtime(n_i, n_j) < distance(n_i, n_j)(กล่าวคือไม่สามารถเข้าถึงตำแหน่งก่อนที่ปลาจะไปถึงที่นั่นอาจเป็นเพราะมันย้อนเวลากลับไป) Arcs ของประเภทหลังนี้สามารถลบออกได้
  • จำเป็นต้องเพิ่มโหนดพิเศษที่แสดงตำแหน่งของผู้เล่นพร้อมส่วนโค้งและค่าใช้จ่ายให้กับโหนดอื่น ๆ ทั้งหมด

การแก้ไขปัญหานี้จะส่งผลให้มีการเยี่ยมชมแต่ละโหนดย่อย (เช่นได้ปลาแต่ละตัวได้ครั้งเดียว) สำหรับเส้นทางที่มีต้นทุนน้อยที่สุด (เช่นใช้เวลาน้อยที่สุดสำหรับการหาปลาทั้งหมด)

บทความนี้อธิบายถึงวิธีการที่สูตรข้างต้นอาจเปลี่ยนเป็นปัญหาพนักงานขายการเดินทางแบบดั้งเดิมและแก้ไขหรือประมาณในภายหลังด้วยเทคนิคที่มีอยู่ ผมยังไม่ได้อ่านผ่านรายละเอียด แต่กระดาษที่ไม่นี้ในวิธีการประกาศให้มีประสิทธิภาพก็คือคนนี้

มีประเด็นที่ชัดเจนเกี่ยวกับความซับซ้อน โดยเฉพาะอย่างยิ่งพื้นที่โหนดไม่มีที่สิ้นสุด! สิ่งนี้สามารถบรรเทาได้โดยการสร้างโหนดขึ้นมาในช่วงเวลาหนึ่งเท่านั้น ถ้าtเป็นจำนวน timesteps เพื่อสร้างโหนดสำหรับและเป็นจำนวนของปลาแล้วขนาดของพื้นที่โหนดจะf t * fโหนดในแต่ละครั้งjจะมีส่วน(f - 1) * (t - j)โค้งขาออกมากที่สุด(เนื่องจากไม่สามารถย้อนเวลากลับไปหรือไปยังส่วนย่อยของตัวเองได้) จำนวนส่วนโค้งทั้งหมดจะอยู่ในลำดับt^2 * f^2ส่วนโค้ง โครงสร้างส่วนโค้งอาจถูกจัดให้เป็นระเบียบเพื่อใช้ประโยชน์จากความจริงที่ว่าเส้นทางของปลาเป็นวัฏจักรในที่สุด ปลาจะทำการกำหนดค่าซ้ำทุกครั้งที่ตัวส่วนร่วมที่ต่ำที่สุดของความยาวรอบของพวกมันดังนั้นอาจจะใช้ข้อเท็จจริงนี้ได้

ฉันไม่รู้เพียงพอเกี่ยวกับ TSP ที่จะบอกว่าสิ่งนี้เป็นไปได้หรือไม่และฉันไม่คิดว่ามันหมายความว่าปัญหาที่โพสต์นั้นจำเป็นต้องเป็นNP-hard ... แต่เป็นแนวทางหนึ่งในการค้นหาวิธีแก้ปัญหาที่เหมาะสมหรือมีขอบเขต .


ขอบคุณนี่เป็นเรื่องใหม่สำหรับฉันและน่าสนใจมาก ฉันคิดว่าฉันควรจะสามารถใช้การเปลี่ยนแปลงนี้ร่วมกับอัลกอริธึม Christofides เพื่อค้นหาวิธีแก้ปัญหาได้อย่างมีประสิทธิภาพภายในค่าประมาณ 3/2 ของค่าที่เหมาะสมที่สุด ถ้าทำให้ใช้งานได้ฉันจะเพิ่มเส้นทางที่ผลิตลงในหน้าสาธิต
Peter de Rivaz

อ่าฉันคิดว่ามีปัญหากับแผนของฉันที่แม้ว่าปัญหาเดิมของฉันจะเป็นกราฟที่สมบูรณ์ซึ่งตอบสนองความไม่เท่าเทียมกันที่เหมาะสมในเมตริก แต่การเปลี่ยนแปลงที่อธิบายไว้จะให้ผลลัพธ์เป็นกราฟที่ไม่สมบูรณ์ดังนั้นอัลกอริทึม Christofides จึงไม่สามารถใช้ได้อีกต่อไป ขอบคุณสำหรับมุมมองที่น่าสนใจ
Peter de Rivaz

ใช่ฉันลืมที่จะพูดถึงความไม่เท่าเทียมกันของสามเหลี่ยมอีกต่อไป เป็นจุดเริ่มต้นที่ดีสำหรับการแก้ปัญหาฮิวริสติกและการประมาณโดยทั่วไปมากขึ้น
timxyz

1

ฉันคิดว่าแนวทางอื่นจะเป็น:

  • คำนวณเส้นทางของเป้าหมาย - คาดการณ์
  • กว่าใช้แผนภาพ Voronoi

อ้างวิกิพีเดีย:

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

ดังนั้นคุณจึงเลือกเป้าหมายทำตามเส้นทางสำหรับขั้นตอนบางอย่างและกำหนดจุดเริ่มต้นที่นั่น ทำเช่นนี้กับเป้าหมายอื่น ๆ ทั้งหมดและคุณจะได้รับแผนภาพโวโรนี ขึ้นอยู่กับว่าคุณอยู่ในพื้นที่ใดคุณย้ายไปยังจุดเริ่มต้นของมัน วิโอล่าคุณได้ปลาตัวแรก ทำซ้ำขั้นตอนนี้จนกว่าคุณจะซื้อทั้งหมด

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.