041.随机算法

Random Algorithm

随机算法在对拍,快速排序算法,快速选择算法中都有一定应用

简易对拍器

Quick Sort,Quick Select

随机算法本身也十分有趣

洗牌算法

打乱一个数组

如果数组有n个元素,那么一共有 n! 种排列

要求每种排列是等可能的

先看实现

    int n;
    vector<int>nums(n);
    
    for(int i = 0 ; i< n - 1 ; ++i ){
        swap( nums[i] , nums[ i + rand()%(n-i)]);
    }

这样得到的数组是等概率随机的,一共有 n! 种可能

证明:

  • 对于 nums[0] 我们将它等概率地放到了 [0,n) 上,共有 n 可能

  • 对于 nums[1] 我们将它等概率地放到了 [1,n) 上,共有 n-1 可能

  • 对于 nums[2] 我们将它等概率地放到了 [2,n) 上,共有 n-2 可能

  • …………

  • 对于 nums[n-2] 我们将它等概率地放到了 [n-2,n) 上,共有 2 可能

  • 对于 nums[n-1] 我们将它放到了 [n-1,n) 上,有 1 可能

累乘得到一共有 n! 种可能

变式 - 随机分布

我们需要将k个元素随机分布进一个长度为 n 的数组里

转化问题:

先将这k个元素现在都放在数组开头,然后打乱数组

这样就实现了k个元素的随机分布

leetcode 384

class Solution {
    vector<int>init;
public:
    Solution(vector<int>& nums) {
        this->init=nums;
    }
    vector<int> reset() {
        return init;  
    }
    vector<int> shuffle() {
        vector<int>a=init;
        int n=a.size();
        for(int i=0;i<n-1;i++){
            swap(a[i],a[i+rand()%(n-i)]);
        }
        return a;
    }
};

水塘抽样算法

case1

如果给你一个数组nums,长度为n,要求等概率随机返回其中一个元素

简单 !

rand() % n即可随机得到一个[0,n)的下标,而且这是等概率的

case2

如果现在不知道数组长度

换句话说:给你一个指向一个一维数组的指针ptr,要求等概率随机返回其中一个元素

我们可以先遍历一遍,算出长度

int n=0;
int *p=ptr;
while(p++){
    n++;
}

然后计算一个随机下标

int i = rand() % n;
int ans = *(ptr + i);

case3

现在元素的内存地址不连续了

换句话说:给你一个链表head,要求等概率随机返回其中一个元素

int n=0;
ListNode*p=head;
while(p){
    n++;
    p=p->next;
}
int i=rand()%n;
p=head;
while(i--){
    p=p->next;
}
int ans=p->val;

好像也很简单

case4

case3的基础上再加一个限制

要求 只遍历一趟 链表,等概率随机返回其中一个元素。

遍历时,我们不知道一共有多少个元素

但我们可以知道当前元素是第几个元素

int getRandom(ListNode*head){
    int ans;
    int i=0;
    ListNode*p=head;
    while(p){
        i++;
        if(rand()%i==0){
            ans=p->val;
        }
        p=p->next;
    }
    return ans;
}

证明

rand() % i == 0的含义:此if语句为真的概率为1/i

因为rand()%i得到一个[0,i)的数,为0的概率为1/i

对于第i个数,他要想被选上必须满足

  1. 轮到它的时候,它被选上了 1/i

  2. i个数后面的所有数都没有被选上

ans可以刷新,所以前面的数对后面的数没有影响

那么第i个数被选上的概率为

每个数都是等概率的,概率为1/n

leetcode 382

class Solution {
    ListNode*h;
public:
    Solution(ListNode* head) {
        h=head;
    }
    int getRandom() {
        int ans,i=0;
        ListNode*p=h;
        while(p){
            i++;
            if(rand()%i==0)ans=p->val;
            p=p->next;
        }
    }
    return ans;
};

变式 - 随机选择 k 个数

未知长度的单链表,只遍历一遍,随机选出k个数

只要在第 i 个元素处以 k/i 的概率选择该元素即可

//保证链表中元素不少于k个
vector<int> getRandom(ListNode*head,int k){
    vector<int>ans(k);
    ListNode*p=head;
    int i=0;
    while(p){
        i++;
        int j=rand()%i;
        if(j<k){
            ans[j]=p->val;
        }
        p=p->next;
    }
    return ans;
}

降维技巧

对于一个 n × m 的地图,我们要随机获取若干个点

我们可以将二维数组 Map[n][m] 压至一维 map[n*m]

M[i][j]对应map[k]

k = i * m + j

i = k / m , j = k % m

leetcode 519

带权随机

leetcode 528

如果随机不是等概率的,而是带着一定的权重,该如何实现 ?

具体地,给你一个长度为n的正整数数组w,其中w[i]代表第i个下标的权重

选中下标i对应元素的概率为w[i] / sum(w)

比如 w = {1,3,2,5,1},sum(w)=12

可以想象这样一个长度为12数组0,1,1,1,2,2,3,3,3,3,3,4

容易理解,在这个数组中等概率随机选择一个元素符合题意

但是,如果sum(w)非常大,我们将无法承担这样的开销

何解?

前缀和 + 二分

对于w = {1,3,2,5,1}

presum[i+1]=presum[i]+w[i]

得到presum = {0,1,4,6,11,12}

注意presum[0]无意义,所以presum[i]对应w[i-1]

我们生成一个[ 0 , sum(w) )的随机数k

presum中查找大于等于k的最小下表index

最后根据presunw下标的偏移关系

得到x-1为答案

class Solution {
    int n;
    vector<int>presum;
public:
    Solution(vector<int>& w) {
        n=w.size();
        presum.resize(n+1);
        for(int i=1;i<=n;++i){
            presum[i]=presum[i-1]+w[i-1];
        }
    }
    int pickIndex() {
        int k=rand()%presum[n]+1;
        int l=1,r=n,mid,ans;
        while(r>=l){
            mid=(l+r)>>1;
            if(presum[mid]>=k){
                ans=mid,r=mid-1;
            }
            else l=mid+1;
        }
        return ans-1;
    }
};

蒙特卡洛验证法

既然是随机算法,OJ怎么判断代码的正确性 ?

如果你的算法不是随机的,leetcode似乎总能检测出错误

蒙特卡洛验证法就是一种检验策略

以一个简单情况为例

实现函数
int getrandom(int n);
传入n
等概率随机返回一个[1,n]的数
//提交
int getrandom(int n){
    return rand() % n + 1;
}

测试程序

#include<iostream>
#include<random>
using namespace std;

const int n = 5;
const int total = 1000000;
const int MIN=199000;
const int MAX=201000;
//如果误差不超过 %0.5 ,认为正确
int getrandom(int);

int main(){
    int cnt[n + 1]={0};
    for(int i=0;i<total;++i){
        cnt[getrandom(n)]++;
    }
    for(int i=1;i<=n;++i){
        if(cnt[i]<MIN||cnt[i]>MAX){
            cout<<"WA";
            return 0;
        }
    }
    cout<<"AC";
    return 0;
}

//在这里提交答案
posted @ 2026-01-11 08:57  射杀百头  阅读(3)  评论(0)    收藏  举报