HRBUXG ACM 第一章 算法概述

1 篇文章 0 订阅

第1章       算法概述

算法(Algorithm):一系列解决问题的清晰指令,代表着用系统的方法描述解决问题的策略机制。

1.1 欧几里得算法(辗转相除法)

功能:求两个数的最大公因子数
输入:两个非负整数m, n
输出:m和n的最大公因子数
步骤:
Step 1:如果m<n,则交换 mn
Step 2:令 rm/n 的余数
Step 3:如果 r=0,则输出m;否则令m=nn=r 并转向步骤2.
#include<bits/stdc++.h>
using namespace std;
typedef int Type;

Type GCD_Loop(Type m, Type n)
{
    if(m < n)
        GCD(n, m);
    int r = m % n;
    while(r) {
        m = n;
        n = r;
    }
    return m;
}

/*
原理:
    gcd(m, n)=gcd(n, m%n)(m>n且 m mod n不为0)
*/
Type GCD_Recursive (Type m, Type n)
{
    if(m < n)
        GCD(n, m);
    if(n == 0)
        return m;
    else
        return GCD(n, m % n);
}

1.2 算法的特征

1. 有穷性
    算法在执行有限步之后必须停止

2. 确定性
    算法的每一个步骤必须有确切的定义,要执行的每一个动作都是清晰无歧义的

3. 输入
    一个算法有0个或者多个输入,作为算法开始执行前的初始值和初始状态。0个输入是指算法本身定出了初始条件

4. 输出项
    一个算法由一个或者多个输出,以反映对输入数据加工后的结果。没有输出项的算法是毫无意义的

5. 可行性
    存在有限时间内完成计算过程    PS:一个实用的算法,不仅要求步骤有限,同时要求运行这些步骤所花费的时间是人们可以接受的。

1.3 算法的设计

       算法设计的整个过程,可以包含对文体需求的说明、数学模型的拟制、算法的详细设计、算法的正确性验证、算法的实现、算法的分析、程序测试以及文档资料的编制。

       同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。一个算法的评价主要从时间复杂性和空间复杂性两方面来考虑。

       算法可大致分为基本算法数据结构的算法数论与代数算法计算几何的算法图论的算法动态规划以及数值分析加密算法排序算法检索算法随机化算法并行算法。算法大致分为以下三类。

(1)有限的、确定性算法。这类算法在有限的一段时间内终止。它们可能要花很长时间来执行指定的任务,但仍将在一定的时间内终止。这类算法得出的结果常取决于输人值。

(2)有限的、非确定算法。这类算法在有限的时间内终止。然而,对于一个(或-些)给定的数值,算法的结果并不是唯一的或确定的。

(3)无限的算法。指那些由于没有定义终止定义条件,或定义的条件无法由输人的数据满足而不终止运行的算法。通常,无限算法的产生是由于未能确定的定义终止条件.

1.3.1 穷举搜索法

       穷举搜索法(xhausive Sarch Agoithm)是对可能是解的众多候选解按某种顺序进行逐一枚举和检验,并从中找出那些符合要求的候选解作为问题的解。

       穷举算法的特点是算法简单,但运行时所花费的时间量大。有些问题所列举出来的情况数目会大得惊人,就是用高速计算机运行,其等待运行结果的时间也将使人无法忍要用穷举算法解决问题时,应尽可能将明显不符合条件的情况排除在外,以尽快取得问题的解。

1.3.2 迭代算法

       迭代算法(Iterative Algorithm)是数值分析中通过从一个初始估计出发寻找一系列近似解来解决问题(一般是解方程或者方程组)的过程,尉氏县这一过程所使用的方法统称为迭代法。

       迭代法适用于求方程或者方程组近似根的一种常用的算法设计方法。设方程为f (x)=0,用某种数学方法导出等价的形式x=g (x),然后按以下步骤执行。

(1)选一个方程的近似根,赋给变量x0

(2)将x0的值保存在变量x1,然后计算g (x1),并将结果存于变量x0

(3)当x0与x1的差的绝对值还小于指定的精度要求时,重复步骤(2)的计算。

       若方程有根,并且用上述方法计算出来的近似根序列收敛,则上述方法求得的x0就认为是方程的根。

例题: 求解f(x) =x^3-x-1=0在区间(1,1.5)内的根。

1)首先我们将方程写成这种形式:

x = x + 1 3 x=\sqrt[3]{x+1} x=3x+1
2)用初始根x0=1.5带入右端,可以得到

x 1 = x 0 + 1 3 ≈ 1.35721 {x_1}=\sqrt[3]{x_0+1}\approx1.35721 x1=3x0+1 1.35721
3)x0和x1的值相差比较大,所以我们要继续迭代求解,将x1再带入公式
x 2 = x 1 + 1 3 ≈ ⋯ ⋯ {x_2}=\sqrt[3]{x_1+1}\approx \cdots\cdots x2=3x1+1
直到我们我们得到的解的序列收敛,即存在极值的时候,迭代结束。

下面是这个方程迭代的次数以及每次xi的解(i=0,1,2…)

kxk
01.5
11.35721
21.33.86
31.32588
41.32494
51.32476
61.32473
71.32472
81.32472

算法核心

#define eps 1e-8
int main()
{
    x0 = 初始近似根;
    do{
        x1 = x0;
        x0 = g(x1); //按特定的方程计算新的近似根
    }while(fabs(x0-x1) > Epsilon);
    printf("方程的近似根是%f\n",x0);
}
1.3.3 递推算法

       递推算法(Recursive Algorithm)是利用问题本身所具有的一种递推关系求问题解的一种方法。它把问题分成若干步,找出相邻几步的关系,从而达到目的,称为递推法。

       设要求问题规模为n的解,当n=0或1时,解或为已知,或能非常方便地得到解。能采用递推法构造算法的问题有重要的递推性质,即当得到问题规模为i-1的解后,由问题的递推性质,能从已求得的规模为1,2,…,i-1 的一系列解,构造出问题规模为i的解。这样,程序可从i=0或1出发,重复地,由已知至i-1规模的解,通过递推,获得规模为i的解,直至得到规模为n的解。

例如:斐波那契数列

F ( n ) = {                   1                    , n = 0 或 者 1 F ( n − 1 ) + F ( n − 2 )    , n ≥ 2 F(n) =\begin{cases} \space \space \space \space \space \space \space \space \space \space \space \space \space \space \space \space \space 1\space \space \space \space \space \space \space \space \space \space \space \space \space \space \space \space \space \space ,n=0或者1\\ F(n-1)+F(n-2) \space \space ,n\geq2\end{cases} F(n)={                 1                  ,n=01F(n1)+F(n2)  ,n2

1.3.4 递归算法

       递归算法( Recursive Algorithm)是一种直接或者间接地调用自身的算法。在计算机编写程序中,递归算法对解决一大类问题是十分有效的,它往往使算法的描述简洁而且易于理解。

       能采用递归描述的算法通常有这样的特征:为求解规模为n的问题,设法将它分解成规模较小的问题,然后从这些小问题的解方便地构造出大问题的解,并且这些规模较小的问题也能采用同样的分解和综合方法,分解成规模更小的问题,并从这些更小问题的解构造出规模较大问题的解。特别地,当规模n=0或1时,能直接得解。

       用递归算法解决问题具有以下特点。

(1)递归就是在过程或函数里调用自身;
(2)在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口;
(3)递归算法解题通常显得很简洁,但递归算法解题的运行效率较低;
(4)在递归调用的过程当中系统为每一层的返回点、局部变量等开辟堆栈来存储。递归次数过多容易造成堆栈溢出等。

例题:汉诺塔问题来自一个古老的传说:在世界刚被创建的时候有一座钻石宝塔(塔A), 其上有64个金碟。所有碟子按从大到小的次序从塔底堆放至塔顶。紧挨着这座塔有另外两个钻石宝塔(塔B和塔C)。 从世界创始之日起,婆罗门的牧师们就一直在试图把塔A上的碟子移动到塔C上去,其间借助于塔B的帮助。 每次只能移动一个碟子,任何时候都不能把一个碟子放在比它小的碟子上面。当牧师们完成任务时,世界末日也就到了。

解决思路

1)情况一:塔A上只有1个碟子时:

     Step:直接将塔A上的碟子由塔A移动到塔C;

2)情况二:塔A上碟子数量超过1个时:

     Step1:将塔A上的n-1个碟子借由塔C移动到塔B上;

     Step2:将塔A上剩下的一个碟子直接移动到塔C上;

     Step3:将塔B上n-1个碟子借由塔A移动到塔C;

#include<bits/stdc++.h>
using namespace std;
 
void Move(int n, char A, char B);
void Hanoi(int n, char A, char B, char C)
{
	if( n == 1)                  
		Move(1, A, C);
	else                         
	{
		Hanoi(n-1, A, C, B);
		Move(n, A, C);
		Hanoi(n-1, B, A, C);
	}
}
 
void Move(int n, char A, char B)
{
	cout << " number " << n << ":" << A << " ---> " << B << endl;
}
 
int main()
{
	char X = 'A';
	char Y = 'B';
	char Z = 'C';
	int	n;
	cout << "Please input the nunber: " << endl;
	cin >> n;
	Hanoi(n, X, Y, Z);
	return 0;
}
1.3.5 分治算法

       分治算法(Divide-and-Conquer Algorithm) 是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题,直到最后子问题可以简单地直接求解,原问题的解即子问题解的合并。

       如果原问题可分割成k个子问题(1<k≤n),且这些子问题都可解,并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。

1.3.6 贪心算法

       贪心算法(Greedy Algorithm)也称贪婪算法。在对问题求解时,总是会做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,所做出的仅是在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题都能产生整体最优解或者是整体最优解的近似解。

       贪心算法的基本思路如下。

(1)建立数学模型来描述问题;
(2)把求解的问题分成若千个子问题;
(3)对每一子问题求解,得到子问题的局部最优解;
(4)把子问题的局部最优解合成原来解问题的一一个解。
1.3.7 动态规划算法

        动态规划算法(Dynamic Programming Algorithm)是一种在数学和计算机科学中使用的,用于求解包含重叠子问题的最优化问题的方法。其基本思想是,将原问题分解为相似的子问题,在求解的过程中通过子问题的解求出原问题的解。动态规划的思想是多种算法的基础,被广泛应用于计算机科学和工程领域。

       动态规划程序设计是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。不像前面所述的那些算法,具有一-个标准的数学表达式和明确清晰的解题方法。动态规划程序设计往往是针对一种最优化问题,由于各种问题的性质不同,确定最优解的条件也互不相同,因而动态规划的设计方法对不同的问题,有各具特色的解题方法,而不存在一种万能的动态规划算法,可以解决各类最优化问题。

1.3.8 回溯算法

       回溯算法(BackTrackingAlgorithm)是一种选优搜索法,按选优条件向前搜索,以达到目标。当探索到某一步时,发现原先的选择并不优或达不到目标,就退回一.步重新选择,这种走不通就退回再走的技术称为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。人们比较熟悉的迷宫问题算法,采用的就是典型的回溯方法。

       回溯方法解决问题的过程是先选择某一可能的线索进行试探,每一步试探都有多种方式,将每一方式都一一试探, 如有问题就返回纠正,反复进行这种试探再返回纠正,直到得出全部符合条件的答案或是问题无解为止。由于回溯方法的本质是用深度优先的方法在解的空间树中搜索,所以在算法中都需要建立一个堆栈,用来保存搜索路径。一旦产生的部分解序列不合要求,就要从堆栈中找到回溯的前一一个位置,继续试探。

1.3.9 分支限界算法

       分支限界算法(BranchandBoundAlgorithm)是一种在表示问题解空间的树上进行系:统搜索的方法。回溯法使用了深度优先策略,而分支限界法一般采用广度优先策略,同时还采用最大收益(或最小损耗)策略来控制搜索的分支。

       分支限界法的基本思想是对有约束条件的最优化问题的所有可行解(数目有限)空间进行搜索。该算法在具体执行时,把全部可行的解空间不断分割为越来越小的子集(称为分支),并为每个子集内的解计算一个下界或上界(称为定界)。在每次分支后,对所有界限超出已知可行解的那些子集不再做进一步分支,解的许多子集(即搜索树上的许多结点)就可以不予考虑了,从而缩小了搜索的范围。这一过程一直进行到找出可行解的值不大于任何子集的界限。因此,这种算法一般可以求得最优解。


1.4 算法的复杂性

       算法的复杂性是算法效率的度量,在评价算法性能时,复杂性是一个重要的依据。

       算法的复杂性的程度与运行该算法所需要的计算机资源的多少有关,所需要的资源越多,表明该算法的复杂性越高;所需要的资源越少,表明该算法的复杂性越低。

       算法在计算机上执行运算,需要一定的存储空间存放描述算法的程序和算法所需的数据,计算机完成运算任务需要一定的时间。根据不同的算法写出的程序放在计算机上运算时,所需要的时间和空间是不同的,算法的复杂性是对算法运算所需时间和空间的一种度量。

       对于任意给定的问题,设计出复杂性尽可能低的算法是在设计算法时考虑的一个重要目标。当给定的问题已有多种算法时,选择其中复杂性最低者,是在选用算法时应遵循的一个重要准则。算法的复杂性分析对算法的设计或选用有着重要的指导意义和实用价值。

       复杂性与所解决问题的规模有关。算法求解问题的输入量称为问题的规模(Size),一般用一个整数n作为表示问题规模的量。

       例如,排序问题中n为排序元素个数;图的问题中n是图的顶点数;矩阵中的n为矩阵的阶数等。

       一般把时间复杂性和空间复杂性分开,并分别用T和S来表示:

1)算法的时间复杂性一般表示为T(n);
2)算法的空间复杂性一般表示为S(n)。
其中n是问题的规模(输入大小)。
1.4.1 时间复杂性

       算法的时间复杂性(Time Complexity)是指执行算法所需要的时间。一般来说,计算机算法是问题规模n的函数f(n),算法的时间复杂性也因此记做T(n)=O(f(n))。因此问题的规模n越大,算法执行的时间的增长率与f(n)的增长率正相关,称作渐进时间复杂性(Asymptotic Time Complexity)。

1.4.1.1 复杂性渐近性态

       定义1.1    设算法的执行时间为T(n),如果存在T(n),使得
lim ⁡ T ( n ) − T ∗ ( n ) T ( n ) = 0 \lim\frac{T(n)-T^*(n)}{T(n)}=0 limT(n)T(n)T(n)=0
       称T*(n)是T(n)当n→∞时的渐进性态,称T*(n)为算法A当n→∞时的渐进复杂性,因为在数学上称T*(n)是T(n)当n→∞时的渐进表达式。直观上,T*(n)是T(n)中略去低阶项所留下的主项,所以它无疑比T(n)来的简单。比如当T(n)=3n2+4nlog2n+7时,T(n)的一个答案是3n2,显然3n2比3n2+4nlog2n+7简单的多。

1.4.1.2 运行时间的上界(O)

       在一般情况下,当输人规模大于或等于某个阈值n0时,算法运行时间的上界是某个正常数的g(n)倍,就称算法的运行时间至多是O(g(n))。

       定义1.2    令n为自然数集合,R+为正实数集合。函数f(n)和g(n)∈R+,若存在自然数n0和正常数c使得对所有的n≥n0,都有f(n)≤cg(n),就称函数f(n)的阶至多是是O(g(n))。因此,如果存在limf(n)/g(n),则
lim ⁡ n → ∞ f ( n ) g ( n ) ≠ ∞ , 蕴 含 着 f ( n ) = O ( g ( n ) ) \lim_{n\rightarrow\infty}{\frac{f(n)}{g(n)}}\neq \infty,蕴含着f(n)=O(g(n)) nlimg(n)f(n)̸=f(n)=O(g(n))
       这个定义表明f(n)的增长最多像g(n)的增长那样快。这时称O(g(n))是f(n)的上界。

       按照O符号的,有以下运算规则:

	(1)O(f)+O(g)=O(max(f,g))
	(2)O(f)+O(g)=O(f+g)
	(3)O(f)*O(g)=O(f*g)
	(4)O(cf(n))=O(f(n)),其中c是一个正的常数
	(5)如果g(n)=O(f(n)),则O(f)+O(g)=O(f)
	(6)f=O(f)

       常见的时间复杂性按数量级递增排列依次是:常数O(1)、对数阶O(logn)、线性阶O(n)、线性对数阶O(nlogn)、平方阶O(n2)、立方阶O(n4)、…、k次方阶O(nk)、指数阶O(2n)、阶乘阶O(n!)。

1.4.1.3 运行时间的下界(Ω)

       在一般情况下,当输入规模等于或大于某个阈值n0时,算法运行时间的下界是某一个正常数的g(n)倍,就称算法的运行时间至多是Ω(g(n))。

       定义1.3    令n为自然数集合,R+为正实数集合。函数f(n)和g(n)∈R+,若存在自然数n0和正常数c使得对所有的n≥n0,都有f(n)≥cg(n),就称函数f(n)的阶至多是是Ω(g(n))。因此,如果存在limf(n)/g(n),则
lim ⁡ n → ∞ f ( n ) g ( n ) ≠ ∞ , 蕴 含 着 f ( n ) = Ω ( g ( n ) ) \lim_{n\rightarrow\infty}{\frac{f(n)}{g(n)}}\neq \infty,蕴含着f(n)=Ω(g(n)) nlimg(n)f(n)̸=f(n)=Ω(g(n))
       这个定义表明一个算法的运行时间增长至少像g(n)那样快。他被广泛的用来表示解一个特定问题的任何算法的时间下界。

       例如:在n个大小不同、顺序凌乱的数据中,寻找一个最小的数据,至少需要n-1次比较操作,它的阶是Ω(n)。

1.4.1.4 运行时间的准确界(θ)

       在一般情况下,当输入规模等于或大于某个阈值n0时,算法的运行时间以c1g(n)为其下界,以c2g(n)为其上界(0<c1≤c2),就认为该算法的运行时间是θ(g(n))。

       定义1.4    令n为自然数集合,R+为正实数集合。函数f(n)和g(n)∈R+,若存在自然数n0和两个正常数c1c2使得对所有的n≥n0,都有c1g(n)≤f(n)≤c2g(n),就称函数f(n)的阶是θ(g(n))。因此,如果存在limf(n)/g(n),则
lim ⁡ n → ∞ f ( n ) g ( n ) = c , 蕴 含 着 f ( n ) = θ ( g ( n ) ) , 其 中 , c 是 大 于 0 的 常 数 。 \lim_{n\rightarrow\infty}{\frac{f(n)}{g(n)}}=c,蕴含着f(n)=θ(g(n)),其中,c是大于0的常数。 nlimg(n)f(n)=cf(n)=θ(g(n))c0
时间复杂度
       示例:设f(n)=10n2+20n,则有

              f(n)=O(n2)

              f(n)=Ω(n2)

              f(n)=θ(n2)

       一般在考虑算法的运算时间时有两种方法:最坏时间复杂性和平均时间复杂性。最坏时间复杂性是指最坏情况下的时间复杂性;平均时间复杂性是指所有可能的输人数据均以等概率出现的情况下,算法的期望运行时间。

       平均时间复杂性考虑的是同样的n值是各种可能的输入,取它们运算时间的平均值。粗看起来似乎平均时间复杂性更合理,其实不然,因为考虑所有可能的输入,数学上分析起来很困难,有时甚至是不可能的。而且各种可能性出现的概率也不一定相同。一般不作特别说明,讨论的时间复杂性均是最坏情况下的时间复杂性。对于实际应用问题,考虑最坏的情况更为重要。最坏情况下的时间复杂性是算法在任何输人数据上运行时间的上界,从而保证了算法的运行时间不会更长。

       例如,在顺序查找算法中,在最坏情况下的时间复杂性为T(n)=O(n),它表示对于任何输人数据,该算法的运行时间不可能大于O(n)。

       检索问题的顺序查找算法—以元素的比较作为基本操作,考虑成功检索的情况。最好情况下的时间复杂性为θ(1);最坏情况下的时间复杂性为θ(n);在等概率前提下,平均情况下的时间复杂性为θ(n)。

       直接插入排序算法—以元素的比较作为基本操作。最好情况下的时间复杂性为0(n);最坏情况下的时间复杂性为θ(n2);在等概率前提下,平均情况下的时间复杂性为θ(n2)。

1.4.2 空间复杂性

       算法的空间复杂性(Space Complexity)是指算法需要消耗的内存空间。其计算和表示方法与时间复杂性类似,一般都用复杂性的渐近性来表示。同时间复杂性相比,空间复杂性的分析要简单得多。

       根据算法执行过程中对存储空间的使用方式,可以把对算法空间复杂性分析分成两种:静态分析和动态分析。

      (1)静态分析。一个算法静态使用的存储空间,称为静态空间。静态分析的方法比较容易,只要求出算法中使用的所有变量的空间,再折合成多少空间存储单位即可。

      (2)动态分析。一个算法在执行过程中,必须以动态方式分配的存储空间是指在算法执行过程中分配的空间,称为动态空间。动态空间主要是存储中间结果或操作单元所占用空间。

       算法的空间复杂性一般也以数量级的形式给出。

       当一个算法的空间复杂性为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1);当一个算法的空间复杂性与以2为底的n的对数成正比时,可表示为O(log2n);当一个算法的空间复杂性与n成线性比例关系时,可表示为O(n)。

       若形参为数组,则只需要为它分配一个存储由实参传送来的地址指针的空间,即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。

       对于一个算法,其时间复杂性和空间复杂性往往是相互影响的。当追求一个较好的时间复杂性时,可能会使空间复杂性的性能变差,即可能导致占用较多的存储空间;反之,当追求一个较好的空间复杂性时,可能会使时间复杂性的性能变差,即可能导致占用较长的运行时间。算法的所有性能之间都存在着或多或少的相互影响。因此,当设计一个算法时,要综合考虑算法的各项性能,算法的使用频率,算法处理的数据量大小,算法描述语言的特性,算法运行的机器系统环境等各方面因素,才能够设计出比较好的算法。

       算法的时间复杂性和空间复杂性合称为算法的复杂性。

1.5 主流在线测试题库(Online Judge)

       1)杭州电子科技大学(HDU):http://acm.hdu.edu.cn/

       2)浙江大学(ZJU):http://acm.zju.edu.cn/onlinejudge/

       3)牛客网(Nowcoder):https://www.nowcoder.com/

       4)计蒜客:https://www.jisuanke.com/

       5)Vjudge:https://vjudge.net/

       6)CodeForces:http://codeforces.com/

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值