文章目录
前言
在生活中,总会遇到一些随机事件,例如,出门时是晴天还是下雨,走到十字路口时遇到的是红灯还是绿灯…这些随机事件充斥着我们每一个人的生活,构成了不确定的未来。在数学上,数学家们创立了概率论与统计学,试图在千变万化的随机事件中找到规律,用概率和期望描绘着万变中的不变。
本章将介绍一些随机算法,在这些算法中,有些算法的方法是随机的,有些算法的时间复杂度是随机的,甚至有些算法的准确性也是随机的。
随机算法的本质: 就是试图在千变万化的随机事件中找到规律。
一、随机的方法
蒙特卡洛模拟算法,这类算法利用了计算机可以快速进行大量的特点,通过重复的随机试验来估计概率或者期望。
1.1 计算圆周率
以原点为中心,画一个半径为1的圆,以及它的一个边长为2的外接正方形。
在每次实验中,在正方形中随机取一个点,并判断该点是否在圆内,由于正方形的面积为4,圆的面积为π,所以落在圆内的概率为π/4。
于是π=落在圆内的次数*4/实验总次数
import numpy as np
def Solve(n):
"""
以原点为中心,画一个半径为1的圆,以及它的一个边长为2的外接正方形。
在每次实验中,在正方形中随机取一个点,并判断该点是否在圆内,由于正方形的面积为4,圆的面积为π,所以落在圆内的概率为π/4。
于是π=落在圆内的次数*4/实验总次数
:param n: 试验总次数
:return:
"""
m=0#点在圆内的次数初始化为0
for i in range(n):
x=np.random.uniform(low=-1, high=1, size=1)[0] #生成一个-1到1之间的均匀分布的随机数
y = np.random.uniform(low=-1, high=1, size=1)[0] # 生成一个-1到1之间的均匀分布的随机数
if(x*x+y*y)<1:
m+=1
return m*4/n
print(Solve(1000000))
n=10时,结果为3.6;n=100时,结果为3.168;n=1000000,结果为3.141056。
1.2 迷宫的十字路口
暑假到了,小余要回家休假,这天上午,小余打算从学校出发到火车站,但是小余对路线并不熟悉。在一个十字路口,小余面前有3条路。
(1)如果走第1条路,小余将在a分钟后回到原地。
(2)如果走第2条路,小余将在b分钟后回到原地。
(3)如果走第3条路,小余将在c分钟后到达火车站。
小余只能随机选择一条路走,另外,小余是个路痴,即使回到原地也会忘记之间走的是那条路,所有小余总是会随机选择一条路走,那么问题来了,小余到达火车站所需时间的期望值是多少。
样例输入a=1,b=2,c=3;输出结果为6。
import numpy as np
#计算一次小余到达火车站的总时间
def MonteCarloSimulation(a,b,c):
"""
:param a: int a
:param b: int b
:param c: int c
:return:
"""
ans=0 #总时间初始为0
while(True):
r=np.random.choice([1, 2, 3], 1)[0]#从[1,2,3]选择一个表示走第几条路;np.random.randint(1,4,1)[0] #[1,4)之间的整数选择1个
if r==1: #走第一条路
ans+=a
elif r==2:#走第二条路
ans+=b
else:#走第三条路
ans+=c
break
return ans
a=1
b=2
c=3
n=100000#实验总次数
sum=0 #初始化
for i in range(n):
sum+=MonteCarloSimulation(a,b,c)
print("期望:",sum/n)
数学证明:将小余到达火车站的需要的时间记为X,如果中途回到原地,那么到达火车站的时间还需要X。根据期望的线性性质有: E(X)=1/3(a+E(x))+1/3(b+E(x))+1/3c
所以E(x)=a+b+c
1.3 大数据和小数据
当重复实验的次数足够多时,积累下的大量数据会反映除一些不变量,趋近于概率,均值趋于期望。蒙特卡洛模拟算法是利用大数据的算法。反过来,有时数据量过大,处理起来需要太多时间,就需要采样取出一小部分,对这一小部分数据进行分析。
小余在和朋友玩一个小游戏,桌上散落着一堆扑克牌,扑克牌上A,2…Q,K分别对应着1,2,3…12、13这13个数字。如何更快的找到4张牌,这四张牌的数字之和为4的倍数。
常规解法用四层for循环解决。时间复杂度高。
所有扑克牌上的数字按照除以4的余数可以分为4类——0,1,2,3。那么任何一类数字只要达到4个,就是一组可行的答案,如果扑克牌的数量足够多,那么一定有一类数字达到4个。
怎么才算足够多,如果n=12,最坏的情况下,每一类都恰好有3个数值,这样恰好每一组都不足4个,但如果再多一点,n=13,多出来的一个数值一定能让某一类数达到至少4个。也就是说,在任何13张牌都能找到满足条件的4张牌。
这就是抽屉原理,把n+1个苹果放到n个抽屉里,一定有一个抽屉有2个苹果。
样例输入n=7,表示7张扑克牌。
[5,2,7,7,11,3,11]
输出7,7,11,3
#穷举算法
n=7
P=[5,2,7,7,11,3,11]
def bruteforce():
for i in range(n):
for j in range(i+1,n):
for k in range(j+1,n):
for l in range(k+1,n):
if(P[i]+P[j]+P[k]+P[l])%4==0:
print(P[i],P[j],P[k],P[l])
return #结束循环
bruteforce()
import numpy as np
P=np.random.randint(1,14,30)#[1-13]之间随机产生30个数
#采样前13个
def sample13():
num=[[],[],[],[]] #准备4个[]用于存储
for i in range(13):#采样前13个
category=P[i]%4 #取余
num[category].append(P[i])
#当某一类的数据达到4个时,把这个4个数据输出
if len(num[category])==4:
print(num[category])
return #结束循环
sample13()#结果[2, 10, 10, 2]
对于小数据,用穷举算法暴力解决
对于大于12个的数据采用采样前13个解决。
二、 随机的时间复杂度
2.1 多米诺骨牌上的等差数列
放假在家的小余玩起来多米诺骨牌,小余想要把多米诺骨牌按照等差数列排列起来。例如,第1块多米诺骨牌的高度为10,第2块多米诺骨牌的高度为12,第3块多米诺骨牌的高度为14,之后每一块多米诺骨牌都比前一块高2。
花了一下午,小余终于把它们排好了,但是小余的朋友悄悄把其中的几块换掉了,被换掉的多米诺骨牌不多,至多有3块。现在请你来帮小余找到被换掉的多米诺骨牌,并把它们还原回去。
输入格式:
n=10,表示骨牌数量a=[20,12,14,16,18,20,15,24,26,28]
输出[10,12,14,16,18,20,22,24,26,28]
这个问题看起来十分简单,只需要把被换掉的至多三个多米诺骨牌找出来即可,但仔细思考一下就会发现困难重重,如果要枚举被换掉的多米诺骨牌,三块多米诺骨牌有n(n-1)(n-2)种情况,太多了。更何况,要想判断这几个多米诺骨牌是不是被换掉的那些,只能通过判断其它多米诺骨牌是否形成等差数列来实现,这个过程时间复杂度0(n)。
换个角度来考虑这个问题,尝试找到那些没有被换掉的多米诺骨牌。对于一个等差数列,只需要知道其中的两个数值,就可以还原出整个数列。
如果能够找到两个被换掉的多米诺骨牌,那么就可以还原出一个等差数列,将这个等差数列a0,a1,a2…an-1相比较,对应位置不相等的就是被换掉的多米诺骨牌。那么如何找到两个没有被换掉的关键多米诺骨牌,可以使用一个大胆的方法-随机抽取。
考虑最坏的情况,只有5块多米诺骨牌,其中3个被换掉了,从中随机抽取两个,恰好抽到两个没有被换掉的多米诺骨牌的概率为2/5*1/4=1/10。这个概率确实有点低,但可以多次尝试,随机抽取m次后,仍然抽不到关键的两块多米诺骨牌的概率为(1-1/10)^m。这是指数函数,会以很快的速度收敛到0。当m达到50时,仍然抽不到的概率已经降到了1%以下。也就是说,有超过99%的把握在50次随机抽取中解决问题。
这与前面的“迷宫的十字路口”问题非常相似,把解决问题需要的计算次数为X,那么
E(X)=9/10(E(x)+n)+1/10n。
import numpy as np
n=10
a=[20,12,14,16,18,20,15,24,26,28]
#检查首项为a0,公差为d的等差数列与a数列不一致的个数是否至多为3
def Check(a0,d):
"""
:param a0: 首项
:param d: 公差
:return:
"""
num=0
for i in range(n):
if a[i]!=a0+i*d:
num+=1
return num<=3
while(True):
#随机抽取两个多米诺骨牌
x=np.random.randint(0,len(a)+1,1)[0] #位置
y=np.random.randint(0,len(a)+1,1)[0]
#如果不小心抽到了同一个,跳过本次循环
if(x==y):
continue # 跳过本次循环
#如果公差不是整数,跳过这步循环,
if((a[y]-a[x])%(y-x)!=0): #因为看数列都为整数,则公差为整数
continue# 跳过本次循环
#计算公差d和首项a0
d=(a[y]-a[x])/(y-x)
a0=a[x]-x*d
#检验每一项是否都是整数,骨牌的高度不可能为0或是负数
if a0<=0 or a0+(n-1)*d<=0:
continue #跳过本次循环
if Check(a0,d):#如果抽到了两个没有被换掉的多米诺骨牌,如果是,输出结果并停止运行程序
for i in range(n):
print(a0+i*d)
break #停止程序
2.2 小余的生活费
每个月,小余的妈妈都要给上大学的小余生活费,妈妈希望小余都能养成理性消费的好习惯,所以会严格控制每个月的生活费,至于生活费该给多少,妈妈有一套自动的确定方法。
已知小余在过去的n个月中,每个月的实际花销是a0,a1,…an-1,小余的妈妈把这n个月的花销按照从小到大的顺序排列起来,取第x个数值(从0开始)作为下个月的生活费。
输入格式 n=10表示一共10个月;x=8表示取第8个数值作为下个月的生活费。
a=[900,1000,800,1200,900,1300,1400,800,1000,1300]。
这个问题非常简单,正如题目描述中提到的一样,排序后取第x个数值即可。可以选择快速排序,首先选择一个支点变量,然后把所有比支点变量小的放在左边,所有比支点变量大的或者相等的放在右边。
这个问题和快速排序相似,都可以借助递归来实现,那么递归的边界是什么?可以想到一种特殊情况,如果一段数组中所有的数值都是相等,那么无论支点变量怎么抽取都不能再缩小答案所在的范围,需要及时停止递归,所以递归的边界就是这段数组中所有数字都相等。
def quick_sort(lst, target, start_index, end_index):
last_end_index = end_index # 存储开始和结束的索引
lat_start_index = start_index
if end_index > start_index: # 停止递归的条件
while start_index < end_index:
# 从右向左搜索
while lst[end_index] >= lst[target] and start_index < end_index:
end_index -= 1
# 从左向右搜索
while lst[start_index] < lst[target] and start_index < end_index:
start_index += 1
lst[start_index], lst[end_index] = lst[end_index], lst[start_index] # 交换较大值和较小值
lst[end_index], lst[target] = lst[target], lst[end_index] # 交换中心点和基准点的值
quick_sort(lst, start_index+1, start_index+1, last_end_index) # 排右半部分的顺序
quick_sort(lst, lat_start_index+1, lat_start_index+1, start_index-1) # 排左半部分的顺序
if __name__ == "__main__":
n = 10
x = 8
a = [900, 1000, 800, 1200, 900, 1300, 1400, 800, 1000, 1300]
quick_sort(a, 0, 0, len(a)-1)
print(a[x])
三、随机的准确性
在算法中引入随机性,也就引入了不确定性,实际是有一定的风险的,在接下来介绍的算法中,有时甚至可能得不到正确的结果,但在一些应用场景中,不得不选择这样的算法。
3.1 从字符串到数字——哈希算法
英语课上,小余完成了他的作业——一篇英语作文,小余的朋友却偷偷抄袭了小余的作业,并且修改了部分内容,小余很生气,要找到抄袭的证据。已知小余的作文是包含了n个字符的字符串,小余需要找到抄袭的部分,也就是一段连续的子串,这段子串也存在于被修改后的作文中,为了证明抄袭,这段子串的长度至少需要达到n/2。
输入格式:输入包括两行,第1行是小余的作文,第2行是被抄袭并修改后的作文,两篇作文的长度分别为n,m。
输出格式:如果能够找到一段长度n/2的公共子串,那么输出YES,否则输出NO。
两篇作文中只会出现大小写英文字母,‘’空格、",“英文逗号、”."英文句号。
暴力解法
最简单的方法是使用暴力解法,即遍历两个字符串的所有可能子串,然后比较它们之间是否存在重叠。这种方法虽然简单,但是效率较低,尤其是在字符串较长的情况下。
sentence1="My name is Xiao Yu.I am learning algoritions."
sentence2="My name is Gao Nian.I am learning algoritions."
def find_overlap(str1, str2):#查找最长公共子串
max_len = 0
overlap = ""
for i in range(len(str1)):
for j in range(len(str2)):
k = 0
while i+k < len(str1) and j+k < len(str2) and str1[i+k] == str2[j+k]:
k += 1
if k > max_len:
max_len = k
overlap = str1[i:i+k]
if max_len>=len(str1)/2:
return "YES:",overlap
else:
return "No:",overlap
print(find_overlap(sentence1,sentence2)) #('YES:', '.I am learning algoritions.')
利用字符串切片
另一种方法是利用Python中的字符串切片功能。我们可以从第一个字符串的开头开始,逐步增加切片长度,然后与第二个字符串进行比较,获取重叠部分。这种方法相对于暴力解法来说,效率更高。
sentence1="My name is Xiao Yu.I am learning algoritions."
sentence2="My name is Gao Nian.I am learning algoritions."
def find_overlap(str1, str2):
max_len = 0
overlap = ""
for i in range(len(str1)):
for j in range(len(str1)-i+1):
if str1[i:i+j] in str2 and j > max_len:
max_len = j
overlap = str1[i:i+j]
if max_len>=len(str1)/2:
return "YES:", overlap
else:
return "No:", overlap
print(find_overlap(sentence1,sentence2))片
哈希算法
字符串哈希的运用非常广泛。常见的有让我们求一个字符串中两个子串是否相等或者求多个字符串中,有多少个不相同的字符串等等,运用很多,哈希是处理字符串的一把利器,所以需要掌握其中的原理。
字符串哈希的核心思想就是将一个字符串转换成数字进行存储。
对于每个字符串,我们可以将其看成一串P进制的数组例如,我们可以将字符串ABCD看成P进制下的1 2 3 4,转换成十进制就是1* P^3 +2P^2 +3P^1 +4*P^0,假设P为8,那么这个数就是668,那么668这个数就对应着ABCD这个字符串,将任意两个字符串转换成数字后进行比较的话就非常方便迅速了,可以做到O(1)的时间复杂度,但是,如果当字符串非常长的时候这个数会非常大,所以我们还要确定一个模数MOD将这个字符串转换后的数字在一个长度为0~MOD-1的区间上一一对应。
那么现在的问题就是P和MOD如何取值,一般来说,将P取131或是13331(经验值),MOD取2^64 (经验值)时哈希冲突的概率比较小,并且,当我们取2^64为余数时,即unsiged long long溢出时会自动对2^64取模。
接下来就是如何求得一个字符串的子串的哈希值
以h[N]表示以str[N]结尾的字符串的哈希值
根据公式推导可以知道区间L到R区间的哈希值为h[R]-h[L-1]*P^(R-L+1).
同时可以求得h[i]=h[i-1]*P+str[i];
sentence1="My name is Xiao Yu.I am learning algoritions."
sentence2="My name is Gao Nian.I am learning algoritions."
class StringHash:
# 字符串哈希,用O(n)时间预处理,用O(1)时间获取段的哈希值
def __init__(self, s):
n = len(s)
self.BASE = BASE = 131 # 进制 131,131313
self.MOD = MOD = 10 ** 13 + 7 # np.dot(2,64)
self.h = h = [0] * (n + 1)
self.p = p = [1] * (n + 1)
for i in range(1, n + 1):
p[i] = (p[i - 1] * BASE) % MOD
h[i] = (h[i - 1] * BASE % MOD + ord(s[i - 1])) % MOD
# 用O(1)时间获取开区间[l,r)(即s[l:r])的哈希值
def get_hash(self, l, r):
return (self.h[r] - self.h[l] * self.p[r - l] % self.MOD) % self.MOD
def find_overlap(str1, str2):#查找最长公共子串
hash1 = StringHash
text1 = hash1(str1)
text2 = hash1(str2)
max_len = 0
overlap = ""
for i in range(len(str1)):
for j in range(len(str2)):
k = 0
result1=text1.get_hash(i,i+k)
result2 = text2.get_hash(j, j + k)
while i+k < len(str1) and j+k < len(str2) and text1.get_hash(i,i+k) == text2.get_hash(j,j+k):
k += 1
if k > max_len:
max_len = k
overlap = str1[i:i+k]
if max_len>=len(str1)/2:
return "YES:",overlap
else:
return "No:",overlap
print(find_overlap(sentence1,sentence2))#('YES:', '.I am learning algoritions.')
#当MOD=128时,结果一样。
哈希算法存在很大的隐患,算法不会出错的前提是“MOD”足够大,如果“MOD”不够大,就会出现不同多的字符串的哈希值相同的情况。