C++经典算法文档格式.docx
《C++经典算法文档格式.docx》由会员分享,可在线阅读,更多相关《C++经典算法文档格式.docx(41页珍藏版)》请在冰豆网上搜索。
![C++经典算法文档格式.docx](https://file1.bdocx.com/fileroot1/2022-11/25/db7bd59b-4ce9-4eaf-81e8-8345b46c2101/db7bd59b-4ce9-4eaf-81e8-8345b46c21011.gif)
m:
=1;
a[1,1]:
h:
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;
=1tomdo
begin
=1tomdowrite(a[i,j]:
4);
writeln;
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+}
x:
a,b,c,d,x1,x2,xx:
extended;
functionf(x:
extended):
f:
=((a*x+b)*x+c)*x+d;
end;
read(a,b,c,d);
forx:
=-100to100do
x1:
=x;
x2:
=x+1;
iff(x1)=0thenwrite(x1:
0:
2,'
'
)
elseiff(x1)*f(x2)<
0then
whilex2-x1>
=0.005do
xx:
=(x1+x2)/2;
iff(x1)*f(xx)<
=0thenx2:
=xx
elsex1:
=xx;
{while}
write(x1:
);
{then}
{for}
回溯法算法
如果上期的“百钱买百鸡”中鸡的种类数是变化的,用枚举法就无能为力了,这里介绍另一种算法——回溯法。
回溯法是一种既带有系统性又带有跳跃性的搜索法,它的基本思想是:
在搜索过程中,当探索到某一步时,发现原先的选择达不到目标,就退回到上一步重新选择。
它主要用来解决一些要经过许多步骤才能完成的,而每个步骤都有若干种可能的分支,为了完成这一过程,需要遵守某些规则,但这些规则又无法用数学公式来描述的一类问题。
下面通过实例来了解回溯法的思想及其在计算机上实现的基本方法。
例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:
a[1]:
=n;
第二步:
若a[1]>
r-1则重复:
若a[i]>
r-i,①若i=r,则输出解,并且a[i]:
=a[i]-1;
②若i<
>
r,则继续生成下一位:
a[i+1]:
=i+1;
若a[i]<
=r-i,则回溯:
i:
=i-1;
a[i]:
第三步:
结束;
程序实现
varn,r,i,j:
array[1..10]ofinteger;
readln(n,r);
ifa[i]>
r-ithen{符合条件}
ifi=rthen{输出}
forj:
=1tordowrite(a[j]:
3);
writeln;
end
else{继续搜索}
begina[i+1]:
end
else{回溯}
begini:
untila[1]=r-1;
下面我们再通过另一个例子看看回溯在信息学奥赛中的应用。
例2数的划分(noip2001tg)
问题描述整数n分成k份,且每份不能为空,任意两份不能相同(不考虑顺序)。
例如:
n=7,k=3,下面三种分法被认为是相同的。
1,1,5;
1,5,1;
5,1,1;
问有多少种不同的分法。
输入:
n,k(6<
n<
=200,2<
=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<
k且sum>
=ai,则添加下一个元素ai+1;
(3)如果i<
k且sum<
ai,则说明达不到目标,回溯到上一步,改变ai-1的值;
算法实现步骤如下:
输入自然数n,k并初始化;
t:
=0;
a[i]:
sum:
=n-1;
nk:
=ndivk;
如果a[1]<
=nk重复:
若i=k,搜索到一个解,计数器t=t+1;
并回溯;
否则:
①若sum>
=a[i]则继续搜索;
②若sum<
a[i]则回溯;
搜索时,inc(i);
=a[i-1];
sum:
=sum-a[i];
回溯时,dec(i);
inc(a[i]);
=sum+a[i+1]-1;
输出。
程序如下:
n,nk,sum,i,k:
t:
longint;
array[1..6]ofinteger;
readln(n,k);
{初始化}
ifi=kthen{判断是否搜索到底}
begininc(t);
dec(i);
inc(a[i]);
end{回溯}
elsebegin
ifsum>
=a[i]then{判断是否回溯}
begininc(i);
end{继续搜}
elsebegindec(i);
{回溯}
untila[1]>
nk;
writeln(t);
回溯法是通过尝试和纠正错误来寻找答案,是一种通用解题法,在NOIP中有许多涉及搜索问题的题目都可以用回溯法来求解
递归算法算法
递归算法的定义:
如果一个对象的描述中包含它本身,我们就称这个对象是递归的,这种用递归来描述的算法称为递归算法。
我们先来看看大家熟知的一个的故事:
从前有座山,山上有座庙,庙里有个老和尚在给小和尚讲故事,他说从前有座山,山上有座庙,庙里有个老和尚在给小和尚讲故事,他说……
上面的故事本身是递归的,用递归算法描述:
procedurebonze-tell-story;
if讲话被打断then故事结束
从前有座山,山上有座庙,庙里有个老和尚在给小和尚讲故事;
bonze-tell-story;
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:
);
{以B柱为中转柱将N个盘子从A柱移动到C柱}
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柱}
从上面的例子我们可以看出,在使用递归算法时,首先弄清楚简单情况下的解法,然后弄清楚如何把复杂情况归纳为更简单的情况。
在信息学奥赛中有的问题的结构或所处理的数据本身是递归定义的,这样的问题非常适合用递归算法来求解,对于这类问题,我们把它分解为具有相同性质的若干个子问题,如果子问题解决了,原问题也就解决了。
例2求先序排列(NOIP2001pj)
[问题描述]给出一棵二叉树的中序与后序排列。
求出它的先序排列。
(约定树结点用不同的大写字母表示,长度≤8)。
[样例]输入:
BADCBDCA输出:
ABCD
我们先看看三种遍历的定义:
先序遍历是先访问根结点,再遍历左子树,最后遍历右子树;
中序遍历是先遍历左子树,再访问根结点,最后遍历右子树;
后序遍历是先遍历左子树,再遍历右子树,最后访问根结点;
从遍历的定义可知,后序排列的最后一个字符即为这棵树的根节点;
在中序排列中,根结点前面的为其左子树,根结点后面的为其右子树;
我们可以由后序排列求得根结点,再由根结点在中序排列的位置确定左子树和右子树,把左子树和右子树各看作一个单独的树。
这样,就把一棵树分解为具有相同性质的二棵子树,一直递归下去,当分解的子树为空时,递归结束,在递归过程中,按先序遍历的规则输出求得的各个根结点,输出的结果即为原问题的解。
源程序
programnoip2001_3;
var z,h:
string;
proceduremake(z,h:
string);
{z为中序排列,h为后序排列}
var s,m:
integer;
begin
=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));
{处理右子树}
readln(z);
readln(h);
make(z,h);
递归算法不仅仅是用于求解递归描述的问题,在其它很多问题中也可以用到递归思想,如回溯法、分治法、动态规划法等算法中都可以使用递归思想来实现,从而使编写的程序更加简洁。
比如上期回溯法所讲的例2《数的划分问题》,若用递归来求解,程序非常短小且效率很高,源程序如下
n,k:
tol:
proceduremake(sum,t,d:
integer);
vari:
ifd=ktheninc(tol)
elsefori:
=ttosumdiv2domake(sum-i,i,d+1);
tol:
make(n,1,1);
writeln(tol);
有些问题本身是递归定义的,但它并不适合用递归算法来求解,如斐波那契(Fibonacci)数列,它的递归定义为:
F(n)=1(n=1,2)
F(n)=F(n-2)+F(n-1)(n>
2)
用递归过程描述为:
Funtionfb(n:
integer):
Begin
ifn<
3thenfb:
=1
elsefb:
=fb(n-1)+fb(n-2);
End;
上面的递归过程,调用一次产生二个新的调用,递归次数呈指数增长,时间复杂度为O(2n),把它改为非递归:
x:
y:
=3tondo
z:
=y;
=x+y;
=z;
修改后的程序,它的时间复杂度为O(n)。
我们在编写程序时是否使用递归算法,关键是看问题是否适合用递归算法来求解。
由于递归算法编写的程序逻辑性强,结构清晰,正确性易于证明,程序调试也十分方便,在NOIP中,数据的规模一般也不大,只要问题适合用递归算法求解,我们还是可以大胆地使用递归算法。
深度优先搜索法算法
在这里介绍两种基本的搜索算法:
深度优先搜索和广度优先搜索法,以树的搜索为例,深度优先搜索法是优先扩展尚未扩展的且具有最大深度的结点;
广度优先搜索法是在扩展完第K层的结点以后才扩展K+1层的结点。
深度优先搜索法与前面讲的回溯法差不多,主要的区别是回溯法在求解过程中不保留完整的树结构,而深度优先搜索则记下完整的搜索树,搜索树起记录解路径和状态判重的作用。
为了减少存储空间,在深度优先搜索中,用标志的方法记录访问过的状态,这种处理方法使得深度优先搜索法与回溯法没什么区别了。
在回溯法中,我们己分析了非递归的实现过程,在这里就只讨论深度优先的递归实现方法。
深度优先搜索的递归实现过程:
proceduredfs(i);
=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:
i,n,m:
array[0..50]ofmap;
proceduredfs(i:
varj:
=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{判断是否在棋盘上}
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
write('
('
1,'
'
)'
=2toidowrite('
->
a[j].x,'
a[j].y,'
halt;
{输出结果并退出程序}
dfs(i+1);
{搜索下一步}
a[i].y:
{出栈}
a[1].x:
a[1].y:
readln(n,m);
dfs
(2);
writeln('
no'
从上面的例子我们可以看出,深度优先搜索算法有两个特点:
1、己产生的结点按深度排序,深度大的结点先得到扩展,即先产生它的子结点。
2、深度大的结点是后产生的,但先得到扩展,即“后产生先扩展”,与栈的工作原理相同,因此用堆栈作为该算法的主要数据结构,存储产生的结点。
对于不同的问题,深度优先搜索算法基本上是一样的,但在具体处理方法和编