一、穷举算法
穷举是最简单的一种算法,依赖计算机的强大计算能力,来穷尽每一种可能的情况,从而达到求解问题的目的。
特点:算法效率低,适合没有明显规律可循的场合。
1.穷举算法的执行步骤是怎样的?
(1)对于一种可能的情况,计算其结果
(2)判断结果是否满足要求,如果不满足,执行第(1)步来搜索下一个可能的情况;如果满足,则表示寻找到一个正确的答案。
2.穷举算法有哪些应用?
2.1鸡兔同笼问题
问题:有鸡兔同笼,上有三十五头,下有九十四足,问鸡兔各几何?
分析:若按以往思维,列方程式:x+y=35;2x+4y=94;得出鸡23只,兔12只。但按照计算机的穷举思想,我们可以遍历各种的组合,得出其中一种可能的值。比如,鸡的取值在0~35之间,通过逐个判断是否符合,从而搜索出答案。
public class Test{
public static int chicken,rabbit;
public static void main(String []args) {
if(compute(35,94) == 1){
System.out.println("鸡有"+chicken+"只,兔有"+rabbit+"只");
}else {
System.out.println("无法求解");
}
}
public static int compute(int head, int foot) {
int result,i,j;
result = 0;
//鸡的取值范围在0~35之间
for(i=0;i<=head;i++){
//通过约束,得出兔子的数量
j=head-i;
//当满足判断条件时,就能得出其中一个答案
if(i*2+j*4 == foot) {
result=1;
chicken=i;
rabbit=j;
return result;
}
}
//若循环结束
return result;
}
}
二、递推算法
递推是很常用的算法思想,有广泛的应用。适合有明显规律的场合。
1.递推算法的步骤是怎样的?
(1)根据已知的结果和关系,求解中间结果。
(2)判定是否达到要求,如果没有达到,则继续根据已知结果和关系求解中间结果;如果满足要求,则表示寻找到一个正确的答案。
2.递推算法有哪些应用?
2.1斐波那契数列问题
问题:假设一对两个月大的兔子以后每一个月都可以生一对小兔子,而一对新生的兔子出生两个月后才可以生兔子。例如,1月份出生,3月份才可以产仔。那么假定一年内没有兔子死亡,那么12月份共有多少对兔子呢?
分析:先来分析下每个月兔子的数量。
第1个月,1对兔子出生。兔子共1对。
第2个月,1对兔子一个月大。兔子共1对。
第3个月,1对兔子两个月大,1对兔子出生。兔子共2对。
第4个月,1对兔子三个月大,1对兔子一个月大,1对兔子出生。兔子共3对。
第5个月,1对兔子四个月大,1对兔子两个月大,1对兔子出生一个月大,2对兔子出生。兔子共5对。
…
从上述规律,可看出第3个月的兔子数量是前面两个月的数量之和。
第n个月的兔子数量公式:F(n)=F(n-1)+F(n-2),其中F(1),F(2)的值为1
public class Test2 {
public static void main(String []args) {
int result = fibonacci(12);
System.out.println("12月共有"+result+"只兔子");
}
public static int fibonacci(int n) {
//初始值,已知的结果
if(n ==1 | n==2){
return 1;
}
//递推公式
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
三、递归算法
递归算法是很常用的算法思想,可以简化代码编写,提高程序的可读性。但是,不合适的递归往往导致程序的执行效率变低。
因为递归算法不断反复调用自身来达到求解问题,因此要求待求解的问题能够分为为相同问题的子问题。
分为直接递归和间接递归。
1.递归算法有哪些应用?
1.1阶乘问题
问题:所谓阶乘就是从1到指定数之间的所有自然数相乘的结果。
分析:阶乘为n!=n*(n-1)…1,(n-1)!=(n-1)(n-2)…1,递推公式为n!=n*(n-1)!,采用递推算法。
int fact(int n) {
if(n<=1){
return 1;
}else {
return n*fact(n-1);
}
}
四、概率算法
概率算法往往不能得到问题的精确解,但是在数值计算领域等到了广泛的应用。
1.概率算法的步骤是怎样的?
(1)将问题转化为相应的集合图形S,S的面积容易计算,问题的结果往往对应几何图形中某一部分S1的面积。
(2)然后,向几何图形中随机撒点。
(3)统计几何图形S和S1中的点数。根据S和S1面积的关系及各图形中的点数来计算得到结果。
(4)判断上述结果是否在需要的精度之内,如果未达到精度则执行步骤(2)。如果达到精度,则输出近似结果。
概率算法可分为:
- 数值概率算法
- 蒙特卡洛算法
- 拉斯维加斯算法
- 舍伍德算法
2.概率算法有哪些应用?
2.1 根据蒙特卡洛算法计算圆周率π的值
问题:如何计算圆周率π的值?
分析:计算圆周率的值,可以转化为求圆形的面积,S=π*r2;根据微积分的思想,随机向圆内撒点,x和y的取值分别为0~1之间,通过x2+y2<=1判断点是否落在圆形内,变相求出圆的面积。撒的点越多,值越精确。
public class Test4 {
public static int count = 0;
public static void main(String []args) {
int n = 1000000000;
System.out.println("取点数为"+n+"次,PI的值为"+MontePI(n));
//取点数为1000000000次,PI的值为3.141683144
}
public static double MontePI(int n) {
int sum = 0;
for(int i=0;i<n;i++) {
double x = Math.random();
double y = Math.random();
if((x*x+y*y)<=1) sum++;
}
double PI = 4.0*sum/n;
return PI;
}
}
总结:蒙特卡洛算法采样越多,越近似最优解;例如,在100个苹果找出最大的苹果,随机取出1个苹果放在手中,再随机取出下一个苹果比较大小。当取完最后一个苹果,当然能找到最大的。但是,采样不多时,只能找到接近最大的,当满足我们的要求时,我们就可以停止计算。
五、分治算法
分治算法是一种化繁就简的算法思想。基本思想是将一个计算复杂的问题分为规模较小、计算简单的小问题求解,然后综合各个小问题,得到最终的答案。
1.分治算法的步骤是怎样的?
(1)对于一个规模为N的问题,若该问题比较容易解决,则直接解决;否则执行下个步骤。
(2)将该问题分解为M个规模较小的子问题,这些子问题互相独立,并且与原问题形式相同。子问题与原问题一样只是规模更小。
(3)递归地解决这些子问题。
(4)然后,将各子问题的解合并得到原问题的解。有时,需要求解与原问题不完全一样的子问题,这些子问题看做合并步骤的一部分。
分治算法需要待求解问题能够转化为若干个小规模的相同问题,通过逐步划分,能够达到一个易于求解的阶段而直接进行求解,程序中可以使用递归算法进行求解。
2.分治算法有哪些应用?
2.1硬币问题
问题:一个袋子里有30个硬币,其中一枚是假币,并且假币和真币真假难辨,只知道假币比真币的重量轻一点。如何分辨出假币呢?
分析:利用天平可以分辨出两侧硬币哪个更重,重的一侧会下沉,轻的一侧会抬起,若重量相等,则天平持平。
对于30个硬币,若两两比较,最差情况要称量15次,在最后一次称量中,较轻的假币一侧,天平会升高抬起。
根据分治思想,可将30硬币分为两堆,较轻的一侧必然包含假币。再将较轻的一侧15个硬币分为两堆,若相等,则多余的那个是假币。若不等,则将剩余7枚硬币,再次分为两堆。依次类推,在最差情况下,再将剩余3枚硬币分为两堆。共需要称重4次。
public class Test3 {
public static int count = 0;
public static void main(String []args) {
int[] coins = {
2,2,2,2,2,2,2,2,2,2,
2,1,2,2,2,2,2,2,2,2,
2,2,2,2,2,2,2,2,2,2};
int position = positionOfFakeCoin(coins, 0, coins.length-1);
System.out.println("假币位置在数组的下标为"+position+",重量为"+coins[position]);
System.out.println("称重次数为:"+count);
//假币位置在数组的下标为11,重量为1
//称重次数为:4
}
//难点1,采用递归算法,为重用数组,函数需要传递高低位指针
public static int positionOfFakeCoin(int[] coins, int low, int high) {
//终止条件:当高低位下标相等时,就找出了假币的位置
if(low == high) return low;
int lWeight=0,rWeight=0;
//难点2,对于下标的处理,硬币数有奇数和偶数,分成两堆时,下标需要仔细斟酌
int half = (high- low -1)/2;
//称重过程,实际就是将一堆硬币的重量累加
for(int i=low; i<=(low+half); i++){
lWeight += coins[i];
}
for(int i=low+half+1;i<=(low+2*half+1);i++){
rWeight += coins[i];
}
count++;
//不断找出较轻的那一堆,若相等,则余下那一枚是假币
if(lWeight < rWeight){
return positionOfFakeCoin(coins,low,low+half);
}
if(lWeight > rWeight){
return positionOfFakeCoin(coins,low+half+1,low+2*half+1);
}
return high;
}
}
总结:这是一个规模为30的问题。若规模较小,比如2或3个硬币,则可以直接解决。但是,30个硬币无法直接解决。可分解为15个硬币的子问题。其中重点是,如何将规模为N的问题分解成规模为M的子问题。例如,此问题是通过天平判断,
30个硬币中有一枚假币,分成两堆,转为其中一堆中15个硬币中有一枚假币。对于奇数枚硬币,分成两堆时,余下的那枚硬币,也能通过推理判断。实际上,30个硬币被分成三类,两个需要称重判断的硬币,和余下的一枚硬币。
2.2股票买卖问题
问题:假设需要在某个周期内买卖某个公司的股票,例如上图中开盘100元/股,在此后的每天中可以买入或卖出此股票一次。在某天中买入此股票,在该天之后才能卖出。若希望达到利益最大化,那么怎样操作?
分析:问题可以转化为股票涨跌的变化,若根据暴力穷举算法来求解,可以有n(n-1)/2种可能的买入和卖出的组合,找出盈利最大的组合,是一种σ(n2)的运算级。运算效率较低
但是对于输入,我们可以从另外的角度将问题转化为股票的涨跌。我们的目的是寻找一段日期,使得从第一天到最后一天的净现值最大。那么问题就变成,寻找数组A的和最大的非空连续子数组。我们称这样的子数组为最大子数组。
乍一看,这样的问题转化,似乎对求解并没有什么帮助,依然要求解n(n-1)/2种组合的值。每一个子数组都要花费线性时间,然而当求解σ(n2)个子数组的和时,我们可以通过分治策略,优化计算方式。
注意:我们说“一个最大子数组”,而不是“最大子数组”,因为可能有多个组合达到最大值。
只有当数组包含负数的时候,最大子数组才有意义。否则,直接是整个数组达到最大值。
public class Test5 {
public static void main(String[] args) {
int[] a = {13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7};
int[] result = findMaxSubarray(a, 0, a.length - 1);
System.out.println("低位下标为" + result[0]+",对应的值为"+a[result[0]]);
System.out.println("高位下标为" + result[1]+",对应的值为"+a[result[1]]);
System.out.println("值为" + result[2]);
//低位下标为7,对应的值为18
//高位下标为10,对应的值为12
//值为43
}
//求解子数组的最大和
public static int[] findMaxSubarray(int[] a, int low, int high) {
//构造数组分别,存入低位,高位下标,和该连续数组的值
int[] result = {0, 0, 0};
//基本情况,当数组只有一个值时,返回该数组的数据
if (low == high) {
result[0] = low;
result[1] = high;
result[2] = a[low];
return result;
}
//中间下标,中间数将数组大致分为两部分,不需要长度完全相等的数组
int mid = (high + low) / 2;
//左侧子数组
int[] lSubarray = findMaxSubarray(a, low, mid);
//右侧子数组
int[] rSubarray = findMaxSubarray(a, mid + 1, high);
//交叉子数组
int[] mSubarray = findMaxCrossSubarray(a, low, mid, high);
if ((lSubarray[2] >= rSubarray[2]) && (lSubarray[2] >= mSubarray[2])) {
//若左子数组最大,则返回左子数组的结果
return lSubarray;
} else if ((rSubarray[2] >= lSubarray[2]) && (rSubarray[2] >= mSubarray[2])) {
//若右子数组最大,则返回右子数组的结果
return rSubarray;
} else {
//若交叉子数组最大,则返回交叉子数组的结果
return mSubarray;
}
}
//求解交叉子数组的最大和
public static int[] findMaxCrossSubarray(int[] a, int low, int mid, int high) {
int[] result = {0, 0, 0};
int lSum = 0, rSum = 0;
int lMax = Integer.MIN_VALUE, rMax = Integer.MIN_VALUE;
for (int i = mid; i >= low; i--) {
lSum += a[i];
if (lSum >= lMax) {
lMax = lSum;
result[0] = i;
}
}
for (int j = mid + 1; j <= high; j++) {
rSum += a[j];
if (rSum >= rMax) {
rMax = rSum;
result[1] = j;
}
}
result[2] = lMax + rMax;
return result;
}
}
总结:采用分治策略,最大连续子数组可以分解为左右两部分子数组和交叉部分的子数组。即原问题由分解后规模更小的子问题和不完全一样的子问题组成。规模更小的子问题可以通过递归的方式解决。不完全一样的子问题,需要单独解决,运算是线性的。综合三个子问题的解,合并得出最终问题的答案。
该算法每次计算的运算规模,都缩小一半,也就是是nlgn级数的运算。而交叉部分的子数组的运算,是n级数的运算。因此运用分治算法后,运算级数从n2变为了nlgn的级数,效率更快了。
3.分治策略的术语
(1)当子问题足够大,需要递归求解时,称为递归情况(recursive case)。
(2)当子问题足够小,不再需要递归时,我们说递归已经“触底”,称为基本情况(base case)。
4.分治算法与其他思想的联系
递归式与分治算法紧密相关,因为递归式可以自然刻画分治算法的运行时间。
可求解递归式的方法,得出算法的渐进界方法。
可求解形如下面公式的递归的界:
T(n)=aT(n/b)+f(n)
其中a>=1,b>1,f(n)是给定的函数。
它刻画了这样一个算法:将生成a个子问题,每个子问题的规模是原来的1/b,分解和合并步骤总共花费f(n)。
递归式的技术细节
类似于2.1的硬币问题,当n为奇数时,2个子问题的规模分为[n/2]和[n/2],即除以2之后取整的值,而不是n/2,因为当n是奇数时,n/2不是整数。当然对于java来说,当除不尽时恰恰是取整的,对于其他语言,就需要注意了,可能得到小数。
对于该问题的公式为:T(n)=T([n/2])+T([n/2])+σ(n)