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


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

    [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洗牌算法)

  • 相关阅读:
    重构之路第一篇——重新组织函数的几种方法
    Maven设置http代理
    maven本地仓库路径和修改
    Maven安装目录分析
    使用Python解析Loadrunner的post的中文数据
    Python3 批量创建文件夹
    tomcat设置上传文件大小
    Jenkins启动
    Sublime Text 3 编译Python3
    常用SQL
  • 原文地址:https://www.cnblogs.com/hellojamest/p/11706708.html
Copyright © 2020-2023  润新知