1.分治策略
分治 策略,从字面上也能看出,是将原有问题分解,使其具有更小的规模,从最小规模向上层层递归并返回 ,最终解决问题。分治策略应用于求股票最大利润问题、矩阵乘法等一系列问题。对于这些已经解决 的问题,我想说这虽然很重要,但是不是最重要的,重要的你要学会分治的思想,然后利用这些思想去解决新的问题,去设计算法,寻找更优的算法,更少的时间复杂度。递归的时间复杂度一般都与lgn有关系,分治一般都是平分原来问题的规模,这样才能使递归树的层数最少。
递归:分治一般是平分,可以减少运行时间,而递归不一定部分,当嵌套循环较多时,可以使用递归,只在递归中写上一个递归(循环)结束的条件即可。
先来说一下求解递归问题的步骤:分解 、解决、合并。这几个字应该是分治的核心了。
分解:将原来的问题分解成多个子问题(一般是平分),子问题的形状要求的东西全 和原问题一样。注意:谨慎处理边界问题,中位数mid,是给右子数组还是左子数组,有时候只是比较一下,然后就把它舍弃了,比如折半查找。注意:绝对不可能每个子问题都包含mid,会无限递归,比如只剩下(0,1)两个位置元素时。此时就要看将mid给谁能把所有的情况都包括。最大子数组问题就能很好的说明这个问题。
解决:递归的求解子问题。看清那些是和原来问题一样,需要递归的(调用自身),还有一些子情况,可能不是递归的。只要子问题足够小就停止递归,直接解决。
合并:假设当子问题都解决了,合并为最终结果。有时只是简单地一个return或者将所有子问题的一个整合。
2.最大子数组
实际问题:根据股票每天的价格如何求解,怎样购买才能利润最大,利润最大就要买进和卖出的差价最大,可以把相邻元素的的价格差写成一个数组,进行分析,也就是最大子数组问题。
我们一般人,通常会想到使用暴力的发发找出所有的子数组,然后比较。这样的时间复杂度为:n的平方,但是使用分治,就能把问题优化成nlgn;注意 :分解子数组时 把mid元素分给左边数组,与求交叉状态时的最大子数组时的(可以简单地记成:哪一半子数组拥有mid哪一半子数组就要在sum上加上mid元素进行比较,但是不能两边都加,会重复求和)
for(int j=mid+1;j<=high;j++)中的j的初始值是对应的,因为要包含所有的情况
,还有两种临界情况只含有左边部分到mid,和只含有mid到右边部分,把mid分到做子数组包含了第一种临界情况,交叉情况包含了:第二种临界情况。这样上图:a的分法就包含了所有的情况。
因为暴力求解在解决n较小的时候还是有很多优势的,所以能否可以将最小子数组的分治求解,在子问题规模小于某个规模的时候使用暴力求解。
/**
* 最大子数组的暴力求解方法:找出所有的子数组进行判断,注意子数组只有一个元素时,也要进行
* 一次是否为当前最大值的比较。
*/
public int [] vioMaxSunArray(int a[],int low,int high)
{
int result[]=new int [3];
result[2]=-1000;
for(int i=low;i<=high;i++)
{
int sum=0;
for(int j=i;j<=high;j++)
{
sum+=a[j];
if(sum>result[2])
{
result[2]=sum;
result[0]=i;
result[1]=j;
}
}
}
return result;
}
/**
* 分治法求最大子数组问题 时间复杂度 :nlgn
* 思路:求出最大子数组可能出现的情况 每种情况求出对应的最大子数组,然后比较那种情况的最大子数组
* 最大
* 找出交叉情况时:最大子数组
*/
public int[] findMaxCrossingSubArray(int []a,int low,int mid ,int high)
{
int result[]=new int [3];
int sum=0,leftSum=-1000,rightSum=-1000;
int maxLeft=0,maxRight=0;
for(int i=mid;i>=low;i--)
{
sum+=a[i];
if(sum>leftSum)
{
leftSum=sum;
maxLeft=i;
}
}
//注意右子数组是不包含mid元素的,所以sum求和 不要再次加入mid,后面左右最大子数组求和时就加了
//两次mid了
sum=0;
for(int j=mid+1;j<=high;j++)
{
sum+=a[j];
if(sum>rightSum)
{
rightSum=sum;
maxRight=j;
}
}
result[0]=maxLeft;
result[1]=maxRight;
result[2]=leftSum+rightSum;
return result;
}
/**
* 最大子数组求解,分解(怎么分解),解决(停止递归的条件)合并(当问题都解决了再怎么合并为原问题的解)
*/
public int[] maxSubArray(int a[],int low,int high)
{
if(low==high)
return new int[]{low,high,a[low]};
else
{
int mid=(low+high)/2;
int []left=maxSubArray(a, low, mid);
int []right=maxSubArray(a, mid+1, high);
int cross[]=findMaxCrossingSubArray(a, low, mid, high);
return left[2]>right[2]?(left[2]>cross[2]?left:cross):(right[2]>cross[2]?right:cross);
}
}
进行交叉求解时:你可能会惊奇,右子数组可能都是负的就不能加在最大子数组上了?不要担心,递归求解的合并步骤会比较出最大的子数组之和。因为只在左子数组上一定能求出比他大的。这三种情况加起来才是总的情况。
第三种求解方法,为联机算法,只适用于最大子数组和为正数的情况,当最大子数组和为负数的时候会返回0;
这种时间的复杂度仅是n,是求解为正数的最大子数组的完美解法。还有一种时间复杂度是:n的三次方,这里就不再赘述了,这么脑残的时间复杂度也用不到。
/**
- * Liner-time max imum contiguous subsequence sum algorithm
- * 该算法的一个附带的优点是,它只对数据进行一次扫描,一旦a[i]被读入并被处理,他就不需要在被存储,
- * 所以,当数组在磁盘或通过互联网传送时,就可以按照顺序读入,在内存中不被储存数组的任何部分,。而且在任何时刻该算法都可以
- * 对已经读入的数据给予序列问题的正确答案(其他几个不具有这个特征),具有这个特征的算法叫联机算法(on-line algorithm).
- * 仅需要常量空间并以线性时间运行的联机算法几乎是完美的算法
- * */
* @param a
* @param low
* @param high
* 最大子数组的联机求解,此方法只能够求出,最大数组和为整数的情况,全是
* 负数会返回0;
*/
public int[] maxSubArrayOnLine(int a[],int low,int high)
{
int []result=new int [3];
int sum=0,flag=0,second=0;
result[2]=0;
for(int i=low;iresult[2])
{
result[2]=sum;
if(flag==0)
{
flag=1;
result[0]=i;
}
else if(flag==2)
{
flag++;
result[0]=second;
}
result[1]=i;
}
else if(sum<0)
{
sum=0;
flag=2;
second=i+1;
}
}
return result;
}
总结:边界问题很重要。这些算法,一直再算新的子数组的和,一直比较,当大于当前最大和时才会更新。
3.分治求逆序对
求解逆序对类似于归并排序,分解成子问题,求各个子数组的逆序对,再求交叉的逆序对,交叉的逆序对,第一个cross采用n的平方的时间复杂度,第二个是在进行合并排序的基础上求解逆序对排序后只进行遍历一次表即可时间复杂度:nlgn。出来第一个逆序对后,那么L后面的元素也能与R的这个元素构成逆序对。
/**
* @param a
* @param low
* @param mid
* @param high
* 找出逆序对分治思想
*/
public void cross(int a[],int low,int mid,int high)
{
for(int i=low;i<=mid;i++)
for(int j=mid+1;ja[j])
System.out.println(a[i]+" "+a[j]);
}
public int cross1(int a[],int p,int q,int r)
{
int n1=q-p+1;
int n2=r-q;
int count=0;
int []L=new int[n1+1];
int []R=new int[n2+1];
System.arraycopy(a, p,L, 0,n1);
System.arraycopy(a, q+1,R, 0,n2);
L[n1]=1000;
R[n2]=1000;
int k=0,j=0;
for(int i=0;i
4.矩阵的乘法Strassen算法
这个算法太难了,恕我愚钝,真心 不会,网上找的代码贴上。
#include
using namespace std;
const int N = 6; //Define the size of the Matrix
template
void Strassen(int n, T A[][N], T B[][N], T C[][N]);
template
void input(int n, T p[][N]);
template
void output(int n, T C[][N]);
int main() {
//Define three Matrices
int A[N][N],B[N][N],C[N][N];
//对A和B矩阵赋值,随便赋值都可以,测试用
for(int i=0; i
void input(int n, T p[][N]) {
for(int i=0; i>p[i][j];
}
}
}
template
void output(int n, T C[][N]) {
cout<<"The Output Matrix is :"<
void Matrix_Multiply(T A[][N], T B[][N], T C[][N]) { //Calculating A*B->C
for(int i=0; i<2; i++) {
for(int j=0; j<2; j++) {
C[i][j] = 0;
for(int t=0; t<2; t++) {
C[i][j] = C[i][j] + A[i][t]*B[t][j];
}
}
}
}
template
void Matrix_Add(int n, T X[][N], T Y[][N], T Z[][N]) {
for(int i=0; i
void Matrix_Sub(int n, T X[][N], T Y[][N], T Z[][N]) {
for(int i=0; i
void Strassen(int n, T A[][N], T B[][N], T C[][N]) {
T A11[N][N], A12[N][N], A21[N][N], A22[N][N];
T B11[N][N], B12[N][N], B21[N][N], B22[N][N];
T C11[N][N], C12[N][N], C21[N][N], C22[N][N];
T M1[N][N], M2[N][N], M3[N][N], M4[N][N], M5[N][N], M6[N][N], M7[N][N];
T AA[N][N], BB[N][N];
if(n == 2) { //2-order
Matrix_Multiply(A, B, C);
} else {
//将矩阵A和B分成阶数相同的四个子矩阵,即分治思想。
for(int i=0; i
递归总结:1.递归要有递归结束,结束条件是否可行,就要看,能否结束递归
2.当函数中仅需要递归一部分时,不需要把整个函数递归,可以把要递归的部分单独封装起来。这样就能只递归这些封装的部分。
3.一个递归算法,划分越平衡,树的高度越低,算法的时间复杂度越低。