[总结]随机抽样与蓄水池抽样问题

现实中碰到很多需要随机抽样的问题,这也是算法工程师面试中常见的题型,特意记录在这里。下面以几个例题为例,展开随机抽样问题的解决方案。

[leetcode]470.Implement Rand10() Using Rand7()

已提供一个Rand7()的API可以随机生成1到7的数字,使用Rand7实现Rand10,Rand10可以随机生成1到10的数字。可以通过拒绝采样的方法来计算:用Rand49生成一个数,如果它位于41-49,则丢弃,继续生成,当生成一个1-40的数时返回。这样子做可以近似看成随机生成1-40之间的数。

class Solution:
    def rand10(self):
        """
        :rtype: int
        """
        while True:
            val = (rand7() - 1) * 7 + rand7()  #减一是为了从0开始
            if(val <= 40):
                break
        return (val-1) % 10 + 1
[leetcode]478.Generate Random Point in a Circle

拒绝采样(Rejection Sampling)。对于圆中任意小的面积内落入点的概率相等。注意刚才说的是任意面积落点的概率是相等的。而如果采用随机半径+随机角度的方式,那么在任意半径上落入点的概率相等。很明显的是靠近圆心的半径比较密,远离圆心的时候半径比较稀疏。

class Solution:
    def __init__(self, radius: float, x_center: float, y_center: float):
        self.r = radius
        self.x = x_center
        self.y = y_center

    def randPoint(self) -> List[float]:
        nr = math.sqrt(random.random()) * self.r
        alpha = random.random() * 2 * 3.141592653
        newx = self.x + nr * math.cos(alpha)
        newy = self.y + nr * math.sin(alpha)
        return [newx, newy]
[leetcode]519.Random Flip Matrix # Fisher-Yates Shuffle算法,利用dict字典映射 取前20个数字?

给定一个全零矩阵的行和列,实现flip函数随机把一个0变成1并返回索引,实现rest函数将所有数归零。
使用Fisher-Yates Shuffle算法,Fisher-Yates洗牌算法是用来打乱一个随机序列的算法,主要步骤为:在0到n(索引)之间生成一个数m,交换m和n(索引对应的数),n(索引)减掉1,循环这三步,直到n等于0。主要思想就是每次采样(索引)时,当前随机采样到的数(索引对应的数)交换到最后一个数(末尾索引对应的数),然后采样池数量减一(末尾索引减一),然后继续采样和交换(不断迭代),直到采样池为空。

import random
class Solution(object):
    def __init__(self, n_rows, n_cols):
        self.n_rows, self.n_cols = n_rows, n_cols
        self.reset()

    def flip(self):
        self.n -= 1
        i = random.randrange(0, self.n+1)
        index = self.dic.get(i, i)
        self.dic[i] = self.dic.get(self.n, self.n)
        return [index // self.n_cols, index % self.n_cols]

    def reset(self):
        self.n = self.n_rows * self.n_cols
        self.dic = {}

在这个题中不能直接使用数组进行这个过程的模拟,内存不够。所以,使用一个字典保存已经被随机数选择过的位置,把这个位置和末尾的total交换的实现方式是使用字典保存这个位置交换成了末尾的那个数字。每次随机到一个数字,然后在字典中查,如果这个数字不在字典中,表示这个数字还没被选中过,那么就直接返回这个数字,把这个数字和末尾数字交换;如果随机数已经在字典中出现过,那么说明这个位置已经被选中过,使用字典里保存的交换后的数字返回。

[leetcode]528.Random Pick with Weight

把概率分布函数转化为累计概率分布函数。然后通过随机数,进行二分查找。
比如,输入是[1,2,3,4],那么概率分布是[1/10, 2/10, 3/10, 4/10, 5/10],累积概率分布是[1/10, 3/10, 6/10, 10/10].总和是10。如果我们产生一个随机数,在1~10之中,然后判断这个数字在哪个区间中就能得到对应的索引。
对于输入[1,2,3,4],计算出来的preSum是[1,3,6,10],然后随机选一个s,然后查找s属于哪个区间。

import random
class Solution:
    def __init__(self, w: List[int]):
        self.cursum = [0]
        for weight in w:
            self.cursum.append(self.cursum[-1]+weight)

    def pickIndex(self) -> int:
        r = random.randrange(0,self.cursum[-1])
        # r = random.random()*self.cursum[-1]
        return self.gethi(self.cursum,r)

    def gethi(self,nums,target):
        lo,hi = 0,len(nums)-1
        while lo<=hi:
            mid = lo + ((hi-lo)>>1)
            if target < nums[mid]:
                hi = mid-1
            else:
                lo = mid+1
        return hi
[leetcode]382.Linked List Random Node

对于数量居多无法实现内存加载、值从流中输入长度未知的情况,我们无法做到先统计数量再使用随机函数实现,所以就会用到蓄水池算法。由于限定了head一定存在,所以我们先让返回值res等于head的节点值,然后让cur指向head的下一个节点,定义一个变量i,初始化为1,若cur不为空我们开始循环,我们在[0, i - 1]中取一个随机数,如果取出来0,那么我们更新res为当前的cur的节点值,然后此时i自增一,cur指向其下一个位置,这里其实相当于我们维护了一个大小为1的水塘,然后我们随机数生成为0的话,我们交换水塘中的值和当前遍历到的值,这样可以保证每个数字的概率相等

import random
class Solution:

    def __init__(self, head: ListNode):
        """
        @param head The linked list's head.
        Note that the head is guaranteed to be not null, so it contains at least one node.
        """
        self.head = head

    def getRandom(self) -> int:
        """
        Returns a random node's value.
        """
        res = self.head.val

        i = 1
        cur = self.head.next
        while cur:
            j = random.randint(0,i)
            if j== 0:
                res = cur.val
            i+=1
            cur = cur.next
        return res
[leetcode]384.Shuffle an Array

每次往后读取数组的时候,当读到第i个的时候以1/i的概率随机替换1~i中的任何一个数,这样保证最后每个数字出现在每个位置上的概率都是相等的。
证明:
设\(x\)元素在第\(m\)次的时候出现在位置\(i\)的概率是\(1/m\),那么在第\(m+1\)次的时候,\(x\)仍然待在位置\(i\)的概率是 \(1/m * m/(m+1) = 1/(m+1)\)

import random
class Solution:

    def __init__(self, nums: List[int]):
        self.nums = nums
        self.shuffle_nums = nums[:]

    def reset(self) -> List[int]:
        """
        Resets the array to its original configuration and return it.
        """
        return self.nums

    def shuffle(self) -> List[int]:
        """
        Returns a random shuffling of the array.
        """
        for i in range(len(self.shuffle_nums)):
            j = random.randint(0,i)
            self.shuffle_nums[i],self.shuffle_nums[j] = self.shuffle_nums[j],             self.shuffle_nums[i]
        return self.shuffle_nums
[leetcode]497.Random Point in Non-overlapping Rectangles

根据面积作为权重,按概率选到长方形。之后在这个长方形的范围内随机选x和y,输出。

class Solution:
    def __init__(self, rects: List[List[int]]):
        self.rects = rects
        self.weights = []
        for x1,y1,x2,y2 in self.rects:
            self.weights.append((x2 - x1 + 1) * (y2 - y1 + 1))

    def pick(self) -> List[int]:
        x1,y1,x2,y2 = random.choices(
            self.rects, weights=self.weights)[0]
        res = [
            random.randrange(x1, x2 + 1),
            random.randrange(y1, y2 + 1)
        ]
        return res

参考:
470.Implement Rand10() Using Rand7() (拒绝采样Reject Sampling)
519.Random Flip Matrix(Fisher-Yates洗牌算法)

原文地址:https://www.cnblogs.com/hellojamest/p/11706708.html

时间: 2024-11-03 03:06:11

[总结]随机抽样与蓄水池抽样问题的相关文章

MySTL:蓄水池抽样算法

给你一个长度为N的链表.N很大,但你不知道N有多大.你的任务是从这N个元素中随机取出k个元素.你只能遍历这个链表一次.你的算法必须保证取出的元素恰好有k个,且它们是完全随机的(出现概率均等). 这一题应该可以用来解决微信红包分配之类的那种问题,主要是概率的证明挺有意思. 1 #include <iostream> 2 #include <algorithm> 3 #include <time.h> 4 5 using namespace std; 6 7 typedef

洗牌算法与蓄水池抽样

今儿看到了,就在此记录一下吧. 洗牌算法 递归做法:先将1~n-1洗牌,然后取随机数k(0<k<n),并交换n与k,代码很简单: 1 int[] shuffle(int[] cards, int n){ 2 if(n == 1){ 3 return cards; 4 } 5 shuffle(cards, n-1); 6 int k = random(1, n-1); 7 swap(card[k], card[n]); 8 return card; 9 } 也可以转成非递归的: 1 void s

蓄水池抽样算法

问题定义: 给你一个长度为N的链表.N很大,但你不知道N有多大.你的任务是从这N个元素中随机取出k个元素.你只能遍历这个链表一次.你的算法必须保证取出的元素恰好有k个,且它们是完全随机的(出现概率均等). 蓄水池抽样算法: 该算法是针对从一个序列中随机抽取不重复的k个数,保证每个数被抽取到的概率为k/n这个问题而构建的.做法是: - 首先构建一个可放k个元素的蓄水池,将序列的前k个元素放入蓄水池中. 然后从第k+1个元素开始,以k/n的概率来决定该元素是否被替换到池子中. 当遍历完所有元素之后,

蓄水池抽样Reservior Sampling

编程珠玑第12章练习题10: 如何从n个对象(可以依次看到这n个对象,但事先不知道n的值)中随机选择一个?具体说来,如何在事先不知道文本文件行数的情况下读取文件,从中随机选择并输出一行? 解答:我们总选择 第1行,并以概率1/2选择第2行,以概率1/3选择第3行,依次类推,在这一过程结束时,每一行选中的概率是相等的(都是1/n,其中n是文件的总行数) i = 0; while more input lines with probability 1.0/++i choice = this inpu

Reservoir Sampling - 蓄水池抽样

问题起源于编程珠玑Column 12中的题目10,其描述如下: How could you select one of n objects at random, where you see the objects sequentially but you do not know the value of n beforehand? For concreteness, how would you read a text file, and select and print one random l

Reservoir Sampling - 蓄水池抽样算法

蓄水池抽样——<编程珠玑>读书笔记 382. Linked List Random Node 398. Random Pick Index         问题:如何随机从n个对象中选择一个对象,这n个对象是按序排列的,但是在此之前你是不知道n的值的.  思路:如果我们知道n的值,那么问题就可以简单的用一个大随机数rand()%n得到一个确切的随机位置,那么该位置的对象就是所求的对象,选中的概率是1/n. 但现在我们并不知道n的值,这个问题便抽象为蓄水池抽样问题,即从一个包含n个对象的列表S中

转-spark抽样之蓄水池抽样

1.蓄水池抽样算法(Reservoir Sampling) https://www.jianshu.com/p/7a9ea6ece2af 2.spark抽样之蓄水池抽样 https://blog.csdn.net/snaillup/article/details/69524931?utm_source=blogxgwz3 代码: /** * Reservoir sampling implementation that also returns the input size. * * @param

海量数据处理之蓄水池抽样算法

一.问题由来 这个题目的由来是在<编程珠玑>里遇到的,故记录一下.还可以这么说,”如何从二进制文件中等概率取整数?”或者”在不知道文件总行数的情况下,如何从文件中随机的抽取一行?”这个题目说的有点不清楚实际上是:一个二进制文件中有好多好多整数,你要随机取出一个. 这个问题的难点就在于你开始不知道有多少的整数,也就是说这个(1/n)你不知道n是多少. 综上,随机抽样问题表示如下:要求从N个元素中随机的抽取k个元素,其中N无法确定. 这种应用的场景一般是数据流的情况下,由于数据只能被读取一次,而且

算法学习之---蓄水池抽样问题

问题:如何在无限大的数据流中随机选取K个数据,保证当前遍历过的i个元素中每一个元素被选中的概率均为 k/i?从而对于n个元素,每个元素被选中的概率均为 k/n. 解:对于前k个元素,我们直接选中放入一个虚拟的蓄水池中,对于第 k+1 个元素,我们用 k/(k+1) 的概率选中它,一旦选中了,就随机替换掉蓄水池中的某一个元素,这样前 k 个被选中的元素在第 k+1 个元素到来时依然被选中的概率即为它不被替换掉的概率: p = 1*(1-被替换掉的概率q) q = k/(k+1) * 1/k