赞
踩
任何一位有志于驾驭计算机的学生,都应该从这些方面入手,重点是:不断学习,反复练习,勤于总结。
究竟什么是算法呢?所谓算法,是指基于特定的计算机模型,旨在解决某一问题而设计的一个指令序列。
算法应具有以下流程:输入与输出;基本操作即加减乘除;确定性即明确的指令序列,可行性即可在对应计算机模型实现的指令序列;有穷性和正确性,有限步骤和正确结果。
算法的特性:计算效率主要包括时间复杂度和空间复杂度。同一种算法也有多种不同的实现方式从而效率不同。
在算法的输入,输出还是中间结果,在计算机中都可以数据的形式表示,以及数据的存储,组织,转移和变换等操作,不同计算平台和模型都会影响整体算法的效率。即数据结构的重要性。我认为算法更像一种思想,但依托于具体问题,具体数据结构的。所以以具体的数据结构来总结与学习算法。最终的结果就是找到最优解或者忧解,也就是归结于问题结果的查找问题。
时间复杂度:随着输入规模的扩大,算法的执行时间将如何增长?
即的表示方法。但是对于小规模可忽略,但对于大规模可关注其渐进上界的大小可用表示,
即,有两种意图:有函数的常系数可以忽略为1,多项式中的低次项可忽略。
如何计算呢?其等于算法所执行基本操作的总次数。基本操作为算数运算、比较、分支、子程序调用与返回等:而这些基本操作均可在常数时间内完成。在最坏情况下的响应速度才是唯一的指标。
当然还有对于应的最好时间复杂度,以及中间时间复杂度。
空间复杂度:时间复杂度是其天然上界,现在主要是用加空间减时间。时间的重要性更强。
关于时间复杂度的取值情况:常数,对数(对于其底数的取值无所谓),对数多项式复杂度(虽不如常数复杂度但仍可接近)
,线性,多项式(实际应用中可接受),指数(实际应用无解,但小规模可能低于多项式)。
实际情况:绝大多数问题并不存在多项式时间算法,有些问题还需要无穷时间。
输入规模:用以描述输入所需的空间规模。
数据结构:数据项之间的某种逻辑次序。包括线性结构,半线性结构,非线性结构。
线性结构:各数据项按照一个线性次序构成一个整体,主要包括向量,列表,栈与队列。
半线性结构:树结构,只要附加某种约束(遍历),便可以在树的元素之间确定某种线性次序。
非线性结构:相互之间均可能存在二元关系的一组对象,此类一般性二元关系,即为图论。
基本数据结构有:向量,列表,栈与队列,二叉树,图,搜索树。
高级数据结构:高级搜索树,词典,优先级队列。
现代数据结构普遍遵从“信息隐藏”的理念,通过统一的接口和内部封装,构成ADT,抽象数据类型。
递归是允许函数和过程进行自我调用。递归 的特点:简洁,减少代码量,提高算法的可读性,保证算法的整体效率。
递归形式:线性递归,二分递归,多分支递归,实现遍历,分治等算法策略。
每一递归实例对自身的调用至多一次。于是每一层上至多只有一个实例,且它们构成一个线性的次序关系。
应用问题可分解为两个独立的子问题,一个对应于单独的某个元素,直接求解。另一个与原问题相同。往往对应与减而治之的策略:没深入一层,问题规模均缩减为一个常数。实例:数组求和。
递归跟踪求递归的时间复杂度和空间复杂度:所有递归实例的创建、执行和销毁所需的时间总和。正比于递归深度。
多递归基,多向递归。
尾递归:在线性递归算法中,若递归调用在递归实例中恰好以最后一步操作的形式出现,则称作尾递归。
尾递归的写法只是具备了使当前函数在调用下一个函数前把当前占有的栈销毁,但是会不会真的这样做,是要具体看编译器是否最终这样做,如果在语言层面上,没有规定要优化这种尾调用,那编译器就可以有自己的选择来做不同的实现,在这种情况下,尾递归就不一定能解决一般递归的问题。例如:
- 一般递归:在函数 A 执行的时候,如果在第二步中,它又调用了另一个函数 B,B 又调用 C.... 栈就会不断地增长不断地装入数据,当这个调用链很深的时候,栈很容易就满 。
- int func(int n)
- {
- if (n <= 1) return 1;
-
- return (n * func(n-1));
- }
- 理论上,在 func(n-1) 返回之前,func(n),不能结束返回。因此func(n)就必须保留它在栈上的数据,直到func(n-1)先返回。
- int tail_func(int n, int res)
- {
- if (n <= 1) return res;
-
- return tail_func(n - 1, n * res);
- }
- 从上可以看到尾递归把返回结果放到了调用的参数里。这个细小的变化导致,tail_func(n, res)不必像以前一样,非要等到拿到了tail_func(n-1, n*res)的返回值,才能计算它自己的返回结果 -- 它完全就等于tail_func(n-1, n*res)的返回值。因此理论上:tail_func(n)在调用tail_func(n-1)前,完全就可以先销毁自己放在栈上的东西。
实际上属于尾递归的算法均可以简单的转化为相应的迭代版本。
二分递归:面对输入规模巨大时运用分而治之策略。每一次递归实例都可能做多次递归,故称作“多路递归”。递归深度为O(logn)。必须保证子问题独立。
若子问题不独立呢?解决方法是:借助一定量的辅助空间,在各子问题求解之后,及时记录下其对应的解答。没当遇到一个子问题,都首先查验它是否已经计算过,以期通过直接查表获得解答,从而避免重复计算。即所谓制表和记忆策略。也可以从递归基出发,自底而上递推得出各子问题的解,即所谓的动态规划算法。
3.动态规划算法
通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
共同点:二者都要求原问题具有最优子结构性质,都是将原问题分而治之,分解成若干个规模较小(小到很容易解决的程序)的子问题.然后将子问题的解合并,形成原问题的解.
不同点:分治法将分解后的子问题看成相互独立的,通过用递归来做。
动态规划将分解后的子问题理解为相互间有联系,有重叠部分,需要记忆,通常用迭代来做。
最优子结构:当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。
描述最优解的结构
递归定义最优解的值
按自底向上的方式计算最优解的值
由计算出的结果构造一个最优解
注意需要需要二维数组用容器,C++动态分配二维数组太坑爹.
问题一:背包问题,可简述为从N个对象中取出若干个对象其值A和不大于sum的组合。输出为一个序列,结果可能并不唯一。然后根据该组合可解决若干问题。需要一个衡量标准来解决该问题。比如对象的值B之和最大。
首先定义问题 :。用F[n-1][sum]来表示。
定义子问题: 用F[j][sum]来表示。
子问题之间的关系:描述j和j-1之间的关系。
若第j个数不加入该组合:则有 F[j][sum] = F[j-1][sum];
若第j个数加入该组合:则有 F[j][sum] = F[j-1][sum-p[j].a]+p[j].b;
统一则为:F[j][sum] = max( F[j-1][sum], F[j-1][sum-p[j].a]+p[j].b);要求F[j][sum],则需要知道F[j-1][sum]和F[j-1][sum-p[j].a]的值。
算法求出该组合序列:
- 算法:从N个数中取出若干个数使其和不大于sum。迭代模式
- int main()
- {
- //int m = 120;
- //int n = 5;
- //vector<int> w = { 0, 40, 50, 70, 40, 20 };
- //vector<int> v = { 0, 10, 25, 40, 20, 10 };
-
- int m, n; //m重量,n数量
- while (cin >> m >> n)
- {
- vector<int> w(n + 1, 0);
- vector<int> v(n + 1, 0);
- for (int i = 1; i <= n; i++)
- {
- int tmp;
- cin >> tmp;
- w[i] = tmp;
- }
- for (int i = 1; i <= n; i++)
- {
- int tmp;
- cin >> tmp;
- v[i] = tmp;
- }
- vector< vector<int> > vec(n + 1, vector<int>(m + 1, 0));
- for (int i = 1; i <= n; i++)
- {
- for (int j = 1; j <= m; j++)
- {
- if (w[i] > j)
- vec[i][j] = vec[i - 1][j];
- else
- {
- int tmp1 = v[i] + vec[i - 1][j - w[i]];
- int tmp2 = vec[i - 1][j];
- vec[i][j] = tmp1 > tmp2 ? tmp1 : tmp2;
- }
- }
- }
- cout << val << endl;
- }
- system("pause");
- }
- 递归版本:
- int main()
- {
- //int m = 120;
- //int n = 5;
- //vector<int> w = { 0, 40, 50, 70, 40, 20 };
- //vector<int> v = { 0, 10, 25, 40, 20, 10 };
- while(cin>>m>>n){
- vector<int> w(n + 1, 0);
- vector<int> v(n + 1, 0);
- for (int i = 1; i <= n; i++)
- {
- int tmp;
- cin >> tmp;
- w[i] = tmp;
- }
- for (int i = 1; i <= n; i++)
- {
- int tmp;
- cin >> tmp;
- v[i] = tmp;
- }
- vector< vector<int> > vec(n + 1, vector<int>(m + 1, 0));
- func(n,120,vec,w,v);
-
- }
- int func(int j,int m,vector<vector<int>>&vec,vector<int>&w,vector<int>&v)
- {
- if(m=0){return 0;}
- if (w[j] > m)
- vec[j - 1][m]=func(j-1,m,vec,w,v)
- vec[j][m]=vec[j - 1][m];
- else
- {
- int tmp1 = v[j] + func(j - 1,m-w[j] ,vec,w,v);
- int tmp2 = func(j-1,m,vec,w,v);
- vec[i][m] = tmp1 > tmp2 ? tmp1 : tmp2;
- }
- }
- 感觉没有迭代简单,还要更加复杂了
从两个字符串中取出若干个相同字符,字符必须成序不要求连续。i,j代表字符串A的范围,j代表B的范围。有最优解的结构。最优解的结构由子结构构成。也是一个二维数组。与前一个问题的共同点是有两个变量,只要确定该两个变量则结果确定。
- public static int lcs(String str1, String str2) {
- int len1 = str1.length();
- int len2 = str2.length();
- int c[][] = new int[len1+1][len2+1];
- for (int i = 0; i <= len1; i++) {
- for( int j = 0; j <= len2; j++) {
- if(i == 0 || j == 0) {
- c[i][j] = 0;
- } else if (str1.charAt(i-1) == str2.charAt(j-1)) {
- c[i][j] = c[i-1][j-1] + 1;
- } else {
- c[i][j] = max(c[i - 1][j], c[i][j - 1]);
- }
- }
- }
- return c[len1][len2];
- }
这里也和前一个问题类似:前一个问题的i,j表示该范围内的最长公共子序列。该子序列与i,j本身没有关系。这里的i,j表示的是以i,j范围内的最长公共子序列,且要求连续,那么如何表示连续呢?因为要获取最长的连续子序列,那么该从所有连续子序列中选择。所以i,j表示的是在i,j范围内以i,j结尾的连续子序列的长度。前一个C[I][J]表示最长公共子序列长度,后一个表示以i,j结尾的连续子序列长度,所以最后还要从其中取一个最大值。所以问题还是关键搞清楚子问题,最优问题是什么,如何从子问题中获取最优解。这个的问题是求取两个字符串的最长连续子串。一种思路是和上题一样,不同范围他的最长连续子串不一样,但是可以推出彼此关系吗?不能,因为是连续,这个条件如何加到C[I][J]中从而改变C[I][J]的含义呢。变的是C[I][J]的含义,从而有不同的公式。
- public static int lcs(String str1, String str2) {
- int len1 = str1.length();
- int len2 = str2.length();
- int result = 0; //记录最长公共子串长度
- int c[][] = new int[len1+1][len2+1];
- for (int i = 0; i <= len1; i++) {
- for( int j = 0; j <= len2; j++) {
- if(i == 0 || j == 0) {
- c[i][j] = 0;
- } else if (str1.charAt(i-1) == str2.charAt(j-1)) {
- c[i][j] = c[i-1][j-1] + 1;
- result = max(c[i][j], result);
- } else {
- c[i][j] = 0;
- }
- }
- }
- return result;
- }
假设有几种硬币,如1 5 10 20 50 100,并且数量无限。请找出能够组成某个数目的找零所使用最少的硬币数。
该题也是如此从一个N个对象中找出若干个数组成的和为sum的组合。确定该组合的最小值,与刚开始的背包问题一样。首先确定要求的结果为最小数量,其变量是[i][sum],其中i为范围。sum为和。C[I][SUM]表示最小数量。要求硬币最少则,面值尽可能大。
所以问题可以描述为:C[I][SUM]
若第I种面值不加入则:C[I][SUM]=C[I-1][SUM];
若第I种面值加入则:C[I][SUM]=C[I-1][SUM%100]+SUM/100;
统一为:C[I][SUM]=min(C[I-1][SUM], C[I-1][SUM%100]+SUM/100).因为I已知,所以可去掉换成一维数组即可。
- 该题也是如此从一个N个已知对象中找出若干个数组成的和为sum的组合。确定该组合的数量最小值,与刚开始的背包问题一样。首先确定要求的结果为最小数量,其变量是[sum],sum为和。C[SUM]表示最小数量。要求硬币最少则,面值尽可能大。sum一知,则组合已知。C[I][SUM]=min(C[I-1][SUM], C[I-1][SUM%100]+SUM/100)。
- int main()
- {
- int sum;
- vector<int> w(0,1,5,10,20,50,100) ;
- int cout=w.size() //sum 为和,且必为自然数
- while (cin >>sum)
- {
- vector<int> C(cout,vector<int>(sum + 1, 0));
- for (int i = 1; i <=sum; i++)
- {
- for (int j = cout-1; j>=1; j--)
- {
- if (i-w[j]>=0)
- {
-
- C[i] = C[i%w[j]]+i/w[j];break;
- }
- }
- }
- cout << C[sum] << endl;
- }
- system("pause");
- }
- 缺点是用二维数组太占空间了。与第一题的背包问题一样。
- 只要J确定,那么C[J]也就确定了。
- 只是需要根据I来分情况。
- 假如要求其组合又该如何解决呢?已知sum,和最小总数。则可根据常数循环得到。
- 若 w 已知但只能用遍历得。
- int main()
- {
- int sum;
- while (cin >>sum)
- {
- int m;
- for(int i=1;i++){
- if(sum-i*i<0)
- m=i-1;break;
- if(sum-i*i==0)
- m=i;break;
- }
- vector<int> C(sum + 1, 0);
- for (int j = 1; j <= sum; j++)
- {
- for(int i=m;i<=1;i--)
- if (j-i*i>=0)
- {
- C[j] = 1 + C[j-i*i];break;
- }
- }
- cout << C[sum] << endl;
- }
- system("pause");
- }
- 感觉这应该就是最优解了。由该式可解决上题。要求是该N个对象已知且有序。
- 背包问题也是如此。可省掉常数空间的解决办法。
本质为:从一个字符串中选出最长的回文子串。回文字符串的子串也是回文,比如P[i,j](表示以i开始以j结束的子串)是回文字符串,那么P[i+1,j-1]也是回文字符串。这样最长回文子串就能分解成一系列子问题了。这样需要额外的空间O(N^2),算法复杂度也是O(N^2)。公式为:
P[I][J]=P[I+1][J-1] && A[I]==A[J];
P[I][J]的值为true 或者false。获取结果为最长回文子串,则length=j-i;
方法二:中心扩展法,以每一个点作为中心进行扩展,计算长度,最后做结果。时间为O(N*N),空间大。很快。
- int main(){
-
- cin.getline(string a);
- int m=a.size();
-
- for(int i=m-1;i>=0;i--){
- for(int j=i;j<m;j++){
-
- if(i=j)
- A[i][j]=true;
- maxlength=1;
- else if(j==i+1){
- A[i][j]=a[i]==a[j];
- maxlength=2;
- }else{第三种情况
- A[i][j]=A[i+1][j-1] && a[i]==a[j];
- }
- }
- }
- }
- 该算法有问题。正确算法是换种方式描述:先解决a,aa两种情况。然后讨论第三种情况:
- 不再根据i,j来描述子串,而是根据子串长度和子串起始位置来描述子串。所以解决办法为:
- for(len=3;len<=length;len++){
- for(int i=0;i<=length-len;i++){
- int j=i+len-1;
- A[i][j]=A[i+1][j-1] && a[i]==a[j];
- if(A[i][j]==true)
- start=i;
- maxlength=len;
- }
- }
- 最让人感觉恼火的就是边界问题。担心越界,如何处理呢?试探法,最小,最大均满足,则成功。
- 如何从另一角度来正确理解数学表达式并解决。该式的解决函数以为:先计算a,aa,aaa,aaaa,长度再变,位置再变,如何确定i,j两者的位置。根据长度变量。
最长递增序列:问题:设L=<a1,a2,…,an>是n个不同的实数的序列,L的递增子序列是这样一个子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<km且aK1<ak2<…<akm。求最大的m值。
第一种方法:排序,然后用LCS来解决:设序列X=<b1,b2,…,bn>是对序列L=<a1,a2,…,an>按递增排好序的序列。那么显然X与L的最长公共子序列即为L的最长递增子序列。这样就把求最长递增子序列的问题转化为求最长公共子序列问题LCS了。
本质为:从N个对象中选出若干个数构成一个组合,条件是该序列有序,并求其最大值。有点类似于求最长公共子序,但要求序列有序。
定义问题;C[j]为以A[J]结尾的递增子序列的最大长度。类似于求最长公共子串(连续)中的设定。应为若设定为不以j结尾,则无法列出数学描述。两者的共同点也是要求序列有序,递增序,连续。
则分为三种情况,算法如下:
- C[j]= C[j-1]+1; a[j]>a[j-1]
- C[j]=C[j-1]; a[j]=a[j-1]
- for(int i=0;i<j;i++){
- if(a[i]<a[j])
- C[j]=max(C[j],C[i]+1)
- };a[j]<a[j-1]
- c[j]表示以j结尾的最长递增子序列的长度
- public static int lcs(vector<int>&a,) {
- int n=a.size();
- vector<int>C(n+1,1);
- int result = 0; //记录最长递增子串长度
- for (int j = 2; j <= n; i++) {
- if(a[j]>a[j-1]) {
- c[j] = C[j-1]+1;
- } else if (a[j]==a[j-1]) {
- c[j] = c[j-1];
- } else {
- for(int i=0;i<j;i++){
- if(a[i]<a[j])
- C[j]=max(C[j],C[i]+1);
- }
- }
- }
- }
- for(int i=0;i<=n;i++){
- result=max(result,C[i]);
- }
- return result;
- }
N方格走的方法也是如此。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。