随机算法
定义
- 不要求对所有输入均正确计算,只要求出现错误的可能性小到可忽略(得能解决问题)
- 同一组输入,不要求同一个结果(不确定)
应用
有些使用确定性求解算法效率会很差的问题,如果用随机算法求解,可以很快得到相当可信的结果,典型用于公钥,RSA算法。
分类
主要分为LasVegas算法和MonteCarlo算法
Las Vegas算法
- 少数应用时会出现求不出解的情况
- 但一旦找到一个解,则一定是正确的
- 求不出解的时候需再次调用算法计算,直到获得解
- 对于此类算法,主要是分析算法的时间复杂度的期望值,以及调用一次产生失败的概率
Monto Carlo算法
-
通常不能保证计算出来的结果总是正确的, 一般只能断定所给解的正确性不小于p ( 1/2<p<1)
-
通过算法的反复执行(即以增大算法的执行时间为代价),能够使发生错误的概率 小到可以忽略的程度 (越算越好)
-
由于每次执行的算法是独立的,故k次执行均发生错误的概率为(1-p)k
-
对于判定问题(回答只能是“Yes”或 “No”)
- 带双错的(two-sided error): 回答”Yes”或”No”都 有可能错
- 带单错的(one-sided error):只有一种回答可能错
-
Las Vegas算法可以看成是单错概率为0的 Monte Carlo算法
优点
- 对于某一给定的问题,随机算法所需的时 间与空间复杂性,往往比当前已知的确定性算法要好
- 到目前为止设计出来的各种随机算法,无 论是从理解上还是实现上,都是极为简单的
- 随机算法避免了去构造最坏情况的例子
具体问题分析及实现
找第k小元素的随机算法 (Las Vegas算法)
- 在n个数中随机的找一个数A[i]=x, 然后将 其余n-1个数与x比较,分别放入三个数组中S1(元素均<x), S2(元素均=x), S3(元素 均>x)
- 若|S1|≥k ,则调用Select(k,S1)
- 若(|S1|+|S2|)≥k,则第k小元素就是x
- 否则就有(|S1|+|S2|)< k,此时调用 Select(k-|S1|-|S2|,S3)
package LasVegas;
import java.util.*;
public class NumKSmall {
public static void main(String[] args) {
// TODO 自动生成的方法存根
Random r = new Random(1);
ArrayList<Integer> x = new ArrayList<Integer>();
for(int i = 0; i < 100; i++) {
x.add(r.nextInt(100));
}
int k = r.nextInt(100);
System.out.println("在x中第 %d 小的数字是: ",k);
System.out.println(NKS_LV(x, k));
}
private static int NKS_LV(ArrayList<Integer> x, int k) {
// TODO 自动生成的方法存根
int len = x.size();
Random r = new Random(1);
int s = x.get(r.nextInt(len));
ArrayList<Integer> S1 = new ArrayList<Integer>();
ArrayList<Integer> S2 = new ArrayList<Integer>();
ArrayList<Integer> S3 = new ArrayList<Integer>();
for(int i = 0; i < len; i++) {
int xx = x.get(i);
if (xx < s) {
S1.add(xx);
}else if (xx == s){
S2.add(xx);
}else {
S3.add(xx);
}
}
if (k <= S1.size()) {
return NKS_LV(S1,k);
}else if ( k <= S1.size() + S2.size()) {
return s;
}else {
return NKS_LV(S3, k - S1.size() - S2.size());
}
}
}
其实经典的BFPTR算法就是改进了随机的部分,而是采用将array分成5个部分,找出5个部分中位数组成的数组的中位数作为最初的s,可以保证**O(N)**的时间复杂度。
用到的思想就是LasVegas或者说Sherwood随机化方法,消除或减少问题的好坏输入实例之间的差别。
测试字符串是否相同(Monte Carlo算法)
设A处有一个长字符串x(e.g. 长度为106), B处也有一个长字符串y,A将x发给B,由 B判断是否有x=y。
- 首先由A发一个x的长度给B,若长 度不等,则x≠y
- 若长度相等,则采用“取指纹”的方法:
- A对x进行处理,取出x的“指纹”,然后将x的“指纹” 发给B
- 由B检查x的“指纹”是否等于y 的“指纹”
- 若取k次“指纹”(每次取法不同),每次两者结果均相同,则认为x与y是相等的
- 随着k的增大,误判率可趋于0
思路是这样,java中hashcode的思路就是这样,取模就是hash,equals判断的时候就是利用哈希,哈希中取的是31。如果多次取不同的k,判断多次两者指纹都一样,MC算法就认定x=y。
//String里的hashCode()
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
错判率分析
B接到指纹Ip(x)后与Ip(y)比较 如果Ip(x)≠Ip(y),当然有x≠y
如果Ip(x)=Ip(y)而x≠y,则称此种情况为一个误匹配
现在需要确定:误匹配的概率有多大?
若总是随机地去取一个小于M的素数p,则对于给定 的x和y,
Pr[failure] =(使得Ip(x)=Ip(y)但x≠y的素数 p(p<M)的个数)/(小于M的素数的总个数)
Pattern Matching (Monto Carlo算法)
问题
给定两个字符串:X=x1,…,xn,Y=y1,…,ym, 看Y是否为X的子串?(即Y是否为X中的 一段)
- KMP算法刚总结过,确实麻烦,随机算法可以大大简化思考的难度。brute-force思想
- 记X(j)=xj x j+1…x j+m-1(从X的第j位开始、 长度与Y一样的子串)
- 从起始位置j=1开始到 j=n-m+1,不去逐一 比较X(j)与Y,而仅逐一比较X(j)的指纹 Ip(X(j))与Y的指纹Ip(Y)
- 由于Ip(X(j+1))可以很方便地根据Ip (X(j))计算出来,故算法可以很快完成
1.随机取一个小于M的素数p,置j←1;
2.计算Ip(Y)、Ip(X(1))及Wp(=2m mod p);
3.While j≤n-m+1
do
{
if Ip(X(j))=Ip(Y) then return j /﹡X(j)极有可能等于Y﹡/
else{
根据Ip(X(j))计算出Ip(X(j+1));j增1
}
}
4.return 0; /﹡X肯定没有子串等于Y﹡/
时间复杂度分析
- 计算Ip(Y)、Ip(X(1))及2m mod p的时间不 超过O(m)次运算
- Ip(X(j+1))的计算,只需用O(1)时间
- 由于循环最多执行n-m+1次,故这部分的 时间复杂度为O(n),于是,总的时间复杂 性为O(m+n)
错判率分析 - 当Y≠X(j),但Ip(Y)=Ip(X(j))时产生失败
- 失败的概率Pr[failure]<1/n 【这里有点迷惑,为什么是<1/n,不应该是<1/m么?每次比的时候是按Y的长度比的啊】,即失败的概率 只与X的长度有关,与Y的长度无关
上面两个问题的应用可以理解为:如果指纹不等,则必不存在;如果指纹相等,则大概率存在。在算法中判定为成功。具体代码没有编写,这种问题多看些资料扩展思路很有必要,我通过学习这些不断查找的名词和资料路径:
Brute Force
Pattern Matching
串匹配 - 中国科学技术大学
指纹函数
模和环(矩阵论,矩阵做f的数学基础需要补足,之前fibonacci的时候那个f就遇到过,本质是一种聪明的映射,需要掌握方法)
其中每个部分深入理解都大有获益。下次遇到相关问题时再进行总结。
- Random Sampling问题
主元素问题
设T[1:n]是一个含有n个元素的数组。当 |{i|T[i]=x}|>n/2时,称元素x是数组T的主元素
问题描述 :对于给定的数组T,判定T数组中是否含有主元素
Monte Carlo算法:
package LasVegas;
import java.util.*;
public class NumKSmall {
public static void main(String[] args) {
// TODO 自动生成的方法存根
Random r = new Random(1);
ArrayList<Integer> x = new ArrayList<Integer>();
for(int i = 0; i < 100; i++) {
x.add(r.nextInt(100));
}
int k = r.nextInt(100);
System.out.printf("在x中第 %d 小的数字是: \n",k);
System.out.println(NKS_LV(x, k));
}
private static int NKS_LV(ArrayList<Integer> x, int k) {
// TODO 自动生成的方法存根
int len = x.size();
Random r = new Random(1);
int s = x.get(r.nextInt(len));
ArrayList<Integer> S1 = new ArrayList<Integer>();
ArrayList<Integer> S2 = new ArrayList<Integer>();
ArrayList<Integer> S3 = new ArrayList<Integer>();
for(int i = 0; i < len; i++) {
int xx = x.get(i);
if (xx < s) {
S1.add(xx);
}else if (xx == s){
S2.add(xx);
}else {
S3.add(xx);
}
}
if (k <= S1.size()) {
return NKS_LV(S1,k);
}else if ( k <= S1.size() + S2.size()) {
return s;
}else {
return NKS_LV(S3, k - S1.size() - S2.size());
}
}
}
n后问题
- 在n×n格的棋盘上放置彼此不受攻击的n 个皇后。
- 按照国际象棋的规则,皇后可以攻击与之处在同一 行或同一列或同一斜线上的棋子。n后问题等价于 在n×n格的棋盘上放置n个皇后,任何2个皇后不放 在同一行或同一列或同一斜线上。
- n后问题的Las Vegas算法思路:
- 各行随机放置皇后,使新放的与已有的互不攻击,
- until (n皇后放好||无可供下一皇后放置的位置)
以八皇后为例
package LasVegas;
import java.util.*;
import java.util.Random;
public class NQueens {
static int n;
static ArrayList<Integer> queens;
public static void main(String[] args) {
n = 8;
boolean t = false;
while( t != true) {
t = FindOne(n);
}
if(t == true) {
System.out.println(queens);
}
}
private static boolean FindOne(int nn) {
// TODO 自动生成的方法存根
queens= new ArrayList<Integer>();
int count = 0;
Random r = new Random();
for(int i = 1; i < 9; i++) {
while(queens.size() < i) {
int a = r.nextInt(8) + 1;
int b =r.nextInt(8) + 1;
int j = a*10 + b;
count++;
if (notConflict(j)) {
queens.add(j);
count = 0;
}
if( count == 8) {
System.out.println("算法失败");
return false;
}
}
}
return true;
}
private static boolean notConflict(int j) {
// TODO 自动生成的方法存根
for(int q : queens) {
if(j%10 == q%10 || j/10 == q/10 || j/10 - j%10 == q/10 - q%10 || j/10 + j%10 == q/10 + q%10) {
return false;
}
}
return true;
}
}