一 算法概述
算法的概念
通俗地讲,算法是指解决问题的一种方法或一个过程。更严格地讲,算法是由若干条指令组成的有穷序列。
算法的性质
1.输入:有0个或多个由外部提供的量作为算法的输入。
2.输出:算法产生至少一个量作为输出。
3.确定性:组成算法的每条指令是清晰的,无歧义的。
4.有限性:算法中每条指令的执行次数是有限的,执行每条指令的时间也是有限的。
程序与算法不同,程序是算法用某种程序设计语言的具体实现。程序可以不满足算法的性质4。例如,操作系统是一个在无限循环中执行的程序,因而不是一个算法。
描述算法的多种方式:自然语言方式,表格方式,伪代码等。
算法复杂性的高低体现在运行该算法所需要的计算机资源的多少上,所需资源越多,该算法的复杂性越高;反之,所需资源越少,该算法的复杂性越低。对计算机资源,最重要的是时间和空间资源。算法的复杂性有时间复杂性和空间复杂性。
算法复杂性是算法运行需要的计算机资源的量。算法复杂性只依赖于要解的问题的规模、算法的输入和算法本身的函数。
最坏情况、最好情况和平均情况下的时间复杂度从某个角度反映算法的效率,各有局限性,各有用处。可操作性最好且最有实际价值的是最坏情况下的时间复杂度。
渐进符号
类似a=b
类似a<=b
类似a>=b
类似a<b
类似a<b
二 递归与分治策略
直接或间接地调用自身的算法称为递归算法。
阶乘函数可以递归的表示
int factorial(int n){if(n==0) return 1;return n*factorial(n-1);
}
斐波那契数列可以递归的表示
int fibonacci(int n){if(n<=1)return 1;return fibonacci(n-1)+fabonacci(n-2);
}
排列问题
Perm(t){if(t==n)printelsefor i= t~n //让每个都做一下排头,然后把剩下的全排列,就变成了一个全新的全排列。Swap(a[t],a[i]);Perm(t+1);Swap(a[t],a[i]); //交换回来,以便下一次全排列
}
汉诺塔问题
void hanoi(int n,int a,int b,int c){//参数b是目标塔座,参数c是辅助塔座//hanoi函数的任务是把n个圆盘通过辅助c塔,从a塔全搬到目标b塔if(n>0){ //如果n=0,此时没有圆盘需要移动了hanoi(n-1,a,c,b);//如果想把第n个移到目标,就需要把上面的n-1个挪走到辅助塔座move(a,b); // move 是一个圆盘从指定塔到另一个指定塔的移动//执行完hanoi(n-1,a,c,b)之后,就可以移动第n个到目标塔座了hanoi(n-1,c,b,a);//这个时候在把辅助塔座上n-1个放到目标塔座的第n个上面//此时n个汉诺塔的转移全部完成}
}
分治法的基本思想是将一个规模为n的问题分解为k个规模较小的子问题,这些子问题相互独立且与原问题相同。递归地解这些子问题,然后将各个子问题的解合并得到原问题的解。
二分搜索算法,采用分治策略,可在最坏情况下用O(logn)的时间完成搜索任务。
二分法的基本思想是,将n个元素分成个数大致相同的两半,取a[n/2]与x比较。如果x=a[n/2],则找到x,算法终止;如果x<a[n/2],则只在数组a的左半部继续搜索x;如果x>a[n/2],则只在数组a的右半部继续搜索x。
int BinarySearch(int a[],int x,int n){int left = 0;int right = n-1;while(left <= right){int mid=(left+right)/2;if(x==a[middle])return middle;if(x>a[middle])left=middle+1;elseright=middle-1;}return -1;
}
Strassen 矩阵乘法 (分治法 + 减少矩阵乘法次数),矩阵乘法耗费的时间要比矩阵加(减)法耗费的时间多得多,要想改进矩阵乘法的计算时间复杂性,必须减少乘法运算。
棋盘覆盖问题
import java.util.Scanner;public class Main {static int tile = 1;public static void main(String[] args) {Scanner sc=new Scanner(System.in);int k = sc.nextInt();int dr = sc.nextInt(); // 特殊方格所在行int dc = sc.nextInt(); // 特殊方格所在列int[][] Board = new int[(int) Math.pow(2, k)][(int) Math.pow(2, k)];Board[dr][dc]=0;ChessBoard(Board, 0, 0, dr, dc, Board.length);for (int i = 0; i < Board.length; i++) {for (int j = 0; j < Board.length; j++) {System.out.print(Board[i][j] + "\t");}System.out.println();}}public static void ChessBoard(int Board[][],int r_0,int c_0,int dr,int dc,int size){int s;int t;if (size==1) return;t=tile++;s=size/2;if (dr<=r_0+s-1 && dc<=c_0+s-1)ChessBoard(Board,r_0,c_0,dr,dc,s);else{Board[r_0+s-1][c_0+s-1]=t;ChessBoard(Board,r_0,c_0,r_0+s-1,c_0+s-1,s);}if (dr<=r_0+s-1 && dc>=c_0+s)ChessBoard(Board,r_0,c_0+s,dr,dc,s);else{Board[r_0+s-1][c_0+s]=t;ChessBoard(Board,r_0,c_0+s,r_0+s-1,c_0+s,s);}if (dr>=r_0+s && dc<c_0+s) //判断特殊方格是否在左下角子棋盘ChessBoard(Board,r_0+s,c_0,dr,dc,s);else //用t号骨牌覆盖右上角继续覆盖此子棋盘{Board[r_0 + s][c_0 + s - 1] = t;ChessBoard(Board, r_0 + s, c_0, r_0 + s, c_0 + s - 1, s);}if (dr>=r_0+s && dc>=c_0+s) //判断特殊方格是否在右下角子棋盘ChessBoard(Board,r_0+s,c_0+s,dr,dc,s);else //用t号骨牌覆盖左上角继续覆盖此子棋盘{Board[r_0+s][c_0+s]=t;ChessBoard(Board,r_0+s,c_0+s,r_0+s,c_0+s,s);}}
}
合并排序
合并排序算法是用分治策略实现对n个元素的进行排序的算法,其基本思想是:将待排序元素分成大小大致相同的两个子集合,分别对两个子集合进行排序,最终将排好的子集合合并成要求的排好序的集合。
MergeSort(int a[],int left,int right){ //left从左面指,right从右面指
//MergeSort的作用是给数组a做合并排序if(left<right){int i=(left+right)/2; //i取中间MergeSort(a,left,i); //对左面合并排序MergeSort(a,i+1,right); //对右面合并排序Merge(a,b,left,i,right); //排完序后合并到数组b,数组b是一个中间数组Copy(a,b,left,right); //因为是给数组a排序,所以把数组b原封不动挪到数组a}
}
快速排序算法是基于分治策略的另一个排序算法。其基本思想是,对于输入的子数组a[p:r],按以下三个步骤进行排序。
分解:以a[p]为基准元素将a[p:r]划分成3段a[p:q-1],a[q]和a[q+1:r],使a[p:q-1]中的任何一个元素小于等于a[q],而a[q+1:r]中任何一个元素大于等于a[q]。下标q在划分过程中确定。
递归求解:通过递归调用快速排序算法,分别对a[p:q-1]和a[q+1:r]进行排序。
合并:由于对a[p:q-1]和a[q+1:r]的排序使就地进行的,因此在a[p:q-1]和a[q+1:r]都已排好的序后,不需要执行任何计算,a[p:r]则已排好序。
void QuickSort(int a[],int p,int r){
//QuickSort的功能是对a数组下标p到r的部分进行快速排序if(p<r){ //当p=r,就要排的数了int q=Partition(a,p,r); //取下标q,从而分成左右两部分//比a[q]小的放左边,大的放右边QuickSort(a,p,q-1); //对左边的进行排序QuickSort(a,q+1,r); //对右边的进行排序}不停的递归调用,拆成两部分,直到要排的只有一个数(或不存在)就停止
}
三 动态规划
动态规划的基本要素:最优子结构性质,重叠子问题性质
动态规划算法与分治法类似,其基本思想是将待求解问题分解成若干子问题,先求解子问题,再结合这些子问题的解得到原问题的解。与分治法不同的是,适合用动态规划求解的问题经分解得到的子问题往往不是独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,以致最后解决原问题需要耗费指数级时间。然而,不同子问题的数目常常只有多项式量级。在用分治法求解时,有些子问题被重复计算了许多次。如果能够保存已解决的子问题的答案,在需要时再找出已求得的答案,这样可以避免大量的重复计算,从而得到多项式时间算法。(所以动态规划又叫填表法)
动态规划适用于解最优化问题,通常4个步骤:
1.找出最优解的性质,并刻画其结构特征。
2.递归地定义最优解。
3.自底向上的方式计算最优值。
4.根据计算最优值时得到的信息构造最优解。
最优子结构:问题的最优解包含了其子问题的最优解。
重叠子问题:递归自顶向下解问题时,每次产生的子问题不总是新问题,有些子问题被反复计算。动态规划对每个子问题只解一次,然后将解保留在一个表格中。
相同的子问题反复出现,并且不同子问题的个数相对较小时,用动态规划算法是有效的。
矩阵连乘问题
求需要的数乘次数最少
假设A1,A2,A3矩阵维数分别是10*100,100*5,5*50,
如果按((A1A2)A3)计算,3个矩阵连乘积需要数乘次数=10*100*5+10*5*50=7500
如果按(A1(A2A3))计算,3个矩阵连乘积需要数乘次数=100*5*50+10*100*50=75000
以此类推......
将矩阵连乘积A1*A2*...*An记为A[i:j]。
所以A[1:n]的计算量就是A[1:k]的计算量加上A[k+1:An]的计算量,再加上A[1:k]和A[k+1:n]相乘的计算量。
那么A[1:n]如果是最优的,A[1:k]和A[k+1:n]这俩必须也是最优的。
设A[i,j]所需的最少乘次数为m[i][j]。
其中k还未定,k的位置有j-i种可能,k是这j-i个位置中使计算量达到最小的那个位置。
但是这样的话,许多子问题被重复计算多次。
所以还有记录最优断开位置的数组s和记录输入参数的数组p。
void MatrixChain(int *p, int n, int **m, int **s){for(int i=1;i<=n;i++)m[i][i]=0; //填表,只有一个矩阵,都没有矩阵和它乘,数层数次数等于0//也就是表的对角线都填0for(int r=2;r<=n;r++){for(int i=1;i<=n;i++){int j=i+r-1;//对角线向上平移r格得新的斜线,开始走这条新斜线,这个斜线上列比行多r-1(r从2到n)//r最开始是2,j比i多1,总共两个矩阵相乘求连乘次数//当r=n已经把表填完了,两个for嵌套循环结束//很明显这是自底向下的m[i][j]=m[i+1][j]+p[i-1]*p[i]*p[j]; //m[i][j]最开始的初值//默认是i处断开,m[i][j]=0+m[i+1][j]+p[i-1]*p[i]*p[j]//左边乘次数0,右边乘次数m[i+1][j]//左右相乘次数p[i-1]*p[i]*p[j]s[i][j]=i; //s[i][j]最开始的初值for(int k=i+1;k<j;k++){ //for循环之后,最优值t赋给m[i][j]int t=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];if(t<m[i][j]){m[i][j]=t;s[i][j]=k; //i和j之间断开处为s[i][j](也就是k)的时候得到最优值}}}}
}
备忘录方法
与动态规划算法不同的是,备忘录方法的递归方式是自顶向下的,而动态规划算法则是自底向上递归的。因此,备忘录方法的控制结构与直接递归方法的控制结构相同,区别在于备忘录方法为每个解过的子问题建立了备忘录以备需要时查看,避免了相同子问题的重复求解。
最长公共子序列