http://blog.csdn.net/whu_chf/article/details/7737479
递归与回溯
[算法分析]
为了描述问题的某一状态,必须用到它的上一状态,而描述上一状态,又必须用到它的上一状态……这
种用自已来定义自己的方法,称为递归定义。例如:定义函数f(n)为:
/n*f(n-1) (n>0)
f(n)= |
/ 1(n=0)
则当0时,须用f(n-1)来定义f(n),用f(n-1-1)来定义f(n-1)……当n=0时,f(n)=1。
由上例我们可看出,递归定义有两个要素:
(1)递归边界条件。也就是所描述问题的最简单情况,它本身不再使用递归的定义。
如上例,当n=0时,f(n)=1,不使用f(n-1)来定义。
(2)递归定义:使问题向边界条件转化的规则。递归定义必须能使问题越来越简单。
如上例:f(n)由f(n-1)定义,越来越靠近f(0),也即边界条件。最简单的情况是f(0)=1。
递归算法的效率往往很低, 费时和费内存空间. 但是递归也有其长处, 它能使一个蕴含递归关系且结构复杂的程序简介精炼,增加可读性. 特别是在难于找到从边界到解的全过程的情况下, 如果把问题推进一步没其结果仍维持原问题的关系, 则采用递归算法编程比较合适.
递归按其调用方式分为: 1. 直接递归, 递归过程P直接自己调用自己; 2. 间接递归, 即P包含另一过程D, 而D又调用P.
递归算法适用的一般场合为:
1. 数据的定义形式按递归定义.
如裴波那契数列的定义: f(n)=f(n-1)+f(n-2); f(0)=1; f(1)=2.
对应的递归程序为:
Function fib(n : integer) : integer;
Begin
if n = 0 then fib := 1 { 递归边界 }
else if n = 1 then fib := 2
else fib := fib(n-2) + fib(n-1) { 递归 }
End;
这类递归问题可转化为递推算法, 递归边界作为递推的边界条件.
2. 数据之间的关系(即数据结构)按递归定义. 如树的遍历, 图的搜索等.
3. 问题解法按递归算法实现. 例如回溯法等.
从问题的某一种可能出发, 搜索从这种情况出发所能达到的所有可能, 当这一条路走到" 尽头 "
的时候, 再倒回出发点, 从另一个可能出发, 继续搜索. 这种不断" 回溯 "寻找解的方法, 称作
" 回溯法 ".
[参考程序]
下面给出用回溯法求所有路径的算法框架. 注释已经写得非常清楚, 请读者仔细理解.
Const maxdepth = ????;
Type statetype = ??????; { 状态类型定义 }
operatertype = ??????; { 算符类型定义 }
node = Record { 结点类型 }
state : statetype; { 状态域 }
operater :operatertype { 算符域 }
End;
{ 注: 结点的数据类型可以根据试题需要简化 }
Var
stack : Array [1..maxdepth] of node; { 存当前路径 }
total : integer; { 路径数 }
Procedure make(l : integer);
Var i : integer;
Begin
if stack[L-1]是目标结点 then
Begin
total := total+1; { 路径数+1 }
打印当前路径[1..L-1];
Exit
End;
for i := 1 to 解答树次数 do
Begin
生成 stack[l].operater;
stack[l].operater 作用于 stack[l-1].state, 产生新状态 stack[l].state;
if stack[l].state 满足约束条件 then make(k+1);
{ 若不满足约束条件, 则通过for循环换一个算符扩展 }
{ 递归返回该处时, 系统自动恢复调用前的栈指针和算符, 再通过for循环换一个算符扩展 }
{ 注: 若在扩展stack[l].state时曾使用过全局变量, 则应插入若干语句, 恢复全局变量在
stack[l-1].state时的值. }
End;
{ 再无算符可用, 回溯 }
End;
Begin
total := 0; { 路径数初始化为0 }
初始化处理;
make(l);
打印路径数total
End.
[例子] 求N个数的全排列。
[分析]求N个数的全排列,可以看成把N个不同的球放入N个不同的盒子中,每个盒子中只能有一
个球。解法与八皇后问题相似。
[参考过程]
procedure try(I:integer);
var j:integer;
begin
for j:=1 to n do
if a[j]=0 then
begin
x[I]:=j;
a[j]:=1;
if I<n then try(I+1)
else print;
a[j]=0;
end;
end;
[习题] 用递归完成:
1、如下图,打印0-N的所有路径(0=〈N〈=9):
1―> 3―> 5―> 7―> 9
^ ^ ^ ^ ^
| | | | |
0―> 2―> 4―> 6―> 8
(说明:图中须加上0->3,2->5,4->7,6->9的连线)
2、快速排序。
3、打印杨辉三角。
最后, 给出一道经典的使用回溯算法解决的问题, 留给读者思考.
题目描述:
对于任意一个m*n的矩阵, 求L形骨牌覆盖后所剩方格数最少的一个方案.
素数环
〖问题描述〗
把从1到20这20个数摆成一个环,要求相邻的两个数的和是一个素数。
〖问题分析〗(聿怀中学 吴轲)
非常明显,这是一道回溯的题目。从1开始,每个空位有20(19)种可能,只要填进去的数合法:
与前面的数不相同;与左边相邻的数的和是一个素数。第20个数还要判断和第1个数的和是否素数。
〖算法流程〗
1、数据初始化;
2、递归填数:
判断第J种可能是否合法;
A、如果合法:填数;判断是否到达目标(20个已填完):是,打印结果;不是,递归填下一个;
B、如果不合法:选择下一种可能;
〖参考程序〗ssh.pas
八皇后问题
〖问题描述〗
在一个8×8的棋盘里放置8个皇后,要求每个皇后两两之间不相"冲"(在每一横列竖列斜列只有一个皇后)。
〖问题分析〗(聿怀中学 吕思博)
这道题可以用递归循环来做,分别一一测试每一种摆法,直到得出正确的答案。主要解决以下几个问题:
1、冲突。包括行、列、两条对角线:
(1)列:规定每一列放一个皇后,不会造成列上的冲突;
(2)行:当第I行被某个皇后占领后,则同一行上的所有空格都不能再放皇后,要把以I为下标的标记置为被占领状态;
(3)对角线:对角线有两个方向。在同一对角线上的所有点(设下标为(i,j)),要么(i+j)是常数,要么(i-j)是常数。因此,当第I个皇后占领了第J列后,要同时把以(i+j)、(i-j)为下标的标记置为被占领状态。
2、数据结构。
(1)解数组A。A[I]表示第I个皇后放置的列;范围:1..8
(2)行冲突标记数组B。B[I]=0表示第I行空闲;B[I]=1表示第I行被占领;范围:1..8
(3)对角线冲突标记数组C、D。
C[I-J]=0表示第(I-J)条对角线空闲;C[I-J]=1表示第(I-J)条对角线被占领;范围:-7..7
D[I+J]=0表示第(I+J)条对角线空闲;D[I+J]=1表示第(I+J)条对角线被占领;范围:2..16
〖算法流程〗
1、数据初始化。
2、从n列开始摆放第n个皇后(因为这样便可以符合每一竖列一个皇后的要求),先测试当前位置(n,m)是否等于0(未被占领):
如果是,摆放第n个皇后,并宣布占领(记得要横列竖列斜列一起来哦),接着进行递归;
如果不是,测试下一个位置(n,m+1),但是如果当n<=8,m=8时,却发现此时已经无法摆放时,便要进行回溯。
3、当n>8时,便一一打印出结果。
〖优点〗逐一测试标准答案,不会有漏网之鱼。
〖参考程序〗queen.pas
2的幂次方
〖问题描述〗
任何一个正整数都可以用2的幂次方表示.
例如:137=2^7+2^3+2^0
同时约定次方用括号来表示,即a^b可表示为a(b)
由此可知,137可表示为:2(7)+2(3)+2(0)
进一步:7=2^2+2+2^0 (2^1用2表示)
3=2+2^0
所以最后137可表示为:2(2(2)+2+2(0))+2(2+2(0))+2(0)
又如:1315=2^10+2^8+2^5+2+1
所以1315最后可表示为:2(2(2+2(0))+2)+2(2(2+2(0)))+2(2(2)+2(0))+2+2(0)
输入:正整数(n<=20000)
输出:符合约定的n的0,2表示(在表示中不能有空格)
〖问题分析〗
一、树的基本术语 |
分支限界 |
相关知识: 深度优先 | 广度优先:三角形 | 剪枝搜索| 递归与回溯 | 素数环 | 八皇后问题 | 跳马问题 | 实例:2的幂次方 |
一、动态规划的基本思想: 二、设计动态规划法的步骤: 三、动态规划问题的特征: 四、习题:
2、轮船(Ships)
|
动态规划在信息学奥林匹克竞赛中的应用
一.动态规划含义:
在现实生活中,有一类活动的过程,由于它的特殊性,可将过程分成若干个互相联系的阶段,在它的每一阶段都要做出决策,从而使整个过程达到最好的活动效果.因此,各个阶段决策确定后,组成一个决策序列,因而也就确定了整个过程的一条活动路线.这种把一个问题看作是一个前后关联具有链状结构的多阶段过程,就称为多阶段决策过程,这种问题称为多阶段决策问题.
在多阶段决策问题中,各个阶段采取的决策,一般来说是和时间有关的,决策依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,故有"动态"的含义,我们称这种解决多阶段决策最优化的过程为动态规划.
二.动态规划特征
动态规划的显著特征是:无后效性,有边界条件,且一般划分为很明显的阶段.
动态规划一般还存在一条或多条状态转移方程.
三.例题
1. Catcher防卫导弹 (GDOI'98)
题目讲得很麻烦,归根结底就是求一整串数中的最长不上升序列
这道题目一开始我使用回溯算法,大概可以拿到1/3的分吧,后来发现这其实是动态规划算法中最基础的题目,用一个二维数组C[1..Max,1..2]来建立动态规划状态转移方程(注:C[1..Max,1]表示当前状态最多可击落的导弹数,C[1..Max,2]表示当前状态的前继标志):Ci=Max{C[j]+1,(j=i+1..n)},然后程序也就不难实现了.
示范程序:
program catcher_hh;
var
f:text;
i,j,k,max,n,num:integer;
a:array [1..4000] of integer; {导弹高度数组}
c:array [1..4000,1..2] of integer; {动态规划数组}
procedure readfile;
begin
assign(f,'catcher.dat'); reset(f);
readln(f,num);
for i:=1 to num do
readln(f,a[i]);
end;
procedure work;
begin
fillchar(c,sizeof(c),0); c[num,1]:=1; {清空数组,赋初值}
{开始进行动态规划}
for i:=num-1 downto 1 do
begin
n:=0; max:=1;
for j:=i+1 to num do
if (a[i]>a[j]) and (max<1+c[j,1])
then begin n:=j; max:=1+c[j,1]; end;
c[i,1]:=max; c[i,2]:=n;
end;
writeln; writeln('Max : ',max); {打印最大值}
max:=0; n:=0;
for i:=1 to num do
if c[i,1]>max then begin max:=c[i,1]; n:=i; end;
{返回寻找路径}
repeat
writeln(n,' '); n:=c[n,2];
until n=0;
end;
begin
readfile; work;
end.
2. Perform巡回演出 (GDKOI'2000)
题目描述:
Flute市的Phlharmoniker乐团2000年准备到Harp市做一次大型演出,本着普及古典音乐的目的,乐团指挥L.Y.M准备在到达Harp市之前先在周围一些小城市作一段时间的巡回演出,此后的几天里,音乐家们将每天搭乘一个航班从一个城市飞到另一个城市,最后才到达目的地Harp市(乐团可多次在同一城市演出).
由于航线的费用和班次每天都在变,城市和城市之间都有一份循环的航班表,每一时间,每一方向,航班表循环的周期都可能不同.现要求寻找一张花费费用最小的演出表.
输入:
输入文件包括若干个场景.每个场景的描述由一对整数n(2<=n<=10)和k(1<=k<=1000)开始,音乐家们要在这n个城市作巡回演出,城市用1..n标号,其中1是起点Flute市,n是终点Harp市,接下来有n*(n-1)份航班表,一份航班表一行,描述每对城市之间的航线和价格,第一组n-1份航班表对应从城市1到其他城市(2,3,...n)的航班,接下的n-1行是从城市2到其他城市(1,3,4...n)的航班,如此下去.
每份航班又一个整数d(1<=d<=30)开始,表示航班表循环的周期,接下来的d个非负整数表示1,2...d天对应的两个城市的航班的价格,价格为零表示那天两个城市之间没有航班.例如"3 75 0 80"表示第一天机票价格是75KOI,第二天没有航班,第三天的机票是80KOI,然后循环:第四天又是75KOI,第五天没有航班,如此循环.输入文件由n=k=0的场景结束.
输出:
对每个场景如果乐团可能从城市1出发,每天都要飞往另一个城市,最后(经过k天)抵达城市n,则输出这k个航班价格之和的最小值.如果不可能存在这样的巡回演出路线,输出0.
样例输入:
3 6
2 130 150
3 75 0 80
7 120 110 0 100 110 120 0
4 60 70 60 50
3 0 135 140
2 70 80
2 3
2 0 70
1 80
0 0
样例输出:
460
0
初看这道题,很容易便可以想到动态规划,因为第x天在第y个地方的最优值只与第x-1天有关,符合动态规划的无后效性原则,即只与上一个状态相关联,而某一天x航班价格不难求出S=C[(x-1) mod m +1].我们用天数和地点来规划用一个数组A[1..1000,1..10]来存储,A[i,j]表示第i天到达第j个城市的最优值,C[i,j,l]表示i城市与j城市间第l天航班价格,则A[i,j]=Min{A[i-1,l]+C[l,j,i] (l=1..n且C[l,j,i]<>0)},动态规划方程一出,尽可以放怀大笑了.
示范程序:
program perform_hh;
var
f,fout:text;
p,l,i,j,n,k:integer;
a:array [1..1000,1..10] of integer; {动态规划数组}
c:array [1..10,1..10] of record {航班价格数组}
num:integer;
t:array [1..30] of integer;
end;
e:array [1..1000] of integer;
procedure work;
begin
{人工赋第一天各城市最优值}
for i:=1 to n do
begin
if c[1,i].t[1]<>0
then a[1,i]:=c[1,i].t[1];
end;
for i:=2 to k do
begin
for j:=1 to n do
begin
for l:=1 to n do
begin
if (j<>l)
and (c[l,j].t[(i-1) mod c[l,j].num+1]<>0) {判断存在航班}
and ((a[i,j]=0) or (a[i-1,l]+c[l,j].t[(i-1) mod c[l,j].num+1]<a[i,j])) {判断比当前解优}
then a[i,j]:=a[i-1,l]+c[l,j].t[(i-1) mod c[l,j].num+1]; {赋值}
end;
end;
end;
e[p]:=a[k,n]; {第p个场景的最优值}
end;
procedure readfile; {读文件}
begin
assign(f,'PERFORM.DAT'); reset(f);
assign(fout,'PERFORM.OUT'); rewrite(fout);
readln(f,n,k); p:=0;
while (n<>0) and (k<>0) do
begin
p:=p+1;
fillchar(c,sizeof(c),0);
fillchar(a,sizeof(a),0);
for i:=1 to n do
begin
for j:=1 to i-1 do
begin
read(f,c[i,j].num);
for l:=1 to c[i,j].num do
read(f,c[i,j].t[l]);
end;
for j:=i+1 to n do
begin
read(f,c[i,j].num);
for l:=1 to c[i,j].num do
read(f,c[i,j].t[l]);
end;
end;
work;
readln(f,n,k);
end;
{输出各个场景的解}
for i:=1 to p-1 do
writeln(fout,e[i]);
write(fout,e[p]);
close(f);
close(fout);
end;
begin
readfile;
end.
四.小结
动态规划与穷举法相比,大大减少了计算量,丰富了计算结果,不仅求出了当前状态到目标状态的最优值,而且同时求出了到中间状态的最优值,这对于很多实际问题来说是很有用的.这几年,动态规划已在各省市信息学奥林匹克竞赛中占据相当重要的地位,每年省赛8道题目中一般有2~3道题目属于动态规划,动态规划相比一般穷举也存在一定缺点:空间占据过多,但对于空间需求量不大的题目来说,动态规划无疑是最佳方法!
五.课后题目
1. m个人抄n本书,每本书页数已知,每个人(第一个人除外)都必须从上一个人抄的最后一本书的下一本抄起(书必须整本整本的抄),求一种分配方法,使抄书页数最多的人抄书页数尽可能少. (GDOI''99 Books).
2. 有一字符串有多种编码方式可供人选择,将这个字符串进行编码,使求得得编码长度最短。 (GDKOI'2000 Compress)
3. Canada境内有自西向东的一系列城市:Halifax,Hamilton,Montelia,Vancouver...,各个城市之间可能有航班相连,也可能没有,现要求从最西的城市出发,自西向东到达最东的城市,再返回最西的城市,除最西城市外,其他每个城市只能访问一次,求最多能访问多少个城市.
动态规划(一)
最短路径
〖题目描述〗
有如下有向图,边IJ上的数字为从点I走到点J的代价。求从点A1到点E1之间的最短路径。
〖题目描述〗
假设有M本书(编号为1,2,…M),想将每本复制一份,M本书的页数可能不同(分别是P1,P2,…PM)。任务是将这M本书分给K个抄写员(K〈=M〉,每本书只能分配给一个抄写员进行复制,而每个抄写员所分配到的书必须是连续顺序的。
意思是说,存在一个连续升序数列 0 = bo < b1 < b2 < … < bk-1 < bk = m,这样,第I号抄写员得到的书稿是从bi-1+1到第bi本书。复制工作是同时开始进行的,并且每个抄写员复制的速度都是一样的。所以,复制完所有书稿所需时间取决于分配得到最多工作的那个抄写员的复制时间。试找一个最优分配方案,使分配给每一个抄写员的页数的最大值尽可能小(如存在多个最优方案,只输出其中一种)。
(GDOI''99 Books).
〖输入格式〗
第一行两个数m,k:表示书本数目与人数;第二行m个由空格分隔的整数,m本书每本的页数.
〖输出格式〗
共k行。每行两个整数:第I行表示第I抄写员抄的书本编号起止。
〖输入输出样例〗
输入样例 | 输出样例 |
9 3 | 1 5 |
Book.pas
过桥问题
〖题目描述〗
GDOI工作组遇到了一个运输货物的问题。现在有N辆车要按顺序通过一个单向的小桥,由于小桥太窄,不能有两辆车并排通过,所以在桥上不能超车。另外,由于小桥建造的时间已经很久,所以小桥只能承受有限的重量,记为Max(吨)。所以,车辆在过桥的时候必须要有管理员控制,将这N辆车按初始顺序分组,每次让一个组过桥,并且只有在一个组中所有的车辆全部过桥以后才让下一组车辆上桥。现在,每辆车的重量和最在速度是已知的,而每组车的过桥时间由该组中速度最慢的那辆车决定。
现在请你编一个程序,将这N辆车分组,使得全部车辆通过小桥的时间最短。
输入格式:
数据存放在当前目录下的文本文件“bridge.in”中。
文件的第一行有三个数,分别为Max(吨),Len(桥的长度,单位:Km),N(三个数之间用一个或多个空格分开)。
接下来有N行,每行两个数,第i行的两个数分别表示第i辆车的重量(吨)和最大速度(m/s)。
注意:所有的输入都为整数,并且任何一辆车的重量都不会超过Max。
输出格式:
答案输出到当前目录下的文本文件“bridge.out”中。
文件只有一行,输出全部车辆通过小桥的最短时间(s),精确到小数点后一位。
输入输出样例:
bridge.in | bridge.out |
100 5 10 | 75.0 |
Bridge.pas
石子归并
〖题目描述〗
在一个圆形操场的四周摆放着N堆石子(N<= 100),现要将石子有次序地合并成一堆.规定每次只能选取相邻的两堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分.编一程序,由文件读入堆栈数N及每堆栈的石子数(<=20).
(!)选择一种合并石子的方案,使用权得做N-1次合并,得分的总和最小;
(2)选择一种合并石子的方案,使用权得做N-1次合并,得分的总和最大;
输入数据:
第一行为石子堆数N;
第二行为每堆的石子数,每两个数之间用一个空格分隔.
输出数据:
从第一至第N行为得分最小的合并方案.第N+1行是空行.从第N+2行到第2N+1行是得分最大合并方案.每种合并方案用N行表示,其中第i行(1<=i<=N)表示第i次合并前各堆的石子数(依顺时针次序输出,哪一堆先输出均可).要求将待合并的两堆石子数以相应的负数表示.
输入输出范例:
输入:
4
4 5 9 4
输出:
-4 5 9 -4
-8 -5 9
-13 -9
22
4 -5 -9 4
4 -14 -4
-4 -18
22
Stone.pas