深度优先搜索
深度优先搜索所遵循的搜索策略是尽可能“深”地搜索图。在深度优先搜索中,对于最新发现的结点,如果它还有以此为起点而未搜索的边,就沿此边继续搜索下去。当结点v的所有边都已被探寻过,搜索将回溯到发现结点v有那条边的始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个过程反复进行直到所有结点都被发现为止。
深度优先搜索的基本算法如下:
procedure dfs_try(i) {递归算法的过程}
for i:=1 to maxr do
begin
if 子结点mr符合条件 then
begin
产生的子结点mr入栈;
if 子结点mr是目标结点 then 输出;
else dfs_try(i+1);
栈顶元素出栈;
end;
end;
procedure dfs(dep) {非递归算法的过程}
dep:=0;
repeat
dep:=dep+1;
j:=0;p:=false;
repeat
j:=j+1;
if mr 符合条件 then
begin
产生子结点mr并将其记录;
if 子结点是目标结点 then 输出并出栈;
else p:=true;
end
else
if j>=maxj then
begin
dep:=dep-1; {回溯}
if dep>0 then 取回栈顶元素;
else p:=true;
end
else p:=false;
until p=true;
until p=0;
全排列-基本算法
采用深度优先搜索算法,即对每一位上的数进行枚举而求得所有排列。一般利用递归完成。全排列是将一组数按一定顺序进行排列,如果这组数有n个,那么全排列数为n!个。
深度优先搜索法有两大基本特点:
1. 对已产生的结点按深度排序,深度大的先得到扩展,即先产生它的子结点;
2. 深度大的结点是后产生的,但先得到扩展,即“后产生先扩展”。因此用堆栈作为该算法的主要数据结构,存储产生的结点,先把产生的数入栈,产生栈顶(即深度最大的元素)子结点,子结点产生以后,出栈,再产生栈顶子结点。
例5 输入n,输出1,2,…,n的全排列(n≤8)。
输入输出示例
sample input:
3
sample output:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
【算法分析】
输出全排列是一经典搜索问题。关键在于:数字不能重复出现。所以用一个数组vis记录每个数字是否出现过。每确定一个数字,就把这个数字的vis值设为true,需要注意的是:回溯的时候需要把vis重新设为false。
【参考程序】
var
n:integer;
a:array[1..8] of integer;
vis:array[1..8] of boolean;
procedure search(depth:integer);
var i:integer;
begin
if (depth>n) then
begin
for i:=1 to n-1 do
write(a[i],′′);
writeln(a[n]);
exit;
end;
for i:=1 to n do {枚举下一个出现的数字}
if not vis[i] then {判断是否出现过}
begin
a[depth]:=i; {没有出现过,把第depth个数设为i}
vis[i]:=true; {i出现过}
search(depth+1); {递归}
vis[i]:=false; {恢复i}
end;
end;{search}
begin
read(n);
fillchar(vis,sizeof(vis),false); {全部没有出现}
search(1);
end.
附: exit 是退出当前程序块。即在任何子程序中执行 exit , 那么将退出这个子程序。如果是在主程序中执行 exit,那么将退出整个程序。
相当于 goto 这个程序块的末尾的 end 。
例如:试除法判断素数时,一旦整除,就把函数值赋为false ,然后exit。
注意:类似上面,exit 也是只对当前这一个子程序产生作用,如果多重嵌套子程序,那么其中某个子程序执行了exit 以后,将返回到调用它的那个语句的下一个语句。
例如:
function f(x:longint):longint;
var
begin
if ... then exit(f(x+1)); <--这里指f:=f(x+1);把f(x+1)当作答案退出.
...
end;
例6 输入n,输出集合{1,2,…,n}的所有子集(n≤8)。
输入输出示例
sample input:
3
sample output:
{}
{3}
{2}
{2,3}
{1}
{1,3}
{1,2}
{1,2,3}
【算法分析】
输出所有子集也是一经典搜索问题。
设S是一个{1,2,…,n}的子集。可以这样认为:一个元素要么属于S,要么不属于S,只有这两种情况。
【参考程序】
var
n:integer;
size:integer;
s:array[1..8] of integer; {子集元素个数}
procedure search(depth:integer); {子集元素}
var i:integer;
begin
if (depth>n) then
begin
write(′{′);
for i:=1 to size-1 do
write(s[i],′,′);
if (size>0) then
write(s[size]);
writeln(′}′)
exit;
end;
search(depth+1); {depth不属于子集}
size:=size+1;
s[size]:=depth;
search(depth+1); {depth属于子集}
size:=size-1;
end;{search}
begin
read(n);
size:=0;
search(1);
end.
以上两种搜索都是非常常用的方式,很多复杂的题目都可以转化为这种模式。
例7 resume问题
小修是个几何迷。她有一天画了一个正n边形,并且将n个顶点用1,2,…,n这n个连续自然数随手编了一下号。然后她又画了一些不相交的对角线,如图4-3所示。顶点旁的数字是小修随手编的号。她把所有的边和对角线都写在一张纸上。对图4-3,她写了:(1,3),(3,2),(2,4),(4,5),(5,1),(1,4),(3,4)。
过了几个星期,她无意中发现了这张写着字的纸,可是怎么也找不着那个几何图形了。她很想把n边形的编号复原,可是试了一天也没弄出来。你能帮助她吗?
输入数据
第一行n(n≤50)。
下面的若干行每行两个数a,b,表示纸上写着(a,b)。
输出数据
仅一行,按顺序依次输出顶点的编号,每两个之间用一个空格
隔开,行末不要有多余空格。对于上面的例子,你的输出应该是1 3 2 4 5。
1 5 4 2 3也是符合题目要求的。两者区别只是逆时针和顺时针而已。
但是你的输出只能是1 3 2 4 5!也就是说你必须把两个符合要求的输出比较大小
(先比较第一位;第一位相等就比较第二位;第二位相等……以此类推),你的输出
应该是较小者。
输入输出示例
sample input:
5
1 3
3 2
2 4
4 5
5 1
1 4
3 4
sample output:
1 3 2 4 5
【算法分析】
首先我们不要被题目所迷惑,我们可以抛开点之间的几何位置,其实这个n多边形的顶点顺序其实就是一个1到n的排列,,…,但是,要求满足-,-,…-,- 之间有边(其实就是图论中的哈密尔顿圈)。
由于题目中n≤50,所以我们只要一一枚举每一个排列,判断一下就可以了,本题已经和第一题非常相似了。
另外还有一点,输出要求最小。处理这一点的方法就是从小到大枚举下一个点的编号。起点的选择也应该从小到大(其实以1为始点肯定可以找到一个满足条件的排列)。
【参考程序】
const maxn=50;
var
n:integer;
a:array[1..maxn] of integer;
g:array[1..maxn,1..maxn] of Boolean; {g[i,j]表示i,j之间是否有边}
vis:array[1..maxn] of Boolean;
procedure init;
var
u,v:integer;
begin
read(n);
fillchar(g,sizeof(g),false);
while not seekeof do {seekeof用于判断文件是否结束}
begin
read(u,v);
g[u,v]:=true;
g[v,u]:=true;
end;
end;{init}
procedure search(depth:integer);
var i:integer;
begin
if (depth>n) then
begin {输出结果}
if (g[a[1],a[n]]) then
begin
for i:=1 to n-1 do
write(a[i],′′);
writeln(a[n]);
halt;
end;
exit;
end;
for i:=1 to n do {枚举下一个点的编号}
if (not vis[i]) and (g[a[depth-1],i]) then
begin
a[depth]:=i;
vis[i]:=true;
search(depth+1);
vis[i]:=false;
end;
end;{search}
begin
assign(input,'input.txt');reset(input);
assign(output,'output.txt');rewrite(output);
init;
fillchar(vis,sizeof(vis),false);
a[1]:=1;
vis[1]:=true;
search(2);
end.
附:SeekEoln和Eoln用于判断文件行结束,SeekEof和Eof用于判断文件结束,但它们之间还是有区别的。Eoln和Eof只判断当前的所在位置是否位于行结束符上或者文件尾部,而SeekEoln和SeekEof会自动跳过所有连续的空格和制表符再进行判断,因此,在执行函数SeekEoln或SeekEof后,当前的所在位置不会是空格或者制表符。
halt 直接中断该程序
exit 跳出过程或函数. 如果在主程序,则效果和halt一样。在函数中,exit(i);表示把i的值赋给函数并退出函数。
break 跳出本层循环
continue:跳过执行的这步for循环。
例8 染色问题
给定一张无向图,要求对图进行染色,有边相连的点不能染同一种颜色。求最少需要几种颜色可以染完。
输入数据:
n,m表示图的点数和边数。(n≤10)
m行每行两个整数,表示一条边。
输出数据:
最少需要的颜色数。
输入输出示例:
sample input:
5 5
1 2
2 3
3 4
4 5
5 1
sample output:
3
【算法分析】
题目要求求最小需要的颜色数目。如果图可以用k种颜色染,它一定可以用k+1种颜色染,同样如果图不可以用k种颜色染,它一定不可以用k-1种颜色染。所以可以从小到大依次枚举k,然后判断用k中颜色是否可以染。如何判断用k种颜色是否可以染呢?可以枚举每一个结点的颜色,判断是否有相连的点同色。
【参考程序】
const maxn=10;
var n,m,k:integer;
colour:array[1..maxn] of integer;
g:array[1..maxn,1..maxn] of boolean; {g[i,j]表示i,j之间是否有边}
procedure init;
var i,u,v:integer;
begin
read(n,m);
fillchar(g,sizeof(g),false);
for i:=1 to m do
begin
read(u,v);
g[u,v]:=true;
g[v,u]:=true;
end;
end;{init}
procedure search(depth:integer);
var i:integer;
r:array[1..maxn] of boolean;
begin
if (depth>n) then
begin
writeln(k);
close(input);close(output);
halt;
end;
fillchar(r,sizeof(r),false); {r[i]表示与depth相连的点中是否有颜色为i的}
for i:=1 to depth-1 do
if (g[depth,i]) then
r[colour[i]]:=true;
for i:=1 to k do
if not r[i] then
begin
colour[depth]:=i;
search(depth+1);
end;
end;{search}
begin
assign(input,'input.txt');reset(input);
assign(output,'output.txt');rewrite(output);
init;
for k:=1 to n do search(1);
end.