8.5 隨機與取樣
384. Shuffle an Array
題目描述
給定一個陣列,要求實現兩個指令函數。第一個函數「shuffle」可以隨機打亂這個陣列,第二個函數「reset」可以恢復原來的順序。
輸入輸出範例
輸入是一個存有整數數字的陣列,以及一個包含指令函數名稱的陣列。輸出是一個二維陣列,表示每個指令生成的陣列。
Input: nums = [1,2,3], actions: ["shuffle","shuffle","reset"]
Output: [[2,1,3],[3,2,1],[1,2,3]]
在這個範例中,前兩次打亂的結果只要是隨機生成即可。
題解
我們採用經典的 Fisher-Yates 洗牌算法
,其原理是通過隨機交換位置來實現隨機打亂,有正向和反向兩種寫法,且實現非常方便。注意這裡「reset」函數以及 Solution 類的構造函數的實現細節。
- C++
- Python
class Solution {
public:
Solution(vector<int> nums) : nums_(nums) {}
vector<int> reset() { return nums_; }
vector<int> shuffle() {
vector<int> shuffled(nums_);
int n = nums_.size();
// 可以使用反向或者正向洗牌,效果相同。
// 反向洗牌:
for (int i = n - 1; i >= 0; --i) {
swap(shuffled[i], shuffled[rand() % (i + 1)]);
}
// 正向洗牌:
// for (int i = 0; i < n; ++i) {
// int pos = rand() % (n - i);
// swap(shuffled[i], shuffled[i+pos]);
// }
return shuffled;
}
private:
vector<int> nums_;
};
class Solution:
def __init__(self, nums: List[int]):
self.base = nums[:]
def reset(self) -> List[int]:
return self.base[:]
def shuffle(self) -> List[int]:
shuffled = self.base[:]
n = len(self.base)
# 可以使用反向或者正向洗牌,效果相同。
# 反向洗牌:
for i in range(n - 1, -1, -1):
j = random.randint(0, i)
shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
# 正向洗牌:
# for i in range(n):
# j = i + random.randint(0, n - i - 1)
# shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
return shuffled
528. Random Pick with Weight
題目描述
給定一個陣列,陣列每個位置的值表示該位置的權重,要求按照權重的概率去隨機抽樣。
輸入輸出範例
輸入是一維正整數陣列,表示權重;以及一個包含指令字串的一維陣列,表示運行幾次隨機抽樣。輸出是一維整數陣列,表示隨機抽樣的整數在陣列中的位置。
Input: weights = [1,3], actions: ["pickIndex","pickIndex","pickIndex"]
Output: [0,1,1]
在這個範例中,每次選擇的位置都是不確定的,但 選擇第 0 個位置的期望為 1/4,選擇第 1 個位置的期望為 3/4。
題解
我們可以先使用前綴和(partial_sum)計算到每個位置為止之前所有數字的總和,這個結果對於正整數陣列是單調遞增的。每當需要抽樣時,我們可以先隨機生成一個數字,然後使用二分法查找該數字在前綴和中的位置,以模擬加權抽樣的過程。這裡的二分法可以用 lower_bound 實現。
以範例為例,權重陣列 [1,3] 的前綴和為 [1,4]。如果我們隨機生成的數字為 1,那麼 lower_bound 返回的位置為 0;如果我們隨機生成的數字是 2、3、4,那麼 lower_bound 返回的位置為 1。
關於前綴和的更多技巧,我們將在接下來的章節中繼續深入講解。
- C++
- Python
class Solution {
public:
Solution(vector<int> weights) : cumsum_(weights) {
partial_sum(cumsum_.begin(), cumsum_.end(), cumsum_.begin());
}
int pickIndex() {
int val = (rand() % cumsum_.back()) + 1;
return lower_bound(cumsum_.begin(), cumsum_.end(), val) -
cumsum_.begin();
}
private:
vector<int> cumsum_;
};
class Solution:
def __init__(self, weights: List[int]):
self.cumsum = weights[:]
for i in range(1, len(weights)):
self.cumsum[i] += self.cumsum[i - 1]
def pickIndex(self) -> int:
val = random.randint(1, self.cumsum[-1])
return bisect.bisect_left(self.cumsum, val, 0, len(self.cumsum))
382. Linked List Random Node
題目描述
給定一個單向鏈結串列,要求設計一個演算法,可以隨機取得其中的一個數字。
輸入輸出範例
輸入是一個單向鏈結串列,輸出是一個數字,表示鏈結串列裡其中一個節點的值。
Input: 1->2->3->4->5
Output: 3
在這個範例中,我們有均等的概率得到任意一個節點,比如 3。
題解
不同於陣列,在未遍歷完鏈結串列前,我們無法知道鏈結串列的總長 度。這裡我們可以使用水庫抽樣:遍歷一次鏈結串列,在遍歷到第 個節點時,有 的概率選擇這個節點覆蓋掉之前的選擇。
我們提供一個簡單的,對於水庫算法隨機性的證明。對於長度為 的鏈結串列的第 個節點,最後被抽樣的充要條件是它被選擇,且之後的節點都沒有被選擇。這種情況發生的概率為:
因此每個點都有均等的概率被選擇。
當然,這道題我們也可以預處理鏈結串列,遍歷一遍之後把它轉化成陣列。
- C++
- Python
class Solution {
public:
Solution(ListNode* node) : head_(node) {}
int getRandom() {
int pick = head_->val;
ListNode* node = head_->next;
int i = 2;
while (node) {
if (rand() % i == 0) {
pick = node->val;
}
++i;
node = node->next;
}
return pick;
}
private:
ListNode* head_;
};
class Solution:
def __init__(self, head: Optional[ListNode]):
self.head = head
def getRandom(self) -> int:
pick = self.head.val
node = self.head.next
i = 2
while node is not None:
if random.randint(0, i - 1) == 0:
pick = node.val
i += 1
node = node.next
return pick