概率或随机算法

233 阅读4分钟

参考:

  1. 谈谈游戏中的随机算法
  2. 洗牌算法详解:你会排序,但你会打乱吗?
  3. 带权重的随机选择算法
  4. 常数时间删除-查找数组中的任意元素

问题总览

微信图片_20230512133130.png

序号题目完成
382. 链表随机节点
384. 打乱数组
398. 随机数索引
528. 按权重随机选择
380. 常数时间插入、删除和获取随机元素
381. O(1) 时间插入、删除和获取随机元素 - 允许重复
470. 用 Rand7 实现 Rand10
519. 随机翻转矩阵
710. 黑名单中的随机数
剑指 Offer II 030. 插入、删除和随机访问都是O(1)的容器
剑指 Offer II 071. 按权重生成随机数

一、洗牌算法

384. 打乱数组

这道题的价值是:如何确保打乱的算法是随机性的?

只要确保总的选择数为n!就可以。

class Solution {
    int[] shuffed;
    int[] source;
    Random random;

    public Solution(int[] nums) {
        int n = nums.length;
        shuffed = new int[n];
        source = new int[n];
        random = new Random();
        for (int i = 0; i < n; i++) {
            source[i] = nums[i];
        }
    }

    public int[] reset() {
        return source;
    }

    public int[] shuffle() {
        int n = source.length;
        for (int i = 0; i < n; i++) {
            shuffed[i] = source[i];
        }
        for (int i = 0; i < n; i++) {
            int r = i + random.nextInt(n - i);
            // 这边必须使用swap,不能用替换,注意r是随机的,是有可能重复的
            // 如果每次从source数组中取一个数,那么可能会重复选择,所以只能用swap
            swap(shuffed, i, r);
        }
        return shuffed;
    }

    public void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

二、水塘抽样算法

382. 链表随机节点

考察的是在一个长度未知的链表(数组)随机读取元素。

class Solution {

    ListNode head;
    Random r = new Random();

    public Solution(ListNode head) {
        this.head = head;
    }

    /* 返回链表中一个随机节点的值 */
    int getRandom() {
        ListNode p = head;
        int i = 0;
        int res = 0;
        while (p != null) {
            i++;
            // [0,i)中随机选择一个元素等于0的概率为1/i,比如遍历到第三个元素,i==3,此时选项有0,1,2
            // 选到0的概率就是1/3
            if (r.nextInt(i) == 0) {
                res = p.val;
            }
            p = p.next;
        }
        // 如果一直没有选中,返回第一个元素
        return res;
    }
}

398. 随机数索引

// 用hashmap+洗牌算法解
class Solution {
    Map<Integer, List<Integer>> elements;
    Random random = new Random();

    public Solution(int[] nums) {
        elements = new HashMap<>();
        int len = nums.length;
        for (int i = 0; i < len; i++) {
            List<Integer> values = elements.getOrDefault(nums[i], new ArrayList<Integer>());
            values.add(i);
            elements.put(nums[i], values);
        }
    }

    public int pick(int target) {
        List<Integer> values = elements.get(target);
        if (values.size() == 1) {
            return values.get(0);
        }
        int r = random.nextInt(0, values.size());
        return values.get(r);
    }
}
// 用水塘抽样算法写
class Solution {
    int[] nums;
    Random random = new Random();

    public Solution(int[] nums) {
        this.nums = nums;
        this.random = new Random();
    }

    public int pick(int target) {
        int count = 0;
        int res = -1;
        for (int i = 0; i < nums.length; i++) {
            // 不等于target的处理
            if (nums[i] != target) {
                continue;
            }
            // 等于target的处理
            count++;
            if (random.nextInt(count) == 0) {
                res = i;
            }
        }
        return res;
    }
}

三、带权重的随机选择

用前缀和把权重铺平。

比如:[3,1,2,4]
得到:[3,4,6,10]

可以选择到的数对应的索引概率
1,2,303/10
411/10
5,622/10
7,8,9,1034/10

528. 按权重随机选择

相同问题:剑指 Offer II 071. 按权重生成随机数

class Solution {
    int[] preSums;
    int sum = 0;
    Random random;

    public Solution(int[] w) {
        int n = w.length;
        preSums = new int[n];
        random = new Random();
        for (int i = 0; i < n; i++) {
            sum += w[i];
            preSums[i] = sum;
        }
    }

    public int pickIndex() {
        return find(preSums, random.nextInt(sum) + 1);
    }

    public int find(int[] nums, int target) {
        if (nums.length == 0) return -1;
        int left = 0, right = nums.length;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                right = mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid;
            }
        }
        return left;
    }
}

四、对随机元素的操作

380. 常数时间插入、删除和获取随机元素

相同问题:剑指 Offer II 071. 按权重生成随机数

class RandomizedSet {
    Map<Integer, Integer> map;
    LinkedList<Integer> elements;
    Random random;

    public RandomizedSet() {
        map = new HashMap<>();
        elements = new LinkedList<>();
        random = new Random();
    }

    // 不存在时插入,返回true
    // 存在时,返回false
    public boolean insert(int val) {
        if (map.containsKey(val)) {
            return false;
        }
        // 插入到末尾
        elements.addLast(val);
        map.put(val, elements.size() - 1);
        return true;
    }

    // 存在时删除,并返回true
    // 不存在时,返回false
    public boolean remove(int val) {
        if (!map.containsKey(val)) {
            return false;
        }
        // 获取元素当前的位置
        int idx = map.get(val);
        int idx2 = elements.size() - 1;
        int val2 = elements.get(idx2);
        // 把原先末尾的值的索引移动到被删除的元素上来
        Collections.swap(elements, idx, idx2);
        map.put(val2, idx);

        // 删除末尾元素
        map.remove(val);
        elements.removeLast();
        return true;
    }

    // 从现有集合中随机返回一个元素
    public int getRandom() {
        return elements.get(random.nextInt(elements.size()));
    }
}

381. O(1) 时间插入、删除和获取随机元素 - 允许重复

class RandomizedCollection {
    // 用Set代替List
    Map<Integer, Set<Integer>> map;
    LinkedList<Integer> elements;
    Random random;

    public RandomizedCollection() {
        map = new HashMap<>();
        elements = new LinkedList<>();
        random = new Random();
    }

    public boolean insert(int val) {
        // 将每一个元素的位置都保存到哈希表里
        Set<Integer> res = map.getOrDefault(val, new HashSet<>());
        // 每次都插入队尾,所以直接用element.size()
        res.add(elements.size());
        map.put(val, res);

        // 插入队尾
        elements.addLast(val);
        return res.size() == 1;
    }

    public boolean remove(int val) {
        // 不存在,则返回false
        if (!map.containsKey(val)) {
            return false;
        }
        // 存在
        Set<Integer> res = map.getOrDefault(val, new HashSet<>());

        // 随机取一个元素的索引,为什么要取第一条,是为了保证O(1)
        int idx = res.iterator().next();
        int idx2 = elements.size() - 1;
        // 取链表的最后一个元素来替换
        int val2 = elements.get(idx2);
        Collections.swap(elements, idx, idx2);
        elements.removeLast();

        // 删除元素
        res.remove(idx);
        if (res.size() == 0) {
            // 考虑被替换的元素只有一个
            map.remove(val);
        } else {
            map.put(val, res);
        }

        // 更新被替换的元素的索引
        Set<Integer> res2 = map.getOrDefault(val2, new HashSet<>());
        if (res2.size() > 0) {
            res2.remove(idx2);
            res2.add(idx);
            map.put(val2, res2);
        }
        return true;
    }

    public int getRandom() {
        return elements.get(random.nextInt(elements.size()));
    }
}

五、带黑名单的随机数

519. 随机翻转矩阵

class Solution {
    int len, m, n;
    Map<Integer, Integer> mapping;
    Random random;

    public Solution(int m, int n) {
        len = m * n;
        this.m = m;
        this.n = n;
        mapping = new HashMap<>();
        random = new Random();
    }

    public int[] flip() {
        int rand = random.nextInt(len);
        int res = rand;
        if (mapping.containsKey(rand)) {
            // 已经被选过了,改成另一个数
            res = mapping.get(rand);
        }
        int last = len - 1;
        if (mapping.containsKey(last)) {
            // 已经被选过了,改成另一个数
            last = mapping.get(last);
        }
        mapping.put(rand, last);
        len--;
        //want = y + x * n
        return new int[]{res / n, res % n};
    }

    public void reset() {
        len = m * n;
        mapping.clear();
    }
}

710. 黑名单中的随机数

class Solution {
    int sz;
    Map<Integer, Integer> mapping;

    public Solution(int N, int[] blacklist) {
        sz = N - blacklist.length;
        mapping = new HashMap<>();

        for (int b : blacklist) {
            mapping.put(b, 666); // 标记黑名单
        }

        int last = N - 1;
        for (int b : blacklist) {
            // 如果 b 已经在区间 [sz, N),可以直接忽略
            if (b >= sz) {
                continue;
            }
            while (mapping.containsKey(last)) { // 找到可以映射的位置
                last--;
            }
            mapping.put(b, last); // 映射
            last--;
        }
    }

    public int pick() {
        // 随机选取一个索引
        int index = (int)(Math.random() * sz);
        // 这个索引命中了黑名单,需要被映射到其他位置
        if (mapping.containsKey(index)) {
            return mapping.get(index);
        }
        // 若没命中黑名单,则直接返回
        return index;
    }
}

六、其他

470. 用 Rand7 实现 Rand10

class Solution extends SolBase {
    public int rand10() {
        int row, col, idx;
        do {
            idx = 1;
            row = rand7();
            col = rand7();
            
            idx = col + (row - 1) * 7;
        } while (idx > 40);
        // idx的取值范围在[1,40],这40个位置上的元素是[1,10],并且每一个元素的概率的1/10
        return 1 + (idx - 1) % 10;
    }
}