C++经典算法.docx

上传人:b****4 文档编号:3819157 上传时间:2022-11-25 格式:DOCX 页数:41 大小:43.42KB
下载 相关 举报
C++经典算法.docx_第1页
第1页 / 共41页
C++经典算法.docx_第2页
第2页 / 共41页
C++经典算法.docx_第3页
第3页 / 共41页
C++经典算法.docx_第4页
第4页 / 共41页
C++经典算法.docx_第5页
第5页 / 共41页
点击查看更多>>
下载资源
资源描述

C++经典算法.docx

《C++经典算法.docx》由会员分享,可在线阅读,更多相关《C++经典算法.docx(41页珍藏版)》请在冰豆网上搜索。

C++经典算法.docx

C++经典算法

分治算法的基本思想

是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问,性质相同。

求出子问题的解,就可得到原问题的解。

分治法解题的一般步骤:

(1)分解,将要解决的问题划分成若干规模较小的同类问题;

(2)求解,当子问题划分得足够小时,用较简单的方法解决;

(3)合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。

例1、比赛安排(noip1996)

设有2^n(n<=6)个球队进行单循环比赛,计划在2^n-1天内完成,每个队每天进行一场比赛。

设计一个比赛的安排,使在2^n-1天内每个队都与不同的对手比赛。

例如n=2时的比赛安排为:

队1234

比赛1-23-4第一天

1-32-4第二天

1-42-3第三天

算法分析:

此题很难直接给出结果,我们先将问题进行分解,设m=2^n,将规模减半,如果n=3(即m=8),8个球队的比赛,减半后变成4个球队的比赛(m=4),4个球队的比赛的安排方式还不是很明显,再减半到两个球队的比赛(m=2),两个球队的比赛安排方式很简单,只要让两个球队直接进行一场比赛即可:

1

2

2

1

分析两个球队的比赛的情况不难发现,这是一个对称的方阵,我们把这个方阵分成4部分(即左上,右上,左下,右下),右上部分可由左上部分加1(即加m/2)得到,而右上与左下部分、左上与右下部分别相等。

因此我们也可以把这个方阵看作是由M=1的方阵所成生的,同理可得M=4的方阵:

1

2

3

4

2

1

4

3

3

4

1

2

4

3

2

1

同理可由M=4方阵生成M=8的方阵:

1

2

3

4

5

6

7

8

2

1

4

3

6

5

8

7

3

4

1

2

7

8

5

6

4

3

2

1

8

7

6

5

5

6

7

8

1

2

3

4

6

5

8

7

2

1

4

3

7

8

5

6

3

4

1

2

8

7

6

5

4

3

2

1

这样就构成了整个比赛的安排表。

在算法设计中,用数组a记录2^n个球队的循环比赛表,整个循环比赛表从最初的1*1方阵按上述规则生成2*2的方阵,再生成4*4的方阵,……直到生成出整个循环比赛表为止。

变量h表示当前方阵的大小,也就是要生成的下一个方阵的一半。

源程序:

var

i,j,h,m,n:

integer;

a:

array[1..32,1..32]ofinteger;

begin

readln(n);

m:

=1;a[1,1]:

=1;h:

=1;

fori:

=1tondom:

=m*2;

repeat

fori:

=1tohdo

forj:

=1tohdobegin

a[i,j+h]:

=a[i,j]+h;{构造右上角方阵}

a[i+h,j]:

=a[i,j+h];{构造左下角方阵}

a[i+h,j+h]:

=a[i,j];{构造右下角方阵}

end;

h:

=h*2;

untilh=m;

fori:

=1tomdo

begin

forj:

=1tomdowrite(a[i,j]:

4);

writeln;

end;

end.

在分治算法中,若将原问题分解成两个较小的子问题,我们称之为二分法,由于二分法划分简单,所以使用非常广泛。

使用二分法与使用枚举法求解问题相比较,时间复杂度由O(N)降到O(log2N),在很多实际问题中,我们可以通过使用二分法,达到提高算法效率的目的,如下面的例子。

例2一元三次方程求解(noip2001tg)

题目大意:

给出一个一元三次方程f(x)=ax3+bx2+cx+d=0,求它的三个根。

所有的根都在区间[-100,100]中,并保证方程有三个实根,且它们之间的差不小于1。

算法分析:

在讲解枚举法时,我们讨论了如何用枚举法求解此题,但如果求解的精度进一步提高,使用枚举法就无能为力了,在此我们再一次讨论如何用二分法求解此题。

由题意知(i,i+1)中若有根,则只有一个根,我们枚举根的值域中的每一个整数x(-100≤x≤100),设定搜索区间[x1,x2],其中x1=x,x2=x+1。

若:

⑴f(x1)=0,则确定x1为f(x)的根;

⑵f(x1)*f(x2)<0,则确定根x在区间[x1,x2]内。

⑶f(x1)*f(x2)>0,则确定根x不在区间[x1,x2]内,设定[x2,x2+1]为下一个搜索区间;

若确定根x在区间[x1,x2]内,采用二分法,将区间[x1,x2]分成左右两个子区间:

左子区间[x1,x]和右子区间[x,x2](其中x=(x1+x2)/2)。

如果f(x1)*f(x)≤0,则确定根在左区间[x1,x]内,将x设为该区间的右界值(x2=x),继续对左区间进行对分;否则确定根在右区间[x,x2]内,将x设为该区间的左界值(x1=x),继续对右区间进行对分;

上述对分过程一直进行到区间的间距满足精度要求为止(即x2-x1<0.005)。

此时确定x1为f(x)的根。

源程序:

{$N+}

var

x:

integer;

a,b,c,d,x1,x2,xx:

extended;

functionf(x:

extended):

extended;

begin

f:

=((a*x+b)*x+c)*x+d;

end;

begin

read(a,b,c,d);

forx:

=-100to100do

begin

x1:

=x;x2:

=x+1;

iff(x1)=0thenwrite(x1:

0:

2,'')

elseiff(x1)*f(x2)<0then

begin

whilex2-x1>=0.005do

begin

xx:

=(x1+x2)/2;

iff(x1)*f(xx)<=0thenx2:

=xx

elsex1:

=xx;

end;{while}

write(x1:

0:

2,'');

end;{then}

end;{for}

end.

回溯法算法

如果上期的“百钱买百鸡”中鸡的种类数是变化的,用枚举法就无能为力了,这里介绍另一种算法——回溯法。

回溯法是一种既带有系统性又带有跳跃性的搜索法,它的基本思想是:

在搜索过程中,当探索到某一步时,发现原先的选择达不到目标,就退回到上一步重新选择。

它主要用来解决一些要经过许多步骤才能完成的,而每个步骤都有若干种可能的分支,为了完成这一过程,需要遵守某些规则,但这些规则又无法用数学公式来描述的一类问题。

下面通过实例来了解回溯法的思想及其在计算机上实现的基本方法。

例1、从N个自然数(1,2,…,n)中选出r个数的所有组合。

算法分析:

设这r个数为a1,a2,…ar,把它们从大到小排列,则满足:

(1)a1>a2>…>ar;

(2)其中第i位数(1<=i<=r)满足ai>r-i;

我们按以上原则先确定第一个数,再逐位生成所有的r个数,如果当前数符合要求,则添加下一个数;否则返回到上一个数,改变上一个数的值再判断是否符合要求,如果符合要求,则继续添加下一个数,否则返回到上一个数,改变上一个数的值……按此规则不断循环搜索,直到找出r个数的组合,这种求解方法就是回溯法。

如果按以上方法生成了第i位数ai,下一步的的处理为:

(1)若ai>r-i且i=r,则输出这r个数并改变ai的值:

ai=ai-1;

(2)若ai>r-i且i≠r,则继续生成下一位ai+1=ai-1;

(3)若ai<=r-i,则回溯到上一位,改变上一位数的值:

ai-1=ai-1-1;

算法实现步骤:

第一步:

输入n,r的值,并初始化;i:

=1;a[1]:

=n;

第二步:

若a[1]>r-1则重复:

若a[i]>r-i,①若i=r,则输出解,并且a[i]:

=a[i]-1;

②若i<>r,则继续生成下一位:

a[i+1]:

=a[i]-1;i:

=i+1;

若a[i]<=r-i,则回溯:

i:

=i-1;a[i]:

=a[i]-1;

第三步:

结束;

程序实现

varn,r,i,j:

integer;

a:

array[1..10]ofinteger;

begin

readln(n,r);i:

=1;a[1]:

=n;

repeat

ifa[i]>r-ithen{符合条件}

ifi=rthen{输出}

begin

forj:

=1tordowrite(a[j]:

3);

writeln;

a[i]:

=a[i]-1;

end

else{继续搜索}

begina[i+1]:

=a[i]-1;i:

=i+1;end

else{回溯}

begini:

=i-1;a[i]:

=a[i]-1;end;

untila[1]=r-1;

end.

下面我们再通过另一个例子看看回溯在信息学奥赛中的应用。

例2数的划分(noip2001tg)

问题描述整数n分成k份,且每份不能为空,任意两份不能相同(不考虑顺序)。

例如:

n=7,k=3,下面三种分法被认为是相同的。

1,1,5;1,5,1;5,1,1;

问有多少种不同的分法。

输入:

n,k(6

输出:

一个整数,即不同的分法。

样例

输入:

73

输出:

4{四种分法为:

1,1,5;1,2,4;1,3,3;2,2,3;}

算法分析:

此题可以用回溯法求解,设自然数n拆分为a1,a2,…,ak,必须满足以下两个条件:

(1)n=a1+a2+…+ak;

(2)a1<=a2<=…<=ak(避免重复计算);

现假设己求得的拆分数为a1,a2,…ai,且都满足以上两个条件,设sum=n-a1-a2-…-ai,我们根据不同的情形进行处理:

(1)如果i=k,则得到一个解,则计数器t加1,并回溯到上一步,改变ai-1的值;

(2)如果i=ai,则添加下一个元素ai+1;

(3)如果i

算法实现步骤如下:

第一步:

输入自然数n,k并初始化;t:

=0;i:

=1;a[i]:

=1;sum:

=n-1;nk:

=ndivk;

第二步:

如果a[1]<=nk重复:

若i=k,搜索到一个解,计数器t=t+1;并回溯;

否则:

①若sum>=a[i]则继续搜索;

②若sum

搜索时,inc(i);a[i]:

=a[i-1];sum:

=sum-a[i];

回溯时,dec(i);inc(a[i]);sum:

=sum+a[i+1]-1;

第三步:

输出。

程序如下:

var

n,nk,sum,i,k:

integer;

t:

longint;

a:

array[1..6]ofinteger;

begin

readln(n,k);

nk:

=ndivk;

t:

=0;i:

=1;a[i]:

=1;sum:

=n-1;{初始化}

repeat

ifi=kthen{判断是否搜索到底}

begininc(t);dec(i);inc(a[i]);sum:

=sum+a[i+1]-1;end{回溯}

elsebegin

ifsum>=a[i]then{判断是否回溯}

begininc(i);a[i]:

=a[i-1];sum:

=sum-a[i];end{继续搜}

elsebegindec(i);inc(a[i]);sum:

=sum+a[i+1]-1;end;{回溯}

end;

untila[1]>nk;

writeln(t);

end.

回溯法是通过尝试和纠正错误来寻找答案,是一种通用解题法,在NOIP中有许多涉及搜索问题的题目都可以用回溯法来求解

 

递归算法算法

递归算法的定义:

如果一个对象的描述中包含它本身,我们就称这个对象是递归的,这种用递归来描述的算法称为递归算法。

我们先来看看大家熟知的一个的故事:

从前有座山,山上有座庙,庙里有个老和尚在给小和尚讲故事,他说从前有座山,山上有座庙,庙里有个老和尚在给小和尚讲故事,他说……

上面的故事本身是递归的,用递归算法描述:

procedurebonze-tell-story;

begin

if讲话被打断then故事结束

elsebegin

从前有座山,山上有座庙,庙里有个老和尚在给小和尚讲故事;

bonze-tell-story;

end

end;

从上面的递归事例不难看出,递归算法存在的两个必要条件:

(1)必须有递归的终止条件;

(2)过程的描述中包含它本身;

在设计递归算法中,如何将一个问题转化为递归的问题,是初学者面临的难题,下面我们通过分析汉诺塔问题,看看如何用递归算法来求解问题;

例1:

汉诺塔问题,如下图,有A、B、C三根柱子。

A柱子上按从小到大的顺序堆放了N个盘子,现在要把全部盘子从A柱移动到C柱,移动过程中可以借助B柱。

移动时有如下要求:

(1)一次只能移动一个盘子;

(2)不允许把大盘放在小盘上边;

(3)盘子只能放在三根柱子上;

算法分析:

当盘子比较多的时,问题比较复杂,所以我们先分析简单的情况:

如果只有一个盘子,只需一步,直接把它从A柱移动到C柱;

如果是二个盘子,共需要移动3步:

(1)把A柱上的小盘子移动到B柱;

(2)把A柱上的大盘子移动到C柱;

(3)把B柱上的大盘子移动到C柱;

如果N比较大时,需要很多步才能完成,我们先考虑是否能把复杂的移动过程转化为简单的移动过程,如果要把A柱上最大的盘子移动到C柱上去,必须先把上面的N-1个盘子从A柱移动到B柱上暂存,按这种思路,就可以把N个盘子的移动过程分作3大步:

(1)把A柱上面的N-1个盘子移动到B柱;

(2)把A柱上剩下的一个盘子移动到C柱;

(3)把B柱上面的N-1个盘子移动到C柱;

其中N-1个盘子的移动过程又可按同样的方法分为三大步,这样就把移动过程转化为一个递归的过程,直到最后只剩下一个盘子,按照移动一个盘子的方法移动,递归结束。

递归过程:

procedureHanoi(N,A,B,C:

integer;);{以B柱为中转柱将N个盘子从A柱移动到C柱}

begin

ifN=1thenwrite(A,’->’,C){把盘子直接从A移动到C}

elsebegin

Hanoi(N-1,A,C,B);{以C柱为中转柱将N-1个盘子从A柱移动到B柱}

write(A,’->’,C);{把剩下的一个盘子从A移动到C}

Hanoi(N-1,B,A,C);{以A柱为中转柱将N-1个盘子从B柱移动到C柱}

end;

end;

从上面的例子我们可以看出,在使用递归算法时,首先弄清楚简单情况下的解法,然后弄清楚如何把复杂情况归纳为更简单的情况。

在信息学奥赛中有的问题的结构或所处理的数据本身是递归定义的,这样的问题非常适合用递归算法来求解,对于这类问题,我们把它分解为具有相同性质的若干个子问题,如果子问题解决了,原问题也就解决了。

例2求先序排列(NOIP2001pj)

[问题描述]给出一棵二叉树的中序与后序排列。

求出它的先序排列。

(约定树结点用不同的大写字母表示,长度≤8)。

[样例]输入:

BADCBDCA输出:

ABCD

算法分析:

我们先看看三种遍历的定义:

先序遍历是先访问根结点,再遍历左子树,最后遍历右子树;

中序遍历是先遍历左子树,再访问根结点,最后遍历右子树;

后序遍历是先遍历左子树,再遍历右子树,最后访问根结点;

从遍历的定义可知,后序排列的最后一个字符即为这棵树的根节点;在中序排列中,根结点前面的为其左子树,根结点后面的为其右子树;我们可以由后序排列求得根结点,再由根结点在中序排列的位置确定左子树和右子树,把左子树和右子树各看作一个单独的树。

这样,就把一棵树分解为具有相同性质的二棵子树,一直递归下去,当分解的子树为空时,递归结束,在递归过程中,按先序遍历的规则输出求得的各个根结点,输出的结果即为原问题的解。

源程序

programnoip2001_3;

var z,h:

string;

proceduremake(z,h:

string);{z为中序排列,h为后序排列}

var s,m:

integer;

begin

m:

=length(h);{m为树的长度}

 write(h[m]);{输出根节点}

 s:

=pos(h[m],z);{求根节点在中序排列中的位置}

 ifs>1thenmake(copy(z,1,s-1),copy(h,1,s-1));{处理左子树}

 ifm>sthenmake(copy(z,s+1,m-s),copy(h,s,m-s));{处理右子树}

end;

begin

 readln(z);

 readln(h);

 make(z,h);

end.

递归算法不仅仅是用于求解递归描述的问题,在其它很多问题中也可以用到递归思想,如回溯法、分治法、动态规划法等算法中都可以使用递归思想来实现,从而使编写的程序更加简洁。

比如上期回溯法所讲的例2《数的划分问题》,若用递归来求解,程序非常短小且效率很高,源程序如下

var

n,k:

integer;

tol:

longint;

proceduremake(sum,t,d:

integer);

vari:

integer;

begin

ifd=ktheninc(tol)

elsefori:

=ttosumdiv2domake(sum-i,i,d+1);

end;

begin

readln(n,k);

tol:

=0;

make(n,1,1);

writeln(tol);

end.

有些问题本身是递归定义的,但它并不适合用递归算法来求解,如斐波那契(Fibonacci)数列,它的递归定义为:

F(n)=1(n=1,2)

F(n)=F(n-2)+F(n-1)(n>2)

用递归过程描述为:

Funtionfb(n:

integer):

integer;

Begin

ifn<3thenfb:

=1

elsefb:

=fb(n-1)+fb(n-2);

End;

上面的递归过程,调用一次产生二个新的调用,递归次数呈指数增长,时间复杂度为O(2n),把它改为非递归:

x:

=1;y:

=1;

fori:

=3tondo

begin

z:

=y;y:

=x+y;x:

=z;

end;

修改后的程序,它的时间复杂度为O(n)。

我们在编写程序时是否使用递归算法,关键是看问题是否适合用递归算法来求解。

由于递归算法编写的程序逻辑性强,结构清晰,正确性易于证明,程序调试也十分方便,在NOIP中,数据的规模一般也不大,只要问题适合用递归算法求解,我们还是可以大胆地使用递归算法。

深度优先搜索法算法

在这里介绍两种基本的搜索算法:

深度优先搜索和广度优先搜索法,以树的搜索为例,深度优先搜索法是优先扩展尚未扩展的且具有最大深度的结点;广度优先搜索法是在扩展完第K层的结点以后才扩展K+1层的结点。

深度优先搜索法与前面讲的回溯法差不多,主要的区别是回溯法在求解过程中不保留完整的树结构,而深度优先搜索则记下完整的搜索树,搜索树起记录解路径和状态判重的作用。

为了减少存储空间,在深度优先搜索中,用标志的方法记录访问过的状态,这种处理方法使得深度优先搜索法与回溯法没什么区别了。

在回溯法中,我们己分析了非递归的实现过程,在这里就只讨论深度优先的递归实现方法。

深度优先搜索的递归实现过程:

proceduredfs(i);

fori:

=1tordo

if子结点mr符合条件then产生的子结点mr入栈;

if子结点mr是目标结点then输出

elsedfs(i+1);

栈顶元素出栈(即删去mr);

endif;

endfor;

在讲解递推法时,我们讨论了用递推法解骑土游历问题,在这里我们再看看如何用深度优先搜索法求解此题。

例1骑士游历:

设有一个n*m的棋盘,在棋盘上任一点有一个中国象棋马,马走的规则为:

1.马走日字2.马只能向右走。

当N,M输入之后,找出一条从左下角到右上角的路径。

例如:

输入N=4,M=4,输出:

路径的格式:

(1,1)->(2,3)->(4,4),若不存在路径,则输出"no"

算法分析:

我们以4×4的棋盘为例进行分析,用树形结构表示马走的所有过程(如下图),求从起点到终点的路径,实际上就是从根结点开始深度优先搜索这棵树。

马从(1,1)开始,

按深度优先搜索法,走一步到达(2,3),判断是否到达终点,若没有,则继续往前走,再走一步到达(4,4),然后判断是否到达终点,若到达则退出,搜索过程结束。

为了减少搜索次数,在马走的过程中,判断下一步所走的位置是否在棋盘上,如果不在棋盘上,则另选一条路径再走。

程序如下:

const

dx:

array[1..4]ofinteger=(2,2,1,1);

dy:

array[1..4]ofinteger=(1,-1,2,-2);

type

map=record

x,y:

integer;

end;

var

i,n,m:

integer;

a:

array[0..50]ofmap;

proceduredfs(i:

integer);

varj:

integer;

begin

forj:

=1to4do

if(a[i-1].x+dx[j]>0)and(a[i-1].x+dx[j]<=n)and(a[i-1].y+dy[j]>0)and(a[i-1].y+dy[j]<=n)then{判断是否在棋盘上}

begin

a[i].x:

=a[i-1].x+dx[j];

a[i].y:

=a[i-1].y+dy[j];{入栈}

if(a[i].x=n)and(a[i].y=m)then

begin

write('(',1,',',1,')');

forj:

=2toidowrite('->(',a[j].x,',',a[j].y,')');

halt;{输出结果并退出程序}

end;

dfs(i+1);{搜索下一步}

a[i].x:

=0;a[i].y:

=0;{出栈}

end;

end;

begin

a[1].x:

=1;a[1].y:

=1;

readln(n,m);

dfs

(2);

writeln('no');

end.

从上面的例子我们可以看出,深度优先搜索算法有两个特点:

1、己产生的结点按深度排序,深度大的结点先得到扩展,即先产生它的子结点。

2、深度大的结点是后产生的,但先得到扩展,即“后产生先扩展”,与栈的工作原理相同,因此用堆栈作为该算法的主要数据结构,存储产生的结点。

对于不同的问题,深度优先搜索算法基本上是一样的,但在具体处理方法和编

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 党团工作 > 入党转正申请

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1