Làm thế nào để thực hiện một shuffle có trọng số


22

Gần đây tôi đã viết một số mã mà tôi nghĩ rất kém hiệu quả, nhưng vì nó chỉ bao gồm một vài giá trị, tôi đã chấp nhận nó. Tuy nhiên, tôi vẫn quan tâm đến một thuật toán tốt hơn cho các điều sau:

  1. Một danh sách các đối tượng X, mỗi đối tượng được gán một "trọng số"
  2. Tổng trọng lượng
  3. Tạo một số ngẫu nhiên từ 0 đến tổng
  4. Lặp lại qua các đối tượng, trừ đi trọng số của chúng từ tổng cho đến khi tổng không dương
  5. Xóa đối tượng khỏi danh sách, sau đó thêm nó vào cuối danh sách mới

Các mục 2,4 và 5 đều mất nthời gian và do đó, đây là một O(n^2)thuật toán.

Điều này có thể được cải thiện?

Như một ví dụ về shuffle có trọng số, một phần tử có cơ hội lớn hơn ở phía trước với trọng lượng cao hơn.

Ví dụ (Tôi sẽ tạo số ngẫu nhiên để biến nó thành sự thật):

6 đối tượng có trọng lượng 6,5,4,3,2,1; Tổng là 21

Tôi đã chọn 19 19-6-5-4-3-2 = -1:, do đó, 2 đi ở vị trí đầu tiên, trọng lượng hiện là 6,5,4,3,1; Tổng là 19

Tôi đã chọn 16: 16-6-5-4-3 = -2vì vậy 3 đi ở vị trí thứ hai, trọng lượng hiện là 6,5,4,1; Tổng là 16

Tôi đã chọn 3 3-6 = -3:, do đó 6 đi ở vị trí thứ ba, trọng lượng hiện là 5,4,1; Tổng là 10

Tôi chọn 8 8-5-4 = -1:, do đó 4 đi ở vị trí thứ tư, trọng lượng hiện là 5,1; Tổng là 6

Tôi đã chọn 5 5-5=0:, do đó 5 đi ở vị trí thứ năm, trọng lượng bây giờ là 1; Tổng là 1

Tôi đã chọn 1: 1-1=0vì vậy 1 đi ở vị trí cuối cùng, tôi không còn trọng lượng nữa, tôi kết thúc


6
Chính xác thì shuffle có trọng số là gì? Có nghĩa là trọng lượng càng cao, vật thể càng có khả năng ở trên boong tàu?
Doval

Vì tò mò, mục đích của bước nào (5). Có nhiều cách để cải thiện điều này nếu danh sách là tĩnh.
Gort Robot

Vâng, Doval. Tôi xóa mục này khỏi danh sách để nó không xuất hiện trong danh sách được xáo trộn nhiều lần.
Nathan Merrill

Là trọng lượng của một mục trong danh sách không đổi?

Một mặt hàng sẽ có trọng lượng lớn hơn một mặt hàng khác, nhưng mặt hàng X sẽ luôn có cùng trọng lượng. (Rõ ràng, nếu bạn loại bỏ các mặt hàng, trọng lượng lớn hơn sẽ trở nên lớn hơn theo tỷ lệ)
Nathan Merrill

Câu trả lời:


13

Điều này có thể được thực hiện trong O(n log(n))việc sử dụng một cây.

Đầu tiên, tạo cây, giữ cho mỗi nút tổng cộng của tất cả các nút con cháu ở bên phải và bên trái của mỗi nút.

Để lấy mẫu một mục, lấy mẫu đệ quy từ nút gốc, sử dụng tổng tích lũy để quyết định xem bạn trả về nút hiện tại, một nút từ bên trái hay một nút từ bên phải. Mỗi khi bạn lấy mẫu một nút, đặt trọng số của nó thành 0 và cũng cập nhật các nút cha.

Đây là triển khai của tôi trong 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))

Sử dụng:

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_shufflelà một máy phát điện, vì vậy bạn có thể lấy mẫu các kmục hàng đầu một cách hiệu quả. Nếu bạn muốn xáo trộn toàn bộ mảng, chỉ cần lặp qua máy phát cho đến khi cạn kiệt (sử dụng listchức năng).

CẬP NHẬT:

Lấy mẫu ngẫu nhiên có trọng số (2005; Efraimidis, Spirakis) cung cấp một thuật toán rất thanh lịch cho việc này. Việc thực hiện là siêu đơn giản và cũng chạy trong 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]

Bản cập nhật cuối cùng có vẻ tương tự như một giải pháp một lớp lót sai . Bạn có chắc là đúng?
Giacomo Alzetta

19

EDIT: Câu trả lời này không diễn giải các trọng số theo cách sẽ được mong đợi. Tức là một mặt hàng có trọng lượng 2 không gấp đôi khả năng là đầu tiên với trọng lượng 1.

Một cách để xáo trộn danh sách là gán các số ngẫu nhiên cho từng thành phần trong danh sách và sắp xếp theo các số đó. Chúng ta có thể mở rộng ý tưởng đó, chúng ta chỉ cần chọn các số ngẫu nhiên có trọng số. Ví dụ, bạn có thể sử dụng random() * weight. Sự lựa chọn khác nhau sẽ tạo ra sự phân phối khác nhau.

Trong một cái gì đó như Python, điều này sẽ đơn giản như:

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

Hãy cẩn thận rằng bạn không đánh giá các khóa nhiều hơn một lần, vì chúng sẽ kết thúc với các giá trị khác nhau.


2
Đây thực sự là thiên tài do sự đơn giản của nó. Giả sử bạn đang sử dụng thuật toán sắp xếp nlogn, nó sẽ hoạt động tốt.
Nathan Merrill

Trọng lượng của trọng lượng là gì? Nếu chúng cao, các đối tượng được sắp xếp đơn giản theo trọng lượng. Nếu chúng thấp, các đối tượng gần như ngẫu nhiên chỉ với nhiễu loạn nhẹ theo trọng lượng. Dù bằng cách nào, phương pháp này tôi luôn sử dụng, nhưng việc tính toán vị trí sắp xếp có thể sẽ cần một số điều chỉnh.
david.pfx

@ david.pfx Phạm vi của các trọng số phải là phạm vi của các số ngẫu nhiên. Theo cách đó max*min = min*max, và do đó, bất kỳ sự hoán vị nào đều có thể xảy ra, nhưng một số có nhiều khả năng (đặc biệt là nếu các trọng số không được trải đều)
Nathan Merrill

2
Thật ra, cách tiếp cận này là sai! Hãy tưởng tượng trọng số 75 và 25. Trong trường hợp 75, 2/3 thời gian nó sẽ chọn một số> 25. Trong 1/3 thời gian còn lại, nó sẽ "đánh bại" 25 50% thời gian. 75 sẽ là 2/3 đầu tiên (1/3 * 1/2): 83%. Chưa tìm ra cách khắc phục.
Adam Rabung

1
Giải pháp này sẽ hoạt động bằng cách thay thế phân phối đồng đều của mẫu ngẫu nhiên bằng phân phối theo cấp số nhân.
P-Gn

5

Đầu tiên, hãy làm việc từ đó trọng số của một phần tử đã cho trong danh sách được sắp xếp là không đổi. Nó sẽ không thay đổi giữa các lần lặp. Nếu vậy, thì ... tốt, đó là một vấn đề lớn hơn.

Để minh họa, hãy sử dụng một cỗ bài mà chúng ta muốn cân các thẻ mặt lên phía trước. weight(card) = card.rank. Tóm tắt những điều này, nếu chúng ta không biết phân phối trọng số thực sự là O (n) một lần.

Các phần tử này được lưu trữ trong một cấu trúc được sắp xếp, chẳng hạn như sửa đổi trong danh sách bỏ qua có thể lập chỉ mục sao cho tất cả các chỉ mục của các cấp có thể được truy cập từ một nút đã cho:

   1 10
 o ---> o -------------------------------------------- -------------> o Cấp cao nhất
   1 3 2 5
 o ---> o ---------------> o ---------> o ---------------- -----------> o Cấp 3
   1 2 1 2 5
 o ---> o ---------> o ---> o ---------> o ----------------- ----------> o Cấp 2
   1 1 1 1 1 1 1 1 1 1 1 
 o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o Cấp dưới

Trưởng 1 2 3 4 5 6 6 8 9 9 NIL
      Nút Node Nút Node Nút Node Nút Nút

Tuy nhiên, trong trường hợp này, mỗi nút cũng 'chiếm' không gian bằng trọng lượng của nó.

Bây giờ, khi tra cứu một thẻ trong danh sách này, người ta có thể truy cập vị trí của nó trong danh sách trong thời gian O (log n) và xóa nó khỏi danh sách liên quan trong thời gian O (1). Ok, nó có thể không phải là O (1), nó có thể là thời gian O (log log n) (tôi phải suy nghĩ về điều này nhiều hơn nữa). Loại bỏ nút thứ 6 trong ví dụ trên sẽ liên quan đến việc cập nhật cả bốn cấp độ - và bốn cấp độ đó độc lập với số lượng phần tử có trong danh sách (tùy thuộc vào cách bạn triển khai các cấp độ).

Vì trọng lượng của một phần tử là không đổi, người ta có thể chỉ cần làm sum -= weight(removed)mà không phải đi qua cấu trúc một lần nữa.

Và do đó, bạn đã có chi phí một lần là O (n) và giá trị tra cứu là O (log n) và xóa khỏi chi phí danh sách của O (1). Điều này trở thành O (n) + n * O (log n) + n * O (1) mang lại cho bạn hiệu suất tổng thể của O (n log n).


Hãy xem xét điều này bằng thẻ, vì đó là những gì tôi đã sử dụng ở trên.

      10
top 3 -----------------------> 4d
                                .
       3 7.
    2 ---------> 2ngày ---------> 4ngày
                  . .
       1 2. 3 4.
bot 1 -> Quảng cáo -> 2d -> 3d -> 4d

Đây là một bộ bài thực sự nhỏ chỉ có 4 lá bài trong đó. Nó sẽ dễ dàng để xem làm thế nào điều này có thể được mở rộng. Với 52 thẻ, một cấu trúc lý tưởng sẽ có 6 cấp độ (log 2 (52) ~ = 6), mặc dù nếu bạn đào sâu vào danh sách bỏ qua, thậm chí có thể giảm xuống một số nhỏ hơn.

Tổng của tất cả các trọng số là 10. Vì vậy, bạn nhận được một số ngẫu nhiên từ [1 .. 10) và 4 Bạn đi qua danh sách bỏ qua để tìm vật phẩm ở trần (4). Vì 4 nhỏ hơn 10, bạn chuyển từ cấp cao nhất sang cấp thứ hai. Bốn là lớn hơn 3, vì vậy bây giờ chúng ta đang ở 2 viên kim cương. 4 nhỏ hơn 3 + 7, vì vậy chúng tôi di chuyển xuống tầng dưới và 4 nhỏ hơn 3 + 3, vì vậy chúng tôi đã có 3 viên kim cương.

Sau khi loại bỏ 3 viên kim cương khỏi cấu trúc, cấu trúc bây giờ trông như sau:

       7
đầu 3 ----------------> 4d
                         .
       3 4.
    2 ---------> 2ngày -> 4đ
                  . .
       1 2. 4 .
bot 1 -> Quảng cáo -> 2d -> 4d

Bạn sẽ lưu ý rằng các nút chiếm một lượng 'không gian' tỷ lệ thuận với trọng lượng của chúng trong cấu trúc. Điều này cho phép lựa chọn trọng số.

Vì cây này xấp xỉ một cây nhị phân cân bằng, việc tra cứu trong này không cần phải đi lớp dưới cùng (sẽ là O (n)) và thay vào đó, từ trên xuống cho phép bạn nhanh chóng bỏ qua cấu trúc để tìm về những gì bạn đang tìm kiếm cho

Thay vào đó, phần lớn có thể được thực hiện với một số loại cây cân bằng. Vấn đề ở đây là sự cân bằng lại cấu trúc khi một nút bị loại bỏ gây nhầm lẫn vì đây không phải là cấu trúc cây cổ điển và người quản gia phải nhớ rằng 4 viên kim cương giờ đã được chuyển từ vị trí [6 7 8 9] sang [3 4 5 6] có thể chi phí nhiều hơn lợi ích của cấu trúc cây.

Tuy nhiên, trong khi danh sách bỏ qua xấp xỉ cây nhị phân trong khả năng bỏ qua danh sách trong thời gian O (log n), thì nó lại có tính đơn giản khi làm việc với danh sách được liên kết thay thế.

Điều này không phải để nói rằng nó là dễ dàng thực hiện tất cả điều này (bạn vẫn cần giữ các tab trên tất cả các liên kết bạn cần sửa đổi khi xóa phần tử), nhưng điều đó có nghĩa là chỉ cập nhật theo nhiều cấp độ bạn có và liên kết của chúng chứ hơn tất cả mọi thứ ở bên phải trên cấu trúc cây thích hợp.


Tôi không chắc chắn như thế nào những gì bạn đang mô tả các trận đấu một danh sách Skip (nhưng sau đó, tôi đã chỉ cần nhìn danh sách lên bỏ qua). Từ những gì tôi hiểu trên Wikipedia, trọng số cao hơn sẽ nghiêng về bên phải nhiều hơn trọng lượng thấp hơn. Tuy nhiên, bạn đang mô tả rằng chiều rộng của bỏ qua nên là trọng lượng. Một câu hỏi khác ... sử dụng cấu trúc này, làm thế nào để bạn chọn một yếu tố ngẫu nhiên?
Nathan Merrill

1
@MrTi do đó sửa đổi về ý tưởng của một danh sách bỏ qua có thể lập chỉ mục. Điều quan trọng là có thể truy cập vào phần tử tại đó trọng số của các phần tử trước đó được tính tổng là <23 trong thời gian O (log n) thay vì thời gian O (n). Bạn vẫn chọn phần tử ngẫu nhiên theo cách bạn mô tả, chọn một số ngẫu nhiên từ [0, tổng (trọng số)] và sau đó lấy phần tử tương ứng từ danh sách. Không có vấn đề gì về thứ tự các nút / thẻ trong danh sách bỏ qua - bởi vì 'không gian' lớn hơn được lấy bởi các vật nặng hơn là chìa khóa.

À, tôi hiểu rồi. Tôi thích nó.
Nathan Merrill
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.