题目来源
题目描述
class Solution {
public:
Solution(vector<int>& nums) {
}
int pick(int target) {
}
};
题目解析
水塘抽样
这道题可以看成是从某个范围中随机挑选一个元素。这里说的随机是均匀随机(uniform random),也就是说,如果有 n 个元素,每个元素被选中的概率都是 1/n,不可以有统计意义上的偏差。
- 因为可能能含有重复元素,所以不能map
- 因为要求不能用太多的空间,那么省空间的随机方法只有水塘抽样
- 具体的,我们在每次pick时对流进行遍历,由于数据流很大,我们不能在遍历过程中使用比如数组之类的容器存储所有满足条件的下标,所以只能对每个
nums[i] == target
执行[是否要将i作为最新答案候选]的操作 - 假设共有m个下标满足
nums[i] == target
,我们需要做到以1/m概率返回任意坐标 - 我们规定当遇到第k个满足nums[i]==target作为下标时,执行一次
[0, k)
的随机操作,当随机结果为0时(发生概率为1/k),我们将该坐标作为最新的答案候选。 - 当对每一个 nums[i] = targetnums[i]=target 的下标都进行上述操作后,容易证明每一位下标返回的概率均为1/m
算法:遍历nums,当我们第i次遇到值为target的元素时,随机选择区间[0, i)
内的一个整数,如果其等于0,则将返回值置为该元素的下标,否则返回值不变
class Solution {
std::vector<int> vec;
public:
Solution(vector<int>& nums) : vec(nums) {
}
int pick(int target) {
int cnt = 0, ans = -1;
for (int i = 0; i < vec.size(); ++i) {
if(vec[i] != target){
continue;
}
++cnt;
if(rand() % cnt == 0){
ans = i;
}
}
return ans;
}
};
水塘抽样
用水塘抽样的方法随机抽取一个元素
- 难点在于,随机选择是[动态]的,比如说你现在有5个元素,你已经随机选取了其中的某个元素a作为结果,但是现在再给你一个新元素b,你应该留着a还是留着b呢?以什么逻辑选择a或者b呢?怎么证明你的选择方法在概率上是公平的呢?
- 结论是:当你遇到第i个元素时,应该有1/i的概率选择该元素,1 - 1/i的概率保持原有的选择。
证明:
- 假设总共有 n 个元素,我们要的随机性无非就是每个元素被选择的概率都是
1/n
,那么对于第 i 个元素,它被选择的概率就是:
- 第 i 个元素被选择的概率是 1/i,第 i+1 次不被替换的概率是 1 - 1/(i+1),以此类推,相乘就是第 i 个元素最终被选中的概率,就是 1/n。
哈希表(预处理)
如果不考虑数组的大小,我们可以在构造函数中,用一个哈希表pos记录nums中相同元素的下标。
对于pick操作,我们可以从pos中取出target对应的下标列表,然后随机选择一个下标并返回
class Solution {
unordered_map<int, vector<int>> pos;
public:
Solution(vector<int> &nums) {
for (int i = 0; i < nums.size(); ++i) {
pos[nums[i]].push_back(i);
}
}
int pick(int target) {
auto &indices = pos[target];
return indices[rand() % indices.size()];
}
};