041.随机算法
Random Algorithm
随机算法在对拍,快速排序算法,快速选择算法中都有一定应用
随机算法本身也十分有趣
洗牌算法
打乱一个数组
如果数组有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个元素的随机分布
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/i -
第
i个数后面的所有数都没有被选上
ans可以刷新,所以前面的数对后面的数没有影响
那么第i个数被选上的概率为

每个数都是等概率的,概率为1/n
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
带权随机
如果随机不是等概率的,而是带着一定的权重,该如何实现 ?
具体地,给你一个长度为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
最后根据presun与w下标的偏移关系
得到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;
}
//在这里提交答案

浙公网安备 33010602011771号