วิธีใช้การสลับแบบถ่วงน้ำหนัก


22

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

  1. รายการของวัตถุ X แต่ละชิ้นจะถูกกำหนดเป็น "น้ำหนัก"
  2. สรุปน้ำหนัก
  3. สร้างตัวเลขสุ่มจาก 0 ถึงผลรวม
  4. วนซ้ำผ่านวัตถุลบน้ำหนักออกจากผลรวมจนกว่าผลรวมจะไม่เป็นบวก
  5. เอาวัตถุออกจากรายการแล้วเพิ่มลงในส่วนท้ายของรายการใหม่

รายการ 2,4 และ 5 ต้องใช้nเวลาดังนั้นมันจึงเป็นO(n^2)อัลกอริธึม

สามารถปรับปรุงได้ไหม

ตัวอย่างของการสลับแบบถ่วงน้ำหนักองค์ประกอบมีโอกาสมากขึ้นที่จะอยู่ข้างหน้าด้วยน้ำหนักที่สูงขึ้น

ตัวอย่าง (ฉันจะสร้างตัวเลขสุ่มเพื่อทำให้เป็นจริง):

6 สิ่งของที่มีน้ำหนัก 6,5,4,3,2,1; ผลรวมคือ 21

ฉันเลือก 19: 19-6-5-4-3-2 = -1ดังนั้น 2 ไปในตำแหน่งแรกน้ำหนักตอนนี้ 6,5,4,3,1; ผลรวมคือ 19

ฉันเลือก 16: 16-6-5-4-3 = -2ดังนั้น 3 ไปในตำแหน่งที่สองตอนนี้น้ำหนักอยู่ที่ 6,5,4,1; ผลรวมคือ 16

ฉันเลือก 3: 3-6 = -3ดังนั้น 6 ไปในตำแหน่งที่สามตอนนี้น้ำหนักอยู่ที่ 5,4,1; ผลรวมคือ 10

ฉันเลือก 8: 8-5-4 = -1ดังนั้น 4 ไปที่ตำแหน่งที่สี่ตอนนี้น้ำหนักอยู่ที่ 5,1; ผลรวมคือ 6

ฉันเลือก 5: 5-5=0ดังนั้น 5 ไปในตำแหน่งที่ห้าน้ำหนักตอนนี้ 1; ผลรวมคือ 1

ฉันเลือก 1: 1-1=0ดังนั้น 1 ไปที่ตำแหน่งสุดท้ายฉันไม่มีน้ำหนักอีกต่อไปฉันเสร็จแล้ว


6
การสลับแบบถ่วงน้ำหนักคืออะไร มันหมายความว่ายิ่งมีน้ำหนักมากเท่าไหร่วัตถุก็จะยิ่งอยู่ด้านบนของสำรับมากขึ้นเท่านั้น?
Doval

จากความอยากรู้จุดประสงค์ของขั้นตอนคืออะไร (5) มีวิธีการปรับปรุงนี้หากรายการคงที่
Gort the Robot

ใช่ Doval ฉันลบรายการออกจากรายการจึงไม่ปรากฏในรายการที่สับมากกว่าหนึ่งครั้ง
Nathan Merrill

น้ำหนักของรายการในรายการคงที่หรือไม่?

รายการหนึ่งจะมีน้ำหนักที่ใหญ่กว่าอีกรายการหนึ่ง แต่รายการ X จะมีน้ำหนักเท่ากันเสมอ (แน่นอนถ้าคุณลบรายการน้ำหนักที่ใหญ่ขึ้นจะกลายเป็นสัดส่วนที่ใหญ่กว่า)
นาธานเมอร์ริลล์

คำตอบ:


13

สิ่งนี้สามารถนำมาใช้ในการO(n log(n))ใช้ต้นไม้

ขั้นแรกสร้างทรีโดยเก็บผลรวมสะสมของโหนดสืบทอดทั้งหมดในแต่ละโหนดทางด้านขวาและด้านซ้ายของแต่ละโหนด

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

นี่คือการใช้งานของฉันใน Python:

import random

def weigthed_shuffle(items, weights):
    if len(items) != len(weights):
        raise ValueError("Unequal lengths")

    n = len(items)
    nodes = [None for _ in range(n)]

    def left_index(i):
        return 2 * i + 1

    def right_index(i):
        return 2 * i + 2

    def total_weight(i=0):
        if i >= n:
            return 0
        this_weigth = weights[i]
        if this_weigth <= 0:
            raise ValueError("Weigth can't be zero or negative")
        left_weigth = total_weight(left_index(i))
        right_weigth = total_weight(right_index(i))
        nodes[i] = [this_weigth, left_weigth, right_weigth]
        return this_weigth + left_weigth + right_weigth

    def sample(i=0):
        this_w, left_w, right_w = nodes[i]
        total = this_w + left_w + right_w
        r = total * random.random()
        if r < this_w:
            nodes[i][0] = 0
            return i
        elif r < this_w + left_w:
            chosen = sample(left_index(i))
            nodes[i][1] -= weights[chosen]
            return chosen
        else:
            chosen = sample(right_index(i))
            nodes[i][2] -= weights[chosen]
            return chosen

    total_weight() # build nodes tree

    return (items[sample()] for _ in range(n - 1))

การใช้งาน:

In [2]: items = list(range(10))
   ...: weights = list(range(10, 0, -1))
   ...:

In [3]: for _ in range(10):
   ...:     print(list(weigthed_shuffle(items, weights)))
   ...:
[5, 0, 8, 6, 7, 2, 3, 1, 4]
[1, 2, 5, 7, 3, 6, 9, 0, 4]
[1, 0, 2, 6, 8, 3, 7, 5, 4]
[4, 6, 8, 1, 2, 0, 3, 9, 7]
[3, 5, 1, 0, 4, 7, 2, 6, 8]
[3, 7, 1, 2, 0, 5, 6, 4, 8]
[1, 4, 8, 2, 6, 3, 0, 9, 5]
[3, 5, 0, 4, 2, 6, 1, 8, 9]
[6, 3, 5, 0, 1, 2, 4, 8, 7]
[4, 1, 2, 0, 3, 8, 6, 5, 7]

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

UPDATE:

การสุ่มตัวอย่างแบบถ่วงน้ำหนัก (2005; Efraimidis, Spirakis)มอบอัลกอริทึมที่หรูหรามากสำหรับสิ่งนี้ การใช้งานนั้นง่ายมากและยังทำงานในO(n log(n)):

def weigthed_shuffle(items, weights):
    order = sorted(range(len(items)), key=lambda i: -random.random() ** (1.0 / weights[i]))
    return [items[i] for i in order]

การปรับปรุงที่ผ่านมาดูเหมือนว่าขนคล้ายกับวิธีการแก้ปัญหาหนึ่งซับที่ไม่ถูกต้อง คุณแน่ใจหรือว่าถูกต้อง?
Giacomo Alzetta

19

แก้ไข:คำตอบนี้ไม่ได้แปลความหมายของน้ำหนักในแบบที่ควรจะเป็น เช่นรายการที่มีน้ำหนัก 2 ไม่น่าจะเป็นสองเท่าเป็นอันดับแรกที่มีน้ำหนัก 1

วิธีหนึ่งในการสลับรายการคือการกำหนดหมายเลขสุ่มให้แต่ละองค์ประกอบในรายการและเรียงลำดับตามหมายเลขเหล่านั้น เราสามารถขยายความคิดนั้นเราแค่ต้องเลือกตัวเลขแบบสุ่มถ่วงน้ำหนัก random() * weightตัวอย่างเช่นคุณสามารถใช้ ตัวเลือกที่ต่างกันจะทำให้เกิดการแจกแจงที่แตกต่างกัน

ในบางสิ่งบางอย่างเช่น Python สิ่งนี้ควรจะง่ายเหมือน:

items.sort(key = lambda item: random.random() * item.weight)

ระวังว่าคุณจะไม่ประเมินคีย์มากกว่าหนึ่งครั้งเพราะมันจะจบลงด้วยค่าที่ต่างกัน


2
นี่คืออัจฉริยะอย่างแท้จริงเนื่องจากความเรียบง่าย สมมติว่าคุณกำลังใช้อัลกอริธึมการจัดเรียง nlogn นี่จะทำงานได้ดี
Nathan Merrill

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

@ david.pfx ช่วงของน้ำหนักควรเป็นช่วงของตัวเลขสุ่ม ด้วยวิธีmax*min = min*maxนี้และดังนั้นการเปลี่ยนแปลงใด ๆ ที่เป็นไปได้ แต่บางคนก็มีแนวโน้มมากขึ้น (โดยเฉพาะอย่างยิ่งหากน้ำหนักไม่กระจายเท่า ๆ กัน)
นาธานเมอร์ริลล์

2
จริงๆแล้ววิธีนี้ผิด! ลองนึกภาพน้ำหนัก 75 และ 25 สำหรับกรณี 75, 2/3 ของเวลาที่จะเลือกตัวเลข> 25 สำหรับเวลาที่เหลือ 1/3 ของเวลามันจะ "ชนะ" 25% 50% ของเวลา 75 จะเป็น 2/3 + ครั้งแรก (1/3 * 1/2) ของเวลา: 83% ยังไม่ได้แก้ไขปัญหา
Adam Rabung

1
โซลูชันนี้ควรทำงานโดยแทนที่การกระจายตัวอย่างของการสุ่มตัวอย่างโดยการแจกแจงเอ็กซ์โพเนนเชียล
P-Gn

5

ขั้นแรกให้ทำงานจากน้ำหนักขององค์ประกอบที่กำหนดในรายการที่จะเรียงเป็นค่าคงที่ มันจะไม่เปลี่ยนแปลงระหว่างการวนซ้ำ ถ้าเป็นเช่นนั้น ... ก็เป็นปัญหาที่ใหญ่กว่า

สำหรับภาพประกอบให้ใช้ไพ่สำรับที่เราต้องการให้น้ำหนักการ์ดใบหน้าด้านหน้า weight(card) = card.rank. ข้อสรุปเหล่านี้หากเราไม่ทราบว่าการกระจายน้ำหนักนั้นแน่นอน O (n) ครั้งเดียว

องค์ประกอบเหล่านี้ถูกเก็บไว้ในโครงสร้างที่มีการเรียงลำดับเช่นการปรับเปลี่ยนในรายการข้ามที่สามารถทำดัชนีได้ซึ่งดัชนีทั้งหมดของระดับสามารถเข้าถึงได้จากโหนดที่กำหนด:

   1 10
 o ---> o -------------------------------------------- -------------> o ระดับสูงสุด
   1 3 2 5
 o ---> o ---------------> o ---------> o ---------------- -----------> o ระดับ 3
   1 2 1 2 5
 o ---> o ---------> o ---> o ---------> o ----------------- ----------> o ระดับ 2
   1 1 1 1 1 1 1 1 1 1 1 1 
 o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ระดับล่างสุด

หัวหน้า 1 2 3 4 5 6 7 8 9 9 10 NIL
      โหนดโหนดโหนดโหนดโหนดโหนดโหนดโหนดโหนดโหนด

อย่างไรก็ตามในกรณีนี้แต่ละโหนดยังมีพื้นที่มากพอ ๆ กับน้ำหนักของมัน

ตอนนี้เมื่อค้นหาการ์ดในรายการนี้หนึ่งสามารถเข้าถึงตำแหน่งในรายการในเวลา O (log n) และลบออกจากรายการที่เกี่ยวข้องในเวลา O (1) ตกลงมันอาจจะไม่ใช่ O (1) มันอาจเป็นเวลา O (บันทึกการใช้งาน n) (ฉันต้องคิดมากกว่านี้อีก) การลบโหนดที่ 6 ในตัวอย่างด้านบนจะเกี่ยวข้องกับการอัปเดตทั้งสี่ระดับ - และสี่ระดับนั้นไม่ขึ้นอยู่กับจำนวนองค์ประกอบที่มีในรายการ (ขึ้นอยู่กับวิธีที่คุณใช้ระดับ)

เนื่องจากน้ำหนักขององค์ประกอบคงที่เราสามารถทำได้sum -= weight(removed)โดยไม่ต้องเข้าไปสำรวจโครงสร้างอีกครั้ง

ด้วยเหตุนี้คุณจึงมีค่าใช้จ่ายครั้งเดียวที่ O (n) และค่าการค้นหา O (log n) และค่าลบออกจากรายการ O (1) สิ่งนี้จะกลายเป็น O (n) + n * O (log n) + n * O (1) ซึ่งให้ประสิทธิภาพโดยรวมของ O (n log n)


ให้ดูที่นี้กับการ์ดเพราะนั่นคือสิ่งที่ฉันใช้ข้างต้น

      10
ด้านบน 3 -----------------------> 4d
                                .
       3 7.
    2 ---------> 2d ---------> 4d
                  . .
       1 2. 3 4.
bot 1 -> โฆษณา -> 2d -> 3d -> 4d

นี่คือจริงๆดาดฟ้าขนาดเล็กที่มีเพียง 4 การ์ดในนั้น มันควรจะง่ายต่อการดูว่าสิ่งนี้สามารถขยายได้ ด้วยไพ่ 52 ใบโครงสร้างในอุดมคติจะมี 6 ระดับ (บันทึก2 (52) ~ = 6) แม้ว่าคุณจะขุดลงในรายการข้ามแม้ว่ามันจะลดลงเหลือจำนวนน้อยก็ตาม

ผลรวมของน้ำหนักทั้งหมดคือ 10 ดังนั้นคุณจะได้ตัวเลขสุ่มจาก [1 .. 10) และ4ของมัน คุณเดินรายการข้ามเพื่อค้นหารายการที่อยู่บนเพดาน (4) เนื่องจาก 4 มีค่าน้อยกว่า 10 คุณจึงย้ายจากระดับสูงสุดเป็นระดับที่สอง สี่มีค่ามากกว่า 3 ดังนั้นตอนนี้เราอยู่ที่ 2 ของเพชร 4 น้อยกว่า 3 + 7 ดังนั้นเราเลื่อนลงไปที่ระดับล่างสุดและ 4 น้อยกว่า 3 + 3 ดังนั้นเราจึงได้เพชร 3 เม็ด

หลังจากลบเพชร 3 ตัวออกจากโครงสร้างตอนนี้โครงสร้างจะมีลักษณะดังนี้:

       7
ด้านบน 3 ----------------> 4d
                         .
       3 4.
    2 ---------> 2d -> 4d
                  . .
       1 2. 4.
บอท 1 -> โฆษณา -> 2d -> 4d

คุณจะทราบว่าโหนดใช้ปริมาณ 'พื้นที่' เป็นสัดส่วนกับน้ำหนักในโครงสร้าง สิ่งนี้ทำให้สามารถเลือกน้ำหนักได้

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

สิ่งนี้สามารถทำได้กับต้นไม้ที่สมดุล ปัญหาที่เกิดขึ้นคือการปรับสมดุลของโครงสร้างเมื่อโหนดถูกลบออกทำให้เกิดความสับสนเนื่องจากนี่ไม่ใช่โครงสร้างต้นไม้แบบดั้งเดิมและพนักงานทำความสะอาดจะจำได้ว่าตอนนี้เพชร 4 เม็ดถูกย้ายจากตำแหน่ง [6 7 8 9] เป็น [3 4] 5] อาจมีราคาสูงกว่าประโยชน์ของโครงสร้างต้นไม้

อย่างไรก็ตามในขณะที่รายการข้ามมีค่าใกล้เคียงกับต้นไม้ไบนารีในความสามารถในการข้ามรายการในเวลา O (log n) แต่ก็มีความเรียบง่ายในการทำงานกับรายการที่เชื่อมโยงแทน

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


ฉันไม่แน่ใจว่าสิ่งที่คุณกำลังอธิบายการแข่งขันรายการข้าม ( แต่นั้นมาผมไม่เพียงแค่มองขึ้นเฮี๊ยบรายการ) จากสิ่งที่ฉันเข้าใจใน Wikipedia น้ำหนักที่สูงขึ้นจะเป็นทางขวามากกว่าน้ำหนักที่ต่ำกว่า อย่างไรก็ตามคุณกำลังอธิบายว่าความกว้างของการข้ามควรเป็นน้ำหนัก อีกหนึ่งคำถาม ... โดยใช้โครงสร้างนี้คุณจะเลือกองค์ประกอบแบบสุ่มได้อย่างไร
Nathan Merrill

1
@MrTi ดังนั้นการปรับเปลี่ยนแนวคิดของรายการข้ามที่จัดทำดัชนีได้ กุญแจสำคัญคือการสามารถเข้าถึงองค์ประกอบที่น้ำหนักขององค์ประกอบก่อนหน้านี้รวมกับ <23 ในเวลา O (log n) มากกว่าเวลา O (n) คุณยังคงเลือกองค์ประกอบสุ่มในแบบที่คุณอธิบายเลือกหมายเลขสุ่มจาก [0, ผลรวม (น้ำหนัก)] แล้วรับองค์ประกอบที่เกี่ยวข้องจากรายการ ไม่สำคัญว่าลำดับใดที่โหนด / การ์ดจะอยู่ในรายการข้าม - เนื่องจากช่องว่างที่ใหญ่กว่าของรายการที่มีน้ำหนักมากเป็นกุญแจสำคัญ

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