最近闲赋在家的时候不太想背那些没用的八股文,就学习一点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();
*/
方法一:蓄水池采样算法
知道了水塘抽样算法后这道题就好做了。
- 先取随机的长方形(通过水塘抽样)
- 然后在长方形内随机取个点就好了。
这里的水塘抽样相比于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();
*/
给定方法 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,以及其他简单概率问题的解决。
之前也没怎么刷过题,还有非常多要学的东西。