奥赛竞赛辅导.docx

上传人:b****1 文档编号:1996565 上传时间:2023-05-02 格式:DOCX 页数:33 大小:1.33MB
下载 相关 举报
奥赛竞赛辅导.docx_第1页
第1页 / 共33页
奥赛竞赛辅导.docx_第2页
第2页 / 共33页
奥赛竞赛辅导.docx_第3页
第3页 / 共33页
奥赛竞赛辅导.docx_第4页
第4页 / 共33页
奥赛竞赛辅导.docx_第5页
第5页 / 共33页
奥赛竞赛辅导.docx_第6页
第6页 / 共33页
奥赛竞赛辅导.docx_第7页
第7页 / 共33页
奥赛竞赛辅导.docx_第8页
第8页 / 共33页
奥赛竞赛辅导.docx_第9页
第9页 / 共33页
奥赛竞赛辅导.docx_第10页
第10页 / 共33页
奥赛竞赛辅导.docx_第11页
第11页 / 共33页
奥赛竞赛辅导.docx_第12页
第12页 / 共33页
奥赛竞赛辅导.docx_第13页
第13页 / 共33页
奥赛竞赛辅导.docx_第14页
第14页 / 共33页
奥赛竞赛辅导.docx_第15页
第15页 / 共33页
奥赛竞赛辅导.docx_第16页
第16页 / 共33页
奥赛竞赛辅导.docx_第17页
第17页 / 共33页
奥赛竞赛辅导.docx_第18页
第18页 / 共33页
奥赛竞赛辅导.docx_第19页
第19页 / 共33页
奥赛竞赛辅导.docx_第20页
第20页 / 共33页
亲,该文档总共33页,到这儿已超出免费预览范围,如果喜欢就下载吧!
下载资源
资源描述

奥赛竞赛辅导.docx

《奥赛竞赛辅导.docx》由会员分享,可在线阅读,更多相关《奥赛竞赛辅导.docx(33页珍藏版)》请在冰点文库上搜索。

奥赛竞赛辅导.docx

奥赛竞赛辅导

A2-回溯、递归、递推学习小结

江苏省连云港市赣榆高级中学仲晨

myheimu@

"Myheimu'sHome"

关键词:

【JSOI2005-2006第一轮函授函授作业A组第2次回溯、递归、递推学习小结】

文件名:

A2-回溯、递归、递推.doc最后修改时间:

2005年4月30日

附带文件:

N皇后问题演示程序.exe(146KB)

文件大小:

928.00KB共28页,合计15297字

 

目录

按Ctrl+鼠标单击前往

A2-回溯、递归、递推学习小结1

一、导言(Introducation)2

二、关于回溯(AboutBacktracking)4

三.递推与递归(Recursion)7

四、回溯·递推·递归运用与优化15

五、高级本相关思考26

一、导言(Introducation)

作为导言,先说一下本文的写作思路与线索:

本文虽为函授作业,但是我没有当作纯粹的作业来作。

我尽量收集各类信息,上网收集资料,认真阅读相关书籍,然后结合高级本写成本文,在本文中涉及了许多内容,我将回溯、递归、递推发散为很广阔的范围,为的是在这次函授之中能够尽量多的学到更多的东西。

Bytheway,我也把本文作为一个自己的资料库,录入许多理解性的内容,作为总结与查询库。

当然,决不能忘记要自己思考,学习小节,因此进行了很多题目算法的思考,我进行了思考研究,写出详解与勘误参考,水平有限,欢迎批评指教。

本文线索就是回溯、递归、递推,我们围绕这三项进行讨论。

 

函授文件里要求思考一下七个问题,它们在本文中都将以专题形式叙述:

⊙回溯思想适合解决哪些问题?

⊙回溯思想在具体实现时,采用递归与非递归的方法,各有什么优缺点?

⊙回溯与递归的关系怎样?

⊙递推与递归的关系怎样?

递推的优点在哪儿?

⊙递推与动态规划的关系怎样?

⊙思考“栈”在递归、回溯中的作用?

⊙结合实例,分析它们的效率。

回溯、递归、递推:

基础而重要。

我们应该努力理解其中的精髓,并把它的思想发挥到极致!

下图是路径搜索的图像,美妙极了,就让它带领我们走向回溯、递归、递推的世界吧!

二、关于回溯(AboutBacktracking)

回溯实质是DFS,也可以说DFS就是回溯(那些“高级”DFS实质不再是回溯了!

),属于静态规划。

专题问题:

回溯思想适合解决哪些问题?

回溯算法很大程度上类似穷举,或者说回溯就是穷举的实现形式之一。

回溯也就是深度搜索,搜索到每个结点判断正确与最佳。

因此,回溯算法适用于大规模状态的逐一处理与寻找最佳状态。

看多了动态规划,其实回溯使用范围更广,因此重要的多!

回溯,首先需要“回”,才能“溯”,这就关系到状态(State)的枚举与递进。

对于一些简单的问题枚举也简单,不在说了。

但是有些问题就不那么简单了,比如一些变化不一,状态联系较大的模型。

对于这些问题,如果使用递归回溯较难,则可以使用数学方法来枚举,就是一些可以递归回溯的题,为了判断、处理方便,也要使用数学枚举。

例如:

数字全排列、数字升序排列……对于数字升序排列(n个数字1-n,排列为m位的数,并且后面的数字比前面的数字大),用回溯需要加判断,比较麻烦,而且如果需要使用这个排列进行其他的状态处理应用(如对应某个棋盘排列)就更麻烦了,所以我们可以使用从后向前找第一个小于后数的数字,将其加“1”,再将其后数字升序排列的办法实现枚举。

处理这个问题的办法是寻找变化规律。

以上就是状态的处理问题。

回溯本是很简单的,有较固定的格式,但是,“标准”的回溯就是穷举!

为了较好的时间效率和空间效率,我们需要进行优化,具体的表现就是——剪枝:

状态判断分为:

提前判断和当前状态判断。

当前状态判断也就是判断当前回溯到了结点(note),判断是否满足题意,对于不同的题目,这个判断形式不同,如:

不用判断(因为能回溯到结点就满足题意了),条件判断(判断其是否满足所有条件),最值判断(用于求最值的试题,比较是否大于max或小于min)……这些一般较简单,在此不表。

提前判断即剪枝,剪枝具有十分重要的作用,我们可以把回溯看作一个搜索树,当前状态判断剪掉的只是叶子(结点),但是剪枝顾名思义剪掉的是树枝(branch)。

这个对速度影响就大了!

在很多情况下,我们已经找到了一组比较好的解。

但是计算机仍然会义无返顾地去搜索比它更“劣”的其他解,搜索到后也只能回溯。

为了避免出现这种情况,我们需要灵活地去定制回溯搜索的边界。

剪枝的方法很多,主要有一下几种:

1.调整法

指通过比较子树来剪枝。

这主要用于一些需要尝试许多一维可能状态时,而且这些一维状态有着明显的单调性或联系性(例如:

如果某状态满足则小于它的状态都满足题意,则可以不再扩展一个不满足状态的更大状态;又例:

一个状态满足则其约数状态满足但不是最优,就可以不再扩展其约数状态。

),这样就有可能实现剪枝,并且会有很大的作用(剪枝力度)。

特别对于单调状态,可以使用二分法将时间复杂度降到

之类的复杂度。

2.范围判断(极端法,分枝定界法)

通过判断之后的范围等条件是否满足来剪枝,例如埃及分数等题目。

这个判断也十分灵活,有的可以通过比较是否有可能超过最值来剪枝,有的可以通过根据数学计算看是否有可能满足,还有的就是使用迭代加宽、迭代加深的DFS算法来作范围限制……

3.数学方法

这就是使用数学判断方法来剪枝,同时也会运用调整法或极端法结合起来使用,数学方法好多,需要就题论题,不表。

例题1计算机网络连接

要将n(n<=30)台计算机连成网络,连接方法:

去除首尾两台计算机与一台计算机相连以外,其他计算机只与两台计算机相连。

连接的长度则为计算机连接的电缆的长度。

求一种连接方式,使需要电缆的长度最短。

分析这个题目其他很好的解法,但是我们在这里练习用回溯搜索来解决。

由于回溯搜索的搜索量比较大,达到了n!

,是不可能搜索完n=30的情况的,所以,我们考虑对它进行优化:

假如目前搜索到了一组解,电缆总长度为kx,那么,如果说以后搜索到的连接方法(不一定是最终连接方法)的连接长度>=kx,那么这个方案的总长度一定不小于kx,那么,就不必要搜索下去了,直接换下一个结点继续搜索。

路径A1-A2-…-An与路径An-An-1-…-A1这两条路径是一个“正反”的关系,本质上是相同的,于是我们可以规定起点始的下标总是小于终点的下标

假如路径的A-B-C-D的长度<A-C-B-D的长度,那么包含A-C-B-D路径的路径的长度一定不是最短。

有了上述的优化,题目就可以得到很快的解决了。

本题用的就是范围判断。

剪枝并不是万能的,有些题目几乎没有剪枝方法,有些题目即使是将冗枝都剪除也会因为满足状态过多而超时。

从而也推出回溯不是万能的(这是就时间空间而言,准确的说是:

穷举理论上万能,但是时间空间表现不好)。

虽说如此,但是我们在没有十分好的其他方法或时间空间允许的条件下,首选回溯。

剪枝剪掉的是时间,对于回溯的空间处理,可以使用更好的数据结构,例如Hash表(HashTable)之类。

回溯的使用形式:

一般使用递归、模拟递归(具体到递归,请参阅本文第三节。

这里我写了递归框架,如下:

Program文件名;

Constmax=###;

Type

Datetype={数据类型}

Var

Stack:

array[1..max]ofdatetype;

Sum:

longint;{sum是总可能数或最值}

Proceduretry(depth:

integer);

begin

ifstack[depth-1]是目标状态then

begin

inc(sum);

处理结果;{输出或判断是否是最值}

exit;

end;

fori:

=1to可能下一状态数do

begin

判断,剪枝;

stack[depth]:

=可能状态[i];

try(depth+1);

end;

end;

Begin

Sum:

=0;

Init;{初始化}

Try

(1);

输出sum;

End.

回溯简单而复杂,所以也没有什么固定的格式,之所以写下这个蹩足的框架,是为了理解回溯的层次递归。

至于模拟递归,新高级本上讲的很清楚了,关系到栈的使用,在第六个专题中将涉及。

回到本节开头,回溯就是DFS,DFS就是回溯。

DFS的算法好多好多,并且优化形式不一,在本文第四节中将对于常用的方法进行学习。

也是由于回溯的思想还算简单,本节就到此为止了,下面需要结合递归来探讨。

三.递推与递归(Recursion)

K上升段

问题描述:

对于自然数1..n的一个排列A[1..N]可以划分为若干个单调递增序列。

每个单调递增序列由连续元素A[st..ed]组成,且满足以下条件:

1<=st,ed<=n;A[i]A[ed+1];

例如:

排列12456391078可划分为3个单调递增序列12345;3910;78;所以我们称这是一个3上升段序列。

现在给定n和k,求出n的全排列中的,k上升段序列的个数。

输入格式:

输入仅有1行,包含两个数n,k(1

输出格式:

输出n的所有k上升段的个数。

样例kink.in32

kink.out4

(说明,符合条件的排列是132,312,213,231)

解析:

本题看似要用回溯或组合数学来计算,但是它却确确实实只能用递推来做!

先给出递推式:

f(n,k)=(n-k)×f(n-1,k-1)+k×f(n-1,k)

形成f(n,k)的由两部分组成:

第一部分是f(n-1,k-1)变化而来,例如13254是一种f(5,3),在每一个不是上升段的结尾处放上“6”都可以变成k个上升段,不是上升段的结尾处共有((n-1)+1)-k=n-k个;

第二部分是f(n-1,k)是变化而来,例如13542是一种f(5,4),在每一个是上升段的结尾处放上“6”都可以保持k个上升段,是上升段的结尾处共有k个。

因此就有了上面的式子,只要递推即可,注意使用高精度。

递推要求数据关系较密切,有明确的公式和单调性,也就因此用途比较狭窄,但是绝不意味这在竞赛中没有用处,下面的例子就是一个很好的例子:

专题问题:

递推与动态规划的关系怎样?

递推没有重复计算什么数据,保持了高效。

让我们想到了动态规划(DP),动态规划与递推很相似,我认为:

动态规划=递推+贪心

大多数的动态规划的状态转移式是A[i,j]=max/min{A[k,l]|k满足条件E1,l满足条件E2}+date

明显的:

max/min就是贪心,而所谓的状态转移就是递推。

所以我个人认为DP就是递推时取了最优值罢了。

当然,DP要复杂的多,可以引申出好多内容和问题,没那么简单。

可转过来说,递推、贪心就没有很难的吗?

个人之见。

专题问题:

递推与递归的关系怎样?

递推的优点在哪儿?

递归与递推表面看来是相逆的过程,其实也是相似的,最终的计算都是从小算到大。

就是递推的使用环境要求高导致了递推的高效性,递推没有重复计算什么数据,保持了高效。

递归大多数会重复计算子问题,导致时间浪费,所以一般不要使用过深的递归,甚至会空间溢出。

但是也不能说递推好,递归差,因为递归却能解决很多递推做不到的事情,在某些特定的环境下也能实现高效,并且递归容易使用。

我们要就事论事!

对于递归和递推的效率问题,一般认为递推比递归优,举一个简单的例子:

最简单的斐波那契数列(Fibonacci),对于f(30),如果使用递归则需要运行1664079次,而递推只需30次就可以了,速度悬殊。

递归:

functionf(n:

longint):

longint;

begin

ifi<3thenexit

(1);

f:

=f(i-1)+f(i-2);

end;

递推:

f[1]:

=1;f[2]:

=1;

fori:

=1ton-2do

begin

f[i+2]:

=f[i]+f[i+1];

end;

一般来说,递归需要有边界条件、递归前进段和递归返回段。

当边界条件不满足时,递归前进;当边界条件满足时,递归返回。

在考虑使用递归算法编写程序时,应满足两点:

1)该问题能够被递归形式描述;

2)是否存在递归结束的边界条件。

只有当这两者都满足时才可以使用递归算法。

递归的能力在于用有限的语句来定义对象的无限集合。

用递归思想写出的程序往往十分简洁易懂。

递归的应用范围很广:

1.经典递归

例如Hanoi塔问题:

经典的递归,原问题包含子问题。

有些问题或者数据结构本来就是递归描述的,用递归做很自然。

2.递归与递推,数学式关系

利用递归的思想建立递推关系,如由兔子生崽而来的fibonacci数列。

但递推由于没有返回段,因此更为简单,有时可以直接用循环实现。

3.分治等以大化小算法

不少分治方法是源于递归思想,或是递归分解+合并处理。

4.回溯

规模较小的问题用回溯解决比较自然。

注意递归前后要保证现场的保存和恢复,即正确的转化问题。

5.动态规划

动态规划的子问题重叠性质与递归有某种相似之处。

递归+动态修改查表是一种不错的建立动态规划模型的方法。

6.树、图、排序等符合递归子问题思想的结构

树、图等数据结构本身就是递归结构,因此当然是使用递归来处理。

7.其他

其他者,不好归类也。

例如表达式处理,排列组合等,很杂的。

例题2给出一棵二叉树的中序与后序排列。

求出它的先序排列。

分析通过对比二叉树的中序与后序排列,我们可以找出根节点及左右子树。

同样的,有可以通过对比左子树的中序与后序排列,找出左子树的根节点……可见,该问题能够被递归描述。

当找到最后一个根节点时,递归无法再进行下去,这就是递归结束的边界条件。

由此可见,递归算法中常常隐含了分治思想。

程序略。

例题3求把一个整数n无序划分成k份互不相同的正整数之和的方法总数。

分析这是一道动态规划题,动态方程如下:

  f[i-1,j]+f[i,j-i]+1((jmodi=0)and(jdivi=1))

  f[i,j]:

=f[i-1,j](i>=j)

  f[i-1,j]+f[i,j-i](else)

  s:

=f(k,n-k)

本题可以用循环来实现递推,也可以考虑用递归求解。

主过程见下一页:

  可以看出,方案一的程序较为冗长,消耗栈空间较大;而方案二较为简洁明了,所用的栈空间也较小,效率较高。

因此,使用递归算法也有一个优化问题。

算法的简洁与否直接制约了程序的可行性和效率。

 

方案一:

Procedurework(I,j:

longint;vars:

longint);

Vart:

longint;

Begin

If(i=1)or(j=1)thens:

=1

 Elseif(i=0)or(j=0)thens:

=0

  Elsebegin

   if(jmodi=0)and(jdivi=1)then

  begin

    work(i-1,j,s);

    t:

=s;

    work(i,j-1,s);

    s:

=s+t+1;

    end

   elseif(i>=j)then

     work(i-1,j)

     elsebegin

      work(i-1,j,s);

      t:

=s;

      work(I,j-1,s);

      s:

=s+t;

end;

End;

方案二:

proceduresearch(v,w,last:

byte);

vari:

byte;

begin

 ifw=0theninc(count)

 else

  ifw=1then

   ifv>=lastthensearch(0,0,0)else

  elsefori:

=lasttov-1dosearch(v-i,w-1,i);

end;

 

递归使一些复杂的问题处理起来简单明了,尤其在学习算法设计、数据结构时更能体会到这一点。

但是,递归在每一次执行时都要为局部变量、返回地址分配栈空间,这就降低了运行效率,也限制了递归的深度。

因此,在必要的时候可以只使用递归的思想来求解,而程序则转用非递归的方式书写,关于递归与非递归的转化,参见新高级本吧。

 

专题问题:

(对不起,这里使用了一点别人的冬冬,第1节概述是我找的资料,其他是我自己的)

1.概述

栈(Stack)是一种特殊的线性表。

我们常举的例子是:

可以把食堂里冼净的一摞碗看作一个栈。

在通常情况下,最先冼净的碗总是放在最底下,后冼净的碗总是摞在最顶上。

而在使用时,却是从顶上拿取,也就是说,后冼的先取用,后摞上的先取用。

好果我们把冼净的碗“摞上”称为进栈,把“取用碗”称为出栈,那么,上例的特点是:

后进栈的先出栈。

然而,摞起来的碗实际上是一个表,只不过“进栈”和“出栈”,或者说,元素的插入和删除是在表的一端进行而已。

一般而言,栈是一个线性表,其所有的插入和删除均是限定在表的一端进行,允许插入和删除的一端称栈顶(Top),不允许插入和删除的一端称栈底(Bottom)。

若给定一个栈S=(a1,a2,a3,…,an),则称a1为栈底元素,an为栈顶元素,元素ai位于元素ai-1之上。

栈中元素按a1,a2,a3,…,an的次序进栈,如果从这个栈中取出所有的元素,则出栈次序为an,an-1,…,a1。

也就是说,栈中元素的进出是按后进先出的原则进行,这是栈结构的重要特征。

因此栈又称为后进先出(LIFO—LastInFirstOut)表。

我们常用一个图来形象地表示栈,其形式如右图:

通常,对栈进行的运算主要有以下几种:

(1)往栈顶加入一个新元素,称进栈;

(2)删除栈顶元素,称退栈;

(3)查看当前的栈顶元素,称出栈。

此外,在使用栈之前,首先需要建立一个空栈,称建栈;在使用栈的过程中,还要不断测试栈是否为空或已满,称为测试栈。

2.栈的存储结构

栈是一种线性表,在计算机中用向量作为栈的存储结构最为简单。

因此,当用编程语言写程序时,用一维数组来建栈十分方便。

例如,设一维数组STACK[1..n]表示一个栈,其中n为栈的容量,即可存放元素的最大个数。

栈的第一个元素,或称栈底元素,是存放在STACK[1]处,第二个元素存放在STACK[2]处,第i个元素存放在STACK[i]处。

另外,由于栈顶元素经常变动,需要设置一个指针变量top,用来指示栈顶当前位置,栈中没有元素即栈空时,令top=0,当top=n时,表示栈满。

3.“栈”在递归、回溯中的作用?

栈是事实上的递归、回溯使用方法,我们使用递归时就是利用了栈的原理:

每一层保存当前的状态,然后继续递归到下一层。

当我们理解了栈的使用与递归后,我们可以使用模拟的栈(以我们自己的原则,创建一个栈,并由我们自己来维护)来实现所有的递归、回溯问题,尽管使用递归很简洁方便,但是对于大型程序、层数众多的程序,我们需要模拟栈来实现。

模拟栈实现回溯的就叫做模拟回溯。

栈的表象,《数据结构》的演示程序十分真切的,我就不想冗述^-^我截了两幅图,在这里代替了。

栈溢出是递归、回溯使用时的大问题,特别是对于数据大的题目,我们应当特别注意。

四、回溯·递推·递归运用与优化

在本节主要是回溯、递推、递归的深入讨论和优化。

下面对一个经典问题做一下讨论:

八皇后之递归算法、回溯算法

我在网上偶尔找到了一个N皇后的演示程序,做的非常好,大家共享,共同进步。

【问题描述】在一个8×8格子的棋盘上,要布局八个皇后,条件是不能出现两个皇后在同一行或同一列或同一斜线的情况,以防“相互攻击”,请问共有多少种合理的布局方式,每一种布局的具体情况如何?

右上图是4皇后问题规则示意

右下图是8皇后问题的第一个解

【分析】显然问题的键在于如何判定某个皇后所在的行、列、斜线上是否有别的皇后;可以从矩阵的特点上找到规律,如果在同一行,则行号相同;如果在同一列上,则列号相同;如果同在/斜线上的行列值之和相同;如果同在\斜线上的行列值之差相同;如果斜线不分方向,则同一斜线上两皇后的行号之差的绝对值与列号之差的绝对值相同。

从下图可以验证上面的说法:

对于一组布局我们可以用一个一维数组来表示:

X:

ARRAY[1..8]OFINTEGER;X[I]的下标I表示第I个皇后在棋盘的第I行,X[I]的内容表示在第I行的第X[I]列,例如:

X[3]=5就表示第3个皇后在第3行的第5列。

在这种方式下,要表示两个皇后A和B不在同一列或斜线上的条件可以描述为:

X[A]<>X[B]ANDABS(A-B)<>ABS(X[A]-X[B]){A和B分别表示两个皇后的行号}

扩展一点DFS——路径搜索:

路径搜索是一个大问题,涉及很多的算法,也关系到生活生产中各个领域。

基础算法即回溯,“高深”一点的算法有最短路Dijkstra算法、欧拉(回)路算法、哈密顿(回)路问题、网络流、启发式搜索等。

回溯算法也有迭代加宽搜索(BFID)、迭代加深搜索(DFID)、A*算法、IDA*、分步搜索等。

在深度优先搜索的过程当中,往往有很多走不通的“死路”。

假如我们把这些“死路”排除在外,不是可以节省很多的时间吗?

打一个比方,前面有一个路径,别人已经提示:

“这是死路,肯定不通”,而你的程序仍然很“执着”地要继续朝这个方向走,走到头来才发现,别人的提示是正确的。

这样,浪费了很多的时间。

针对这种情况,我们可以把“死路”给标记一下不走,就可以得到更高的搜索效率,处理“死路”的这种方法就是启发式搜索、记忆化搜索。

对于一些有最优子结构的问题,我们往往采用动态规划算法来实现。

采用动态规划算法,需要弄清状态以及状态是如何转移的,接着列出状态转移方程。

首先举一个非常简单的例子

例题4数字三角形(经典问题)

分析无论对与新手还是老手,这都是再熟悉不过的题了,很容易地,我们写出状态转移方程:

f(i,j)=a[i,j]+min{f(i+1,j)+f(i,j+1)}

对于动态规划算法解决这个问题,我们根据状态转移方程和状态转移方向,比较容易地写出动态规划的循环表示方法。

但是,当状态和转移非常复杂的时候,也许写出循环式的动态规划就不是那么简单了。

我们尝试从正面的思路去分析问题,如上例,不难得出一个非常简单的递归过程:

if(i==0)return(a[i,j]);

f1=f(i+1,j);f2=f(i,j+1);

iff1>f2returna[i,j]+f1;elsereturna[i,j]+f2;

显而易见,这个算法就是最简单的搜索算法。

时间复杂度为2n,明显是会超时的。

分析一下搜索的过程,实际上,很多调用都是不必要的,也就是把产生过的最优状态,又产生了一次。

为了避免浪费,很显然,我们存放一个opt数组:

Opt[i,j]-每产生一个f(i,j),将f(i,j)的值放入opt中,以后再次调用到f(

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

当前位置:首页 > 工程科技 > 能源化工

copyright@ 2008-2023 冰点文库 网站版权所有

经营许可证编号:鄂ICP备19020893号-2