LeetCode笔记---简单概率入门

这篇博客介绍了通过LeetCode中涉及的随机化算法,如蓄水池采样、Kunth洗牌算法,解决数组、链表、非重叠矩形中的随机点选取等问题。此外,还探讨了如何在限制条件下实现O(1)时间复杂度的插入、删除和随机访问操作,并展示了如何用Rand7()实现Rand10()。
摘要由CSDN通过智能技术生成

更多看这里吧😁

 最近闲赋在家的时候不太想背那些没用的八股文,就学习一点LeetCode提升基础的编程。
 昨天把网站上tag是“随机化”的中等题全部做完了。还是有一点收获的。随便记录几题,就当懒人备忘录了


蓄水池采样算法

在这里插入图片描述

当K = 1
在这里插入图片描述
当K = K
在这里插入图片描述


382. 链表随机节点
在这里插入图片描述
在这里插入图片描述
方法一:转数组
直接把链表转数组,支持随即查找,然后随机在长度范围内取个值就好了。
时间复杂度O(1), 空间复杂度O(n)

只不过前提是random这个系统API生成的随机数首先得是均匀的。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {

    int[] arr = new int[10001];

    int size = 0;

    Random ran = new Random();

    public Solution(ListNode head) {
        while (head != null) {
            arr[size++] = head.val;
            head = head.next;
        }
    }
    
    public int getRandom() {
        return arr[ ran.nextInt(size) ];
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(head);
 * int param_1 = obj.getRandom();
 */

方法二:蓄水池采样算法
进阶问题用该算法,本题属于蓄水池算法中K = 1的问题,所以每次以 1/ i 概率取当前遍历到的元素(i 是下标从1开始)。
不过这个算法时间复杂度O(n), 空间复杂度O(1),在多次调用时候效率较差。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {

    ListNode head = null;

    Random ran = new Random();

    public Solution(ListNode head) {
        this.head = head;
    }
    
    public int getRandom() {
        ListNode cur = head;
        int i = 1;
        int ans = 0;
        while (cur != null) {
            if (ran.nextDouble() < 1.0 / i) {
                ans = cur.val;
            }
            cur = cur.next;
            i++;
        }
        return ans;
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(head);
 * int param_1 = obj.getRandom();
 */

398. 随机数索引
在这里插入图片描述
乍一看就是水塘抽样,但是看看长度和调用次数对于O(n)时间复杂度算法可能会超时
。写了下水塘抽样果然超时了。

方法一:蓄水池采样算法(超时)

class Solution {
    int[] nums;
    Random rand;
    public Solution(int[] nums) {
        this.nums = nums;
        rand = new Random();
    }
    
    public int pick(int target) {
        int num = 0;
        int index = -1;
        for(int i = 0; i < nums.length; i++){
            if(nums[i] == target){
                if(rand.nextInt(++num) == 0){
                    index = i;
                }
            }
        }
        return index;
    }
}

方法二:哈希表
把每种元素放到对应的筒子里,每次对该筒子长度随机取就好了

class Solution {

    Random ran = new Random();

    HashMap<Integer, List<Integer>> map = new HashMap<>(1024);

    public Solution(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            List<Integer> list = map.getOrDefault(nums[i], new ArrayList<>(1024));
            list.add(i);
            map.put(nums[i], list);
        }
    }
    
    public int pick(int target) {
        List<Integer> list = map.get(target);
        return list.get( ran.nextInt(list.size()) );
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(nums);
 * int param_1 = obj.pick(target);
 */

528. 按权重随机选择
在这里插入图片描述
在这里插入图片描述

w[ i ] / sum [ i ] 可以看出要求前缀和,
在这里插入图片描述
在这里插入图片描述
所以先求前缀和然后对总和sum取随机数得到r,然后在前缀和数组中通过二分找到大于等于r的后继值的索引 idx ,返回idx - 1(前缀和相对于原数组数组有1个偏移)就好了。

class Solution {

    int[] sum = null;

    Random ran = new Random();

    public Solution(int[] w) {
        sum = new int[w.length + 1];
        int p = 0;
        for (int i = 1; i < sum.length; i++) {
            sum[i] = p + w[i - 1];
            p = sum[i];
        }
    }
    
    public int pickIndex() {
        int r = ran.nextInt(sum[sum.length - 1]) + 1;
        return bs(r) - 1;
    }

    private int bs(int target) {
        int l = 0, r = sum.length - 1;
        while (l < r) {
            int mid = (l + r) >> 1;
            if (sum[mid] >= target) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return l;
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(w);
 * int param_1 = obj.pickIndex();
 */

497. 非重叠矩形中的随机点
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

方法一:蓄水池采样算法
知道了水塘抽样算法后这道题就好做了。

  1. 先取随机的长方形(通过水塘抽样)
  2. 然后在长方形内随机取个点就好了。

这里的水塘抽样相比于k = 1的水塘抽样概率1 / i 有所不同,只要保证每次每个长方形被抽中的概率均匀分布就好了。比如当前遍历到第二个长方形,概率就是
p = cur[1] / sum[1]
cur[1]表示当前长方形内点数(面积),sum[1]表示到第2个长方形的总点数(前缀和面积)
比如长方形面积依次是[1, 2, 3],求前缀和面积是[1, 3, 6]
cur[1] / sum[1] = 2 / 3 表示到第二个长方形为止,抽第二个长方形的概率是2/3,那么此时抽第一个长方形的概率是(1 -2/3) * 1 = 1/3,满足概率分布。
同理每次抽取都符合概率分布,所以每次抽的时候对以p = cur[1] / sum[1]是合理的。

class Solution {

    int[][] rs;
    int[] sum;
    int n;
    Random ran = new Random();

    public Solution(int[][] rects) {
        rs = rects;
        n = rs.length;
        sum = new int[n + 1];
        for (int i = 1; i <= n; i++) {
             sum[i] = sum[i - 1] + 
             (rs[i - 1][2] - rs[i - 1][0] + 1) * 
             (rs[i - 1][3] - rs[i - 1][1] + 1);
        }
    }
    
    public int[] pick() {
        int idx = -1;
        for(int i = 0; i < n; ++i) {
            int x1 = rs[i][0], y1 = rs[i][1], x2 = rs[i][2], y2 = rs[i][3];
            int cur = (x2-x1+1) * (y2-y1+1);
            if(ran.nextDouble() < cur * 1.0 / sum[i + 1]) {
                idx = i;
            } 
        }
        int x1 = rs[idx][0], y1 = rs[idx][1], x2 = rs[idx][2], y2 = rs[idx][3];
        return new int[]{x1 + ran.nextInt(x2-x1+1), y1 + ran.nextInt(y2-y1+1)};
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(rects);
 * int[] param_1 = obj.pick();
 */

方法二:前缀和+二分
在这里插入图片描述
和之前那题思路一样,可以等分大小为1的区间。根据前缀和的单调性可以利用二分找上界。
比如长方形面积依次是[1, 2, 3],总和为6,通过这样方法每个长方形取到的概率是
[1 / 6, 2 / 6, 3/ 6]满足等概率分布,因此对总和sum的范围随机得到r,利用二分在前缀和中找r的上界索引,就是随机取到的长方形的索引了。

class Solution {

    int[][] rs;
    int[] sum;
    int n;
    Random ran = new Random();

    public Solution(int[][] rects) {
        rs = rects;
        n = rs.length;
        sum = new int[n + 1];
        for (int i = 1; i <= n; i++) {
             sum[i] = sum[i - 1] + 
             (rs[i - 1][2] - rs[i - 1][0] + 1) * 
             (rs[i - 1][3] - rs[i - 1][1] + 1);
        }
    }
    
    public int[] pick() {
        int range = ran.nextInt(sum[n]) + 1;
        int idx = bs(range);
        int[] rec = rs[idx];
        int x = ran.nextInt(rec[2] - rec[0] + 1) + rec[0];
        int y = ran.nextInt(rec[3] - rec[1] + 1) + rec[1];
        return new int[] {x, y};
    }

    private int bs(int target) {
        int l = 0, r = n;
        while (l < r) {
            int mid = l + r >> 1;
            if (sum[mid] >= target) r = mid;
            else l = mid + 1;
        }
        return r - 1;
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(rects);
 * int[] param_1 = obj.pick();
 */

Kunth洗牌算法

Kunth洗牌算法可以在O(n)的时间复杂度下均匀等概率分配每个元素。

利用经典洗牌算法,就是 Knuth 算法。

如下图,在整个数组 [0, n-1] 中(包括最后一个元素)随机选出一个元素,将它和最后那个元素 [n-1] 交换,然后再在数组 [0, n-2] 中随机选出一个元素,将它与倒数第二个元素 [n-2] 交换…一直到最后一个元素,就完成了算法。

一个数组[1,2,3,4,5],
第一次洗牌比如交换5和2。5被交换的概率1/5
第二次洗牌(当前范围为[1,2,3,4])比如交换4和3,那么此时选中4的概率是1 /4 * (1 - 1/5) = 1/ 5
同理……
每次洗牌选中的元素概率都是1/5
……
n个元素的数组每次都是1/n,因此等概率的。


384. 打乱数组

在这里插入图片描述
在这里插入图片描述
这就是个裸题,直接公式丢进去就好了

class Solution {

    int[] backup = null;

    int[] opt = null;

    Random ran = new Random();

    public Solution(int[] nums) {
        backup = Arrays.copyOf(nums, nums.length);
        opt = nums;
    }
    
    public int[] reset() {
        return backup;
    }
    
    public int[] shuffle() {
        for (int i = opt.length -1; i >= 0; i--) {
            swap(opt, i, ran.nextInt(i + 1));
        }
        return opt;
    }

    private void swap(int[] arr , int a, int b) {
        int t = arr[a];
        arr[a] = arr[b];
        arr[b]  =t;
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(nums);
 * int[] param_1 = obj.reset();
 * int[] param_2 = obj.shuffle();
 */

其他概率问题

剑指 Offer II 030. 插入、删除和随机访问都是 O(1) 的容器
在这里插入图片描述
在这里插入图片描述
主要是random,如何最快。
 如果直接用map存放,insert或者remove确实可以O(1)时间复杂度,但是random就做不到了。因为Java的hashmap底层不只是数组结构,可能还包含链表或者红黑树,不支持随机下标查询。
 所以可以再用一个数组list,通过map存 val -> idx的映射,数组存val, 这样insert和remove同时在map和list中操作。
 不过remove应该要记得把要删除元素与list中末尾元素交换,这样删除就只用删除最后一个,实现了list删除某个元素达到O(1)。
 这样就可以random一个随机下标然后在数组list中随即查找了,实现了random O(1)的时间复杂度

class RandomizedSet {

    HashMap<Integer, Integer> map = new HashMap<>();

    ArrayList<Integer> list = new ArrayList<>();

    Random ran = new Random();

    /** Initialize your data structure here. */
    public RandomizedSet() {

    }
    
    /** Inserts a value to the set. Returns true if the set did not already contain the specified element. */
    public boolean insert(int val) {
        if (map.containsKey(val)) {
            return false;
        }
        map.put(val, list.size());
        list.add(val);
        return true;
    }
    
    /** Removes a value from the set. Returns true if the set contained the specified element. */
    public boolean remove(int val) {
        if (!map.containsKey(val)) {
            return false; 
        }
        int idx = map.get(val);
        int tval = list.get(list.size() - 1);
        list.set(list.size() - 1, val);
        list.set(idx, tval);
        list.remove(list.size() - 1);
        map.put(tval, idx);
        map.remove(val);
        return true;
    }
    
    /** Get a random element from the set. */
    public int getRandom() {
        int r = ran.nextInt(list.size());
        return list.get( r );
    }
}

/**
 * Your RandomizedSet object will be instantiated and called as such:
 * RandomizedSet obj = new RandomizedSet();
 * boolean param_1 = obj.insert(val);
 * boolean param_2 = obj.remove(val);
 * int param_3 = obj.getRandom();
 */

不过其实也可以直接只用一个set存所有信息,每次random时候都是O(n)搜索一遍,也能过这题(2022.6.10)。

class RandomizedSet {

    HashSet<Integer> set = new HashSet<>();

    Random ran = new Random();

    /** Initialize your data structure here. */
    public RandomizedSet() {

    }
    
    /** Inserts a value to the set. Returns true if the set did not already contain the specified element. */
    public boolean insert(int val) {
        if (set.contains(val)) {
            return false;
        }
        set.add(val);
        return true;
    }
    
    /** Removes a value from the set. Returns true if the set contained the specified element. */
    public boolean remove(int val) {
        if (!set.contains(val)) {
            return false; 
        }
        set.remove(val);
        return true;
    }
    
    /** Get a random element from the set. */
    public int getRandom() {
        int idx  =ran.nextInt(set.size());
        int i = 0;
        for (Integer x: set) {
            if (i == idx) {
                return x;
            }
            i++;
        }
        return -1;
    }
}

/**
 * Your RandomizedSet object will be instantiated and called as such:
 * RandomizedSet obj = new RandomizedSet();
 * boolean param_1 = obj.insert(val);
 * boolean param_2 = obj.remove(val);
 * int param_3 = obj.getRandom();
 */

470. 用 Rand7() 实现 Rand10()

给定方法 rand7 可生成 [1,7] 范围内的均匀随机整数,试写一个方法 rand10 生成 [1,10] 范围内的均匀随机整数。

你只能调用 rand7() 且不能调用其他方法。请不要使用系统的 Math.random() 方法。

每个测试用例将有一个内部参数 n,即你实现的函数 rand10() 在测试时将被调用的次数。请注意,这不是传递给 rand10() 的参数。

ran7只能生成[1,7]的数,要实现rand10[1, 10],
首先要保证每个元素的概率1/ 10,因此要构造事件:
a事件:[1,6]内奇偶个数相等,因此取到任意偶数概率1/2。
还差1/5
b事件:那就用rand7随机生成[1,5]的数就好了,遇到大于5的continue掉。
a和b事件同时发生概率1/10,并且a发生时候是5,b发生就设置b具体的值。

/**
 * The rand7() API is already defined in the parent class SolBase.
 * public int rand7();
 * @return a random integer in the range 1 to 7
 */
class Solution extends SolBase {
    public int rand10() {
        int a = rand7();
        while (a == 7) {
            a = rand7();
        }
        int b = rand7();
        while (b > 5) {
            b = rand7();
        }
        return ((a & 1) == 1 ? 0 : 5) + b;
    }
}

今日总结

 今天还是挺有收获的,学习了蓄水池采样算法,Kunth,以及其他简单概率问题的解决。
 之前也没怎么刷过题,还有非常多要学的东西。

更多看这里吧😁

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值